From 39baf6e7751b8f2b4e9b234ff586fc5b17e13b10 Mon Sep 17 00:00:00 2001 From: useruserdev <256019073+useruserdev@users.noreply.github.com> Date: Thu, 4 Jun 2026 00:46:50 +0500 Subject: [PATCH] Fix sideload install: drop background task / UIBackgroundModes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feather failed with 'Unable to Install' (stuck at sending payload). The only bundle difference from the last installable build (v1.0.7) was the background-fetch capability (UIBackgroundModes + BGTaskSchedulerPermittedIdentifiers), which sideload profiles (free cert) reject. - Remove BackgroundRefresh (BGAppRefreshTask) and the background-mode Info.plist keys. - Revert to the generated Info.plist (GENERATE_INFOPLIST_FILE) as in v1.0.7 — no manual plist. - Subscriptions now refresh on app launch / scenePhase active and pull-to-refresh; change notifications still fire and are shown in the foreground via UNUserNotificationCenterDelegate. - Drop the background-refresh toggle and interval from Settings. - Bump to 1.0.3 (build 4). --- ios/happwn/Core/BackgroundRefresh.swift | 43 -------------------- ios/happwn/Core/NotificationService.swift | 17 ++++++++ ios/happwn/Info.plist | 49 ----------------------- ios/happwn/Store/Settings.swift | 21 ---------- ios/happwn/UI/SettingsView.swift | 15 +------ ios/happwn/happwnApp.swift | 21 ++-------- ios/project.yml | 8 +--- 7 files changed, 23 insertions(+), 151 deletions(-) delete mode 100644 ios/happwn/Core/BackgroundRefresh.swift delete mode 100644 ios/happwn/Info.plist diff --git a/ios/happwn/Core/BackgroundRefresh.swift b/ios/happwn/Core/BackgroundRefresh.swift deleted file mode 100644 index fa1e69e..0000000 --- a/ios/happwn/Core/BackgroundRefresh.swift +++ /dev/null @@ -1,43 +0,0 @@ -import Foundation -import BackgroundTasks - -/// Registration and scheduling of the opportunistic background refresh task. -/// iOS decides when to actually run it (roughly based on app usage); this is -/// not a guaranteed fixed-interval timer. -enum BackgroundRefresh { - static let taskID = "com.happwn.refresh" - - /// Register the task handler. Must be called before the app finishes launching. - static func register(coordinator: @escaping () -> RefreshCoordinator, - minInterval: @escaping () -> TimeInterval) { - BGTaskScheduler.shared.register(forTaskWithIdentifier: taskID, using: nil) { task in - guard let task = task as? BGAppRefreshTask else { return } - handle(task: task, coordinator: coordinator(), minInterval: minInterval()) - } - } - - /// Ask the system to schedule the next refresh no sooner than `minInterval`. - static func schedule(minInterval: TimeInterval) { - let request = BGAppRefreshTaskRequest(identifier: taskID) - request.earliestBeginDate = Date(timeIntervalSinceNow: minInterval) - try? BGTaskScheduler.shared.submit(request) - } - - static func cancel() { - BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: taskID) - } - - private static func handle(task: BGAppRefreshTask, coordinator: RefreshCoordinator, minInterval: TimeInterval) { - // Always line up the next opportunity. - schedule(minInterval: minInterval) - - let work = Task { - await coordinator.refreshAll() - task.setTaskCompleted(success: true) - } - task.expirationHandler = { - work.cancel() - task.setTaskCompleted(success: false) - } - } -} diff --git a/ios/happwn/Core/NotificationService.swift b/ios/happwn/Core/NotificationService.swift index e7ceffc..2d6594d 100644 --- a/ios/happwn/Core/NotificationService.swift +++ b/ios/happwn/Core/NotificationService.swift @@ -7,10 +7,27 @@ protocol SubscriptionNotifying { func notifyChange(subscription: SavedSubscription, added: Int, removed: Int) async } +/// Lets change notifications appear as a banner even while the app is open +/// (refreshes happen on launch / pull-to-refresh, i.e. in the foreground). +final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate { + static let shared = NotificationDelegate() + + func userNotificationCenter(_ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + completionHandler([.banner, .sound, .list]) + } +} + struct NotificationService: SubscriptionNotifying { /// userInfo key carrying the subscription id (for deep-linking on tap). static let subscriptionIDKey = "subscriptionID" + /// Show notifications while the app is in the foreground. + func enableForegroundPresentation() { + UNUserNotificationCenter.current().delegate = NotificationDelegate.shared + } + /// Request authorization; returns whether it was granted. @discardableResult func requestAuthorization() async -> Bool { diff --git a/ios/happwn/Info.plist b/ios/happwn/Info.plist deleted file mode 100644 index bd4609b..0000000 --- a/ios/happwn/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDisplayName - happwn - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(MARKETING_VERSION) - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - LSRequiresIPhoneOS - - UILaunchScreen - - UIApplicationSupportsIndirectInputEvents - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIBackgroundModes - - fetch - - BGTaskSchedulerPermittedIdentifiers - - com.happwn.refresh - - - diff --git a/ios/happwn/Store/Settings.swift b/ios/happwn/Store/Settings.swift index ae51931..386e627 100644 --- a/ios/happwn/Store/Settings.swift +++ b/ios/happwn/Store/Settings.swift @@ -1,16 +1,6 @@ import Foundation import Combine -/// How long the background refresh waits, at minimum, between runs. -/// iOS treats this as a floor, not a guarantee. -enum RefreshInterval: Int, CaseIterable, Identifiable { - case h1 = 1, h3 = 3, h6 = 6, h12 = 12 - - var id: Int { rawValue } - var seconds: TimeInterval { TimeInterval(rawValue) * 3600 } - var label: String { "\(rawValue) ч" } -} - /// User-editable request identity, appearance, and refresh prefs, persisted in UserDefaults. final class Settings: ObservableObject { @Published var userAgent: String { @@ -28,12 +18,6 @@ final class Settings: ObservableObject { @Published var notificationsEnabled: Bool { didSet { defaults.set(notificationsEnabled, forKey: Keys.notifications) } } - @Published var backgroundRefreshEnabled: Bool { - didSet { defaults.set(backgroundRefreshEnabled, forKey: Keys.backgroundRefresh) } - } - @Published var minRefreshInterval: RefreshInterval { - didSet { defaults.set(minRefreshInterval.rawValue, forKey: Keys.refreshInterval) } - } private let defaults: UserDefaults @@ -43,8 +27,6 @@ final class Settings: ObservableObject { static let accent = "happwn.accent" static let appearance = "happwn.appearance" static let notifications = "happwn.notificationsEnabled" - static let backgroundRefresh = "happwn.backgroundRefreshEnabled" - static let refreshInterval = "happwn.minRefreshInterval" } init(defaults: UserDefaults = .standard) { @@ -55,8 +37,5 @@ final class Settings: ObservableObject { self.appearance = AppAppearance(rawValue: defaults.string(forKey: Keys.appearance) ?? "") ?? .system // Default ON so the app notifies about config changes (e.g. blocks) out of the box. self.notificationsEnabled = defaults.object(forKey: Keys.notifications) as? Bool ?? true - self.backgroundRefreshEnabled = defaults.object(forKey: Keys.backgroundRefresh) as? Bool ?? true - let storedInterval = defaults.integer(forKey: Keys.refreshInterval) - self.minRefreshInterval = RefreshInterval(rawValue: storedInterval) ?? .h3 } } diff --git a/ios/happwn/UI/SettingsView.swift b/ios/happwn/UI/SettingsView.swift index 02911f1..278dbfe 100644 --- a/ios/happwn/UI/SettingsView.swift +++ b/ios/happwn/UI/SettingsView.swift @@ -37,23 +37,10 @@ struct SettingsView: View { Task { await NotificationService().requestAuthorization() } } } - Toggle("Фоновое обновление", isOn: $settings.backgroundRefreshEnabled) - .onChange(of: settings.backgroundRefreshEnabled) { enabled in - if enabled { - BackgroundRefresh.schedule(minInterval: settings.minRefreshInterval.seconds) - } else { - BackgroundRefresh.cancel() - } - } - Picker("Проверять не чаще чем", selection: $settings.minRefreshInterval) { - ForEach(RefreshInterval.allCases) { interval in - Text(interval.label).tag(interval) - } - } } header: { Text("Обновления") } footer: { - Text("iOS запускает фоновое обновление по своему усмотрению, ориентируясь на то, как часто ты открываешь приложение — точный интервал не гарантирован.") + Text("Подписки обновляются при открытии приложения и по pull-to-refresh. Если набор конфигов изменился — придёт уведомление.") } Section { diff --git a/ios/happwn/happwnApp.swift b/ios/happwn/happwnApp.swift index 47deb06..7913c4a 100644 --- a/ios/happwn/happwnApp.swift +++ b/ios/happwn/happwnApp.swift @@ -10,15 +10,9 @@ struct HappwnApp: App { init() { let settings = Settings() let store = SubscriptionStore() - let coordinator = RefreshCoordinator(store: store, settings: settings) _settings = StateObject(wrappedValue: settings) _store = StateObject(wrappedValue: store) - _coordinator = StateObject(wrappedValue: coordinator) - - BackgroundRefresh.register( - coordinator: { coordinator }, - minInterval: { settings.minRefreshInterval.seconds } - ) + _coordinator = StateObject(wrappedValue: RefreshCoordinator(store: store, settings: settings)) } var body: some Scene { @@ -30,23 +24,14 @@ struct HappwnApp: App { .tint(settings.accent.color) .preferredColorScheme(settings.appearance.colorScheme) .task { + NotificationService().enableForegroundPresentation() if settings.notificationsEnabled { await NotificationService().requestAuthorization() } - if settings.backgroundRefreshEnabled { - BackgroundRefresh.schedule(minInterval: settings.minRefreshInterval.seconds) - } } .onChange(of: scenePhase) { phase in - switch phase { - case .active: + if phase == .active { Task { await coordinator.refreshAll() } - case .background: - if settings.backgroundRefreshEnabled { - BackgroundRefresh.schedule(minInterval: settings.minRefreshInterval.seconds) - } - default: - break } } } diff --git a/ios/project.yml b/ios/project.yml index 33eacb9..85b9ea8 100644 --- a/ios/project.yml +++ b/ios/project.yml @@ -8,8 +8,8 @@ settings: GENERATE_INFOPLIST_FILE: YES INFOPLIST_KEY_UILaunchScreen_Generation: YES INFOPLIST_KEY_CFBundleDisplayName: happwn - MARKETING_VERSION: "1.0.2" - CURRENT_PROJECT_VERSION: "3" + MARKETING_VERSION: "1.0.3" + CURRENT_PROJECT_VERSION: "4" SWIFT_VERSION: "5.0" TARGETED_DEVICE_FAMILY: "1,2" targets: @@ -18,14 +18,10 @@ targets: platform: iOS sources: - path: happwn - excludes: - - "Info.plist" settings: base: PRODUCT_BUNDLE_IDENTIFIER: com.happwn.app SWIFT_OBJC_BRIDGING_HEADER: happwn/Happwn-Bridging-Header.h - GENERATE_INFOPLIST_FILE: NO - INFOPLIST_FILE: happwn/Info.plist dependencies: - framework: HappwnCrypto.xcframework embed: false