diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index aeaf6decf8..2d427d20a7 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -19,12 +19,13 @@ class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let token = deviceToken.map { String(format: "%02hhx", $0) }.joined() logger.debug("AppDelegate: didRegisterForRemoteNotificationsWithDeviceToken \(token)") - ChatModel.shared.deviceToken = token + let m = ChatModel.shared + m.deviceToken = token let useNotifications = UserDefaults.standard.bool(forKey: "useNotifications") if useNotifications { Task { do { - try await apiRegisterToken(token: token) + m.tokenStatus = try await apiRegisterToken(token: token) } catch { logger.error("apiRegisterToken error: \(responseError(error))") } @@ -47,10 +48,16 @@ class AppDelegate: NSObject, UIApplicationDelegate { if let token = ChatModel.shared.deviceToken { logger.debug("AppDelegate: didReceiveRemoteNotification: verification, confirming \(verification)") Task { + let m = ChatModel.shared do { + if case .active = m.tokenStatus {} else { m.tokenStatus = .confirmed } try await apiVerifyToken(token: token, code: verification, nonce: nonce) + m.tokenStatus = .active try await apiIntervalNofication(token: token, interval: 20) } catch { + if let cr = error as? ChatResponse, case .chatCmdError(.errorAgent(.NTF(.AUTH))) = cr { + m.tokenStatus = .expired + } logger.error("AppDelegate: didReceiveRemoteNotification: apiVerifyToken or apiIntervalNofication error: \(responseError(error))") } completionHandler(.newData) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index a2846d825a..2a747881f0 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -24,6 +24,7 @@ final class ChatModel: ObservableObject { @Published var userSMPServers: [String]? @Published var appOpenUrl: URL? @Published var deviceToken: String? + @Published var tokenStatus = NtfTknStatus.new var messageDelivery: Dictionary Void> = [:] @@ -930,3 +931,12 @@ struct LinkPreview: Codable { var description: String = "" var image: String } + +enum NtfTknStatus: String, Decodable { + case new = "NEW" + case registered = "REGISTERED" + case invalid = "INVALID" + case confirmed = "CONFIRMED" + case active = "ACTIVE" + case expired = "EXPIRED" +} diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 5067cd4b0f..85438dd285 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -173,6 +173,7 @@ enum ChatResponse: Decodable, Error { case chatItemDeleted(deletedChatItem: AChatItem, toChatItem: AChatItem) case rcvFileAccepted case rcvFileComplete(chatItem: AChatItem) + case ntfTokenStatus(status: NtfTknStatus) case cmdOk case chatCmdError(chatError: ChatError) case chatError(chatError: ChatError) @@ -218,6 +219,7 @@ enum ChatResponse: Decodable, Error { case .chatItemDeleted: return "chatItemDeleted" case .rcvFileAccepted: return "rcvFileAccepted" case .rcvFileComplete: return "rcvFileComplete" + case .ntfTokenStatus: return "ntfTokenStatus" case .cmdOk: return "cmdOk" case .chatCmdError: return "chatCmdError" case .chatError: return "chatError" @@ -266,6 +268,7 @@ enum ChatResponse: Decodable, Error { case let .chatItemDeleted(deletedChatItem, toChatItem): return "deletedChatItem:\n\(String(describing: deletedChatItem))\ntoChatItem:\n\(String(describing: toChatItem))" case .rcvFileAccepted: return noDetails case let .rcvFileComplete(chatItem): return String(describing: chatItem) + case let .ntfTokenStatus(status): return String(describing: status) case .cmdOk: return noDetails case let .chatCmdError(chatError): return String(describing: chatError) case let .chatError(chatError): return String(describing: chatError) @@ -459,8 +462,10 @@ func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteM throw r } -func apiRegisterToken(token: String) async throws { - try await sendCommandOkResp(.apiRegisterToken(token: token)) +func apiRegisterToken(token: String) async throws -> NtfTknStatus { + let r = await chatSendCmd(.apiRegisterToken(token: token)) + if case let .ntfTokenStatus(status) = r { return status } + throw r } func apiVerifyToken(token: String, code: String, nonce: String) async throws { @@ -959,7 +964,8 @@ enum StoreError: Decodable { enum AgentErrorType: Decodable { case CMD(cmdErr: CommandErrorType) case CONN(connErr: ConnectionErrorType) - case SMP(smpErr: SMPErrorType) + case SMP(smpErr: ProtocolErrorType) + case NTF(ntfErr: ProtocolErrorType) case BROKER(brokerErr: BrokerErrorType) case AGENT(agentErr: SMPAgentError) case INTERNAL(internalErr: String) @@ -982,17 +988,17 @@ enum ConnectionErrorType: Decodable { } enum BrokerErrorType: Decodable { - case RESPONSE(smpErr: SMPErrorType) + case RESPONSE(smpErr: ProtocolErrorType) case UNEXPECTED case NETWORK - case TRANSPORT(transportErr: SMPTransportError) + case TRANSPORT(transportErr: ProtocolTransportError) case TIMEOUT } -enum SMPErrorType: Decodable { +enum ProtocolErrorType: Decodable { case BLOCK case SESSION - case CMD(cmdErr: SMPCommandError) + case CMD(cmdErr: ProtocolCommandError) case AUTH case QUOTA case NO_MSG @@ -1000,15 +1006,15 @@ enum SMPErrorType: Decodable { case INTERNAL } -enum SMPCommandError: Decodable { +enum ProtocolCommandError: Decodable { case UNKNOWN case SYNTAX case NO_AUTH case HAS_AUTH - case NO_QUEUE + case NO_ENTITY } -enum SMPTransportError: Decodable { +enum ProtocolTransportError: Decodable { case badBlock case largeMsg case badSession diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index aa00ca8375..ff969c9ca2 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -133,9 +133,8 @@ struct SettingsView: View { } if let token = chatModel.deviceToken { HStack { - Image(systemName: "bolt.fill") - .padding(.trailing, 4) - NotificationsToggle(token) + notificationsIcon() + notificationsToggle(token) } } Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))") @@ -150,7 +149,35 @@ struct SettingsView: View { case error(LocalizedStringKey, String) } - private func NotificationsToggle(_ token: String) -> some View { + private func notificationsIcon() -> some View { + let icon: String + let color: Color + switch (chatModel.tokenStatus) { + case .new: + icon = "bolt" + color = .primary + case .registered: + icon = "bolt.fill" + color = .primary + case .invalid: + icon = "bolt.slash" + color = .primary + case .confirmed: + icon = "bolt.fill" + color = .yellow + case .active: + icon = "bolt.fill" + color = .green + case .expired: + icon = "bolt.slash.fill" + color = .primary + } + return Image(systemName: icon) + .padding(.trailing, 9) + .foregroundColor(color) + } + + private func notificationsToggle(_ token: String) -> some View { Toggle("Check messages", isOn: $useNotifications) .onChange(of: useNotifications) { enable in if enable { @@ -160,6 +187,7 @@ struct SettingsView: View { Task { do { try await apiDeleteToken(token: token) + chatModel.tokenStatus = .new } catch { DispatchQueue.main.async { @@ -191,7 +219,7 @@ struct SettingsView: View { primaryButton: .destructive(Text("Confirm")) { Task { do { - try await apiRegisterToken(token: token) + chatModel.tokenStatus = try await apiRegisterToken(token: token) } catch { DispatchQueue.main.async { useNotifications = false