diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 176b6cdcf..169bf35ab 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -6587,7 +6587,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 414; + CURRENT_PROJECT_VERSION = 415; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -6659,7 +6659,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 414; + CURRENT_PROJECT_VERSION = 415; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -6724,7 +6724,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 414; + CURRENT_PROJECT_VERSION = 415; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -6798,7 +6798,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 414; + CURRENT_PROJECT_VERSION = 415; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -7706,7 +7706,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 414; + CURRENT_PROJECT_VERSION = 415; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7777,7 +7777,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 414; + CURRENT_PROJECT_VERSION = 415; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/Session/Notifications/SyncPushTokensJob.swift b/Session/Notifications/SyncPushTokensJob.swift index 2a93618fb..2df9657b3 100644 --- a/Session/Notifications/SyncPushTokensJob.swift +++ b/Session/Notifications/SyncPushTokensJob.swift @@ -42,75 +42,97 @@ public enum SyncPushTokensJob: JobExecutor { } }() - // Push tokens don't normally change while the app is launched, so you would assume checking once - // during launch is sufficient, but e.g. on iOS11, users who have disabled "Allow Notifications" - // and disabled "Background App Refresh" will not be able to obtain an APN token. Enabling those - // settings does not restart the app, so we check every activation for users who haven't yet - // registered. - // - // It's also possible for a device to successfully register for push notifications but fail to - // register with Session - // - // Due to the above we want to re-register at least once every ~12 hours to ensure users will - // continue to receive push notifications - // - // In addition to this if we are custom running the job (eg. by toggling the push notification - // setting) then we should run regardless of the other settings so users have a mechanism to force - // the registration to run - let lastPushNotificationSync: Date = UserDefaults.standard[.lastPushNotificationSync] - .defaulting(to: Date.distantPast) - - guard - job.behaviour == .runOnce || - !isRegisteredForRemoteNotifications || - Date().timeIntervalSince(lastPushNotificationSync) >= SyncPushTokensJob.maxFrequency - else { - SNLog("[SyncPushTokensJob] Deferred due to Fast Mode disabled or recent-enough registration") + // Apple's documentation states that we should re-register for notifications on every launch: + // https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/HandlingRemoteNotifications.html#//apple_ref/doc/uid/TP40008194-CH6-SW1 + guard job.behaviour == .runOnce || !isRegisteredForRemoteNotifications else { + SNLog("[SyncPushTokensJob] Deferred due to Fast Mode disabled") deferred(job) // Don't need to do anything if push notifications are already registered return } - Logger.info("Re-registering for remote notifications.") + // Determine if the device has 'Fast Mode' (APNS) enabled + let isUsingFullAPNs: Bool = UserDefaults.standard[.isUsingFullAPNs] - // Perform device registration - PushRegistrationManager.shared.requestPushTokens() - .subscribe(on: queue) - .flatMap { (pushToken: String, voipToken: String) -> AnyPublisher in - Deferred { - Future { resolver in - SyncPushTokensJob.registerForPushNotifications( - pushToken: pushToken, - voipToken: voipToken, - isForcedUpdate: true, - success: { resolver(Result.success(())) }, - failure: { resolver(Result.failure($0)) } - ) + // If the job is running and 'Fast Mode' is disabled then we should try to unregister the existing + // token + guard isUsingFullAPNs else { + Just(Storage.shared[.lastRecordedPushToken]) + .setFailureType(to: Error.self) + .flatMap { lastRecordedPushToken in + if let existingToken: String = lastRecordedPushToken { + SNLog("[SyncPushTokensJob] Unregister using last recorded push token: \(redact(existingToken))") + return Just(existingToken) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() } + + SNLog("[SyncPushTokensJob] Unregister using live token provided from device") + return PushRegistrationManager.shared.requestPushTokens() + .map { token, _ in token } + .eraseToAnyPublisher() } - .handleEvents( + .flatMap { pushToken in PushNotificationAPI.unregister(Data(hex: pushToken)) } + .map { + // Tell the device to unregister for remote notifications (essentially try to invalidate + // the token if needed + DispatchQueue.main.sync { UIApplication.shared.unregisterForRemoteNotifications() } + + Storage.shared.write { db in + db[.lastRecordedPushToken] = nil + } + return () + } + .subscribe(on: queue) + .sinkUntilComplete( receiveCompletion: { result in switch result { - case .failure(let error): - SNLog("[SyncPushTokensJob] Failed to register due to error: \(error)") - - case .finished: - Logger.warn("Recording push tokens locally. pushToken: \(redact(pushToken)), voipToken: \(redact(voipToken))") - SNLog("[SyncPushTokensJob] Completed") - UserDefaults.standard[.lastPushNotificationSync] = Date() - - Storage.shared.write { db in - db[.lastRecordedPushToken] = pushToken - db[.lastRecordedVoipToken] = voipToken - } + case .finished: SNLog("[SyncPushTokensJob] Unregister Completed") + case .failure: SNLog("[SyncPushTokensJob] Unregister Failed") } + + // We want to complete this job regardless of success or failure + success(job, false) } ) - .eraseToAnyPublisher() + return + } + + // Perform device registration + Logger.info("Re-registering for remote notifications.") + PushRegistrationManager.shared.requestPushTokens() + .flatMap { (pushToken: String, voipToken: String) -> AnyPublisher in + PushNotificationAPI + .register( + with: Data(hex: pushToken), + publicKey: getUserHexEncodedPublicKey(), + isForcedUpdate: true + ) + .retry(3) + .handleEvents( + receiveCompletion: { result in + switch result { + case .failure(let error): + SNLog("[SyncPushTokensJob] Failed to register due to error: \(error)") + + case .finished: + Logger.warn("Recording push tokens locally. pushToken: \(redact(pushToken)), voipToken: \(redact(voipToken))") + SNLog("[SyncPushTokensJob] Completed") + UserDefaults.standard[.lastPushNotificationSync] = Date() + + Storage.shared.write { db in + db[.lastRecordedPushToken] = pushToken + db[.lastRecordedVoipToken] = voipToken + } + } + } + ) + .map { _ in () } + .eraseToAnyPublisher() } + .subscribe(on: queue) .sinkUntilComplete( // We want to complete this job regardless of success or failure - receiveCompletion: { _ in success(job, false) }, - receiveValue: { _ in } + receiveCompletion: { _ in success(job, false) } ) } @@ -147,68 +169,3 @@ extension SyncPushTokensJob { private func redact(_ string: String) -> String { return OWSIsDebugBuild() ? string : "[ READACTED \(string.prefix(2))...\(string.suffix(2)) ]" } - -extension SyncPushTokensJob { - fileprivate static func registerForPushNotifications( - pushToken: String, - voipToken: String, - isForcedUpdate: Bool, - success: @escaping () -> (), - failure: @escaping (Error) -> (), - remainingRetries: Int = 3 - ) { - let isUsingFullAPNs: Bool = UserDefaults.standard[.isUsingFullAPNs] - - Just(Data(hex: pushToken)) - .setFailureType(to: Error.self) - .flatMap { pushTokenAsData -> AnyPublisher in - guard isUsingFullAPNs else { - return PushNotificationAPI.unregister(pushTokenAsData) - .map { _ in true } - .eraseToAnyPublisher() - } - - return PushNotificationAPI - .register( - with: pushTokenAsData, - publicKey: getUserHexEncodedPublicKey(), - isForcedUpdate: isForcedUpdate - ) - .map { _ in true } - .eraseToAnyPublisher() - } - .catch { error -> AnyPublisher in - guard remainingRetries == 0 else { - SyncPushTokensJob.registerForPushNotifications( - pushToken: pushToken, - voipToken: voipToken, - isForcedUpdate: isForcedUpdate, - success: success, - failure: failure, - remainingRetries: (remainingRetries - 1) - ) - - return Just(false) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - - return Fail(error: error) - .eraseToAnyPublisher() - } - .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .sinkUntilComplete( - receiveCompletion: { result in - switch result { - case .finished: break - case .failure(let error): failure(error) - } - }, - receiveValue: { didComplete in - guard didComplete else { return } - - success() - } - ) - } -} diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 0526360a4..2db3e27ba 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -237,7 +237,8 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension }, migrationsCompletion: { [weak self] result, needsConfigSync in switch result { - case .failure: SNLog("[NotificationServiceExtension] Failed to complete migrations") + // Only 'NSLog' works in the extension - viewable via Console.app + case .failure: NSLog("[NotificationServiceExtension] Failed to complete migrations") case .success: DispatchQueue.main.async { self?.versionMigrationsDidComplete(needsConfigSync: needsConfigSync)