From e3502c57388a48339676733018200e268f591abe Mon Sep 17 00:00:00 2001 From: Evgeny Date: Fri, 3 Jul 2026 13:47:45 +0100 Subject: [PATCH] ios: reliably show API alerts (use UIKit) (#7195) * ios: reliably show API alerts (use UIKit) * fix infinite recursion --- apps/ios/Shared/Model/SimpleXAPI.swift | 286 +++++++++--------- apps/ios/Shared/Views/Chat/ChatInfoView.swift | 27 +- .../Chat/ChatItem/CIRcvDecryptionError.swift | 6 +- .../Chat/Group/AddGroupMembersView.swift | 5 +- .../Views/Chat/Group/GroupLinkView.swift | 11 +- .../Chat/Group/GroupMemberInfoView.swift | 22 +- .../Views/ChatList/ChatListNavLink.swift | 26 +- .../ChatList/ContactConnectionInfo.swift | 6 +- .../ios/Shared/Views/Helpers/ShareSheet.swift | 4 + .../Shared/Views/NewChat/NewChatView.swift | 42 +-- .../Onboarding/CreateSimpleXAddress.swift | 16 +- .../Shared/Views/Onboarding/YourNetwork.swift | 8 +- .../RemoteAccess/ConnectDesktopView.swift | 3 +- .../Views/UserSettings/UserAddressView.swift | 10 +- .../Views/UserSettings/UserProfilesView.swift | 10 +- apps/ios/SimpleXChat/ErrorAlert.swift | 106 +++++-- 16 files changed, 287 insertions(+), 301 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 3b610663dc..d7ee777d5b 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -569,7 +569,7 @@ private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async return cItems } if let networkErrorAlert = networkErrorAlert(r) { - AlertManager.shared.showAlert(networkErrorAlert) + await MainActor.run { showAlert(networkErrorAlert) } } else { sendMessageErrorAlert(r.unexpected) } @@ -1003,15 +1003,15 @@ func apiVerifyGroupMember(_ groupId: Int64, _ groupMemberId: Int64, connectionCo return nil } -func apiAddContact(incognito: Bool) async -> ((CreatedConnLink, PendingContactConnection)?, Alert?) { +func apiAddContact(incognito: Bool) async -> (CreatedConnLink, PendingContactConnection)? { guard let userId = ChatModel.shared.currentUser?.userId else { logger.error("apiAddContact: no current user") - return (nil, nil) + return nil } let r: APIResult? = await chatApiSendCmdWithRetry(.apiAddContact(userId: userId, incognito: incognito), bgTask: false) - if case let .result(.invitation(_, connLinkInv, connection)) = r { return ((connLinkInv, connection), nil) } - let alert: Alert? = if let r { connectionErrorAlert(r) } else { nil } - return (nil, alert) + if case let .result(.invitation(_, connLinkInv, connection)) = r { return (connLinkInv, connection) } + if let r { await MainActor.run { showAlert(connectionErrorAlert(r)) } } + return nil } func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> PendingContactConnection? { @@ -1026,130 +1026,124 @@ func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> Pendi if let r { throw r.unexpected } else { return nil } } -func apiConnectPlan(connLink: String, linkOwnerSig: LinkOwnerSig? = nil, inProgress: BoxedValue) async -> ((CreatedConnLink, ConnectionPlan)?, Alert?) { +func apiConnectPlan(connLink: String, linkOwnerSig: LinkOwnerSig? = nil, inProgress: BoxedValue) async -> (CreatedConnLink, ConnectionPlan)? { guard let userId = ChatModel.shared.currentUser?.userId else { logger.error("apiConnectPlan: no current user") - return (nil, nil) + return nil } let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPlan(userId: userId, connLink: connLink, linkOwnerSig: linkOwnerSig), inProgress: inProgress) - if case let .result(.connectionPlan(_, connLink, connPlan)) = r { return ((connLink, connPlan), nil) } - let alert: Alert? = if let r { apiConnectResponseAlert(r) } else { nil } - return (nil, alert) + if case let .result(.connectionPlan(_, connLink, connPlan)) = r { return (connLink, connPlan) } + if let r { await apiConnectResponseAlert(r) } + return nil } func apiConnect(incognito: Bool, connLink: CreatedConnLink) async -> (ConnReqType, PendingContactConnection)? { - let (r, alert) = await apiConnect_(incognito: incognito, connLink: connLink) - if let alert = alert { - AlertManager.shared.showAlert(alert) - return nil - } else { - return r - } -} - -func apiConnect_(incognito: Bool, connLink: CreatedConnLink) async -> ((ConnReqType, PendingContactConnection)?, Alert?) { guard let userId = ChatModel.shared.currentUser?.userId else { logger.error("apiConnect: no current user") - return (nil, nil) + return nil } let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnect(userId: userId, incognito: incognito, connLink: connLink)) let m = ChatModel.shared switch r { case let .result(.sentConfirmation(_, connection)): - return ((.invitation, connection), nil) + return (.invitation, connection) case let .result(.sentInvitation(_, connection)): - return ((.contact, connection), nil) + return (.contact, connection) case let .result(.contactAlreadyExists(_, contact)): if let c = m.getContactChat(contact.contactId) { ItemsModel.shared.loadOpenChat(c.id) } - let alert = contactAlreadyExistsAlert(contact) - return (nil, alert) + await contactAlreadyExistsAlert(contact) + return nil default: () } - let alert: Alert? = if let r { apiConnectResponseAlert(r) } else { nil } - return (nil, alert) + if let r { await apiConnectResponseAlert(r) } + return nil } -private func apiConnectResponseAlert(_ r: APIResult) -> Alert { - switch r.unexpected { - case .error(.invalidConnReq): - mkAlert( - title: "Invalid connection link", - message: "Please check that you used the correct link or ask your contact to send you another one." - ) - case .error(.unsupportedConnReq): - mkAlert( - title: "Unsupported connection link", - message: "This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." - ) - case let .error(.simplexDomainNotReady(domain, err)): - switch err { - case .noValidLink: - mkAlert( - title: "No valid link", - message: "The SimpleX name \(domain.fullDomainName) is registered, but it has no valid link." +private func apiConnectResponseAlert(_ r: APIResult) async { + await MainActor.run { + switch r.unexpected { + case .error(.invalidConnReq): + showAlert( + NSLocalizedString("Invalid connection link", comment: ""), + message: NSLocalizedString("Please check that you used the correct link or ask your contact to send you another one.", comment: "") ) - case .unknownDomain: - mkAlert( - title: "Unconfirmed name", - message: "The SimpleX name \(domain.fullDomainName) is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner." + case .error(.unsupportedConnReq): + showAlert( + NSLocalizedString("Unsupported connection link", comment: ""), + message: NSLocalizedString("This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.", comment: "") ) - } - case .errorAgent(.NO_NAME_SERVERS): - mkAlert( - title: "SimpleX name error", - message: "None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link." - ) - case .errorAgent(.SMP(_, .AUTH)): - mkAlert( - title: "Connection error (AUTH)", - message: "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." - ) - case let .errorAgent(.SMP(_, .BLOCKED(info))): - Alert( - title: Text("Connection blocked"), - message: Text("Connection is blocked by server operator:\n\(info.reason.text)"), - primaryButton: .default(Text("Ok")), - secondaryButton: .default(Text("How it works")) { - DispatchQueue.main.async { - UIApplication.shared.open(contentModerationPostLink) - } + case let .error(.simplexDomainNotReady(domain, err)): + switch err { + case .noValidLink: + showAlert( + NSLocalizedString("No valid link", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("The SimpleX name %@ is registered, but it has no valid link.", comment: ""), domain.fullDomainName) + ) + case .unknownDomain: + showAlert( + NSLocalizedString("Unconfirmed name", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner.", comment: ""), domain.fullDomainName) + ) } - ) - case .errorAgent(.SMP(_, .QUOTA)): - mkAlert( - title: "Undelivered messages", - message: "The connection reached the limit of undelivered messages, your contact may be offline." - ) - case let .errorAgent(.INTERNAL(internalErr)): - if internalErr == "SEUniqueID" { - mkAlert( - title: "Already connected?", - message: "It seems like you are already connected via this link. If it is not the case, there was an error (\(internalErr))." + case .errorAgent(.NO_NAME_SERVERS): + showAlert( + NSLocalizedString("SimpleX name error", comment: ""), + message: NSLocalizedString("None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link.", comment: "") ) - } else { - connectionErrorAlert(r) + case .errorAgent(.SMP(_, .AUTH)): + showAlert( + NSLocalizedString("Connection error (AUTH)", comment: ""), + message: NSLocalizedString("Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection.", comment: "") + ) + case let .errorAgent(.SMP(_, .BLOCKED(info))): + showAlert( + NSLocalizedString("Connection blocked", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Connection is blocked by server operator:\n%@", comment: ""), info.reason.text), + actions: {[ + okAlertAction, + UIAlertAction(title: NSLocalizedString("How it works", comment: ""), style: .default) { _ in + DispatchQueue.main.async { + UIApplication.shared.open(contentModerationPostLink) + } + } + ]} + ) + case .errorAgent(.SMP(_, .QUOTA)): + showAlert( + NSLocalizedString("Undelivered messages", comment: ""), + message: NSLocalizedString("The connection reached the limit of undelivered messages, your contact may be offline.", comment: "") + ) + case let .errorAgent(.INTERNAL(internalErr)): + if internalErr == "SEUniqueID" { + showAlert( + NSLocalizedString("Already connected?", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("It seems like you are already connected via this link. If it is not the case, there was an error (%@).", comment: ""), internalErr) + ) + } else { + showAlert(connectionErrorAlert(r)) + } + case let .errorAgent(.SMP(serverAddress, .NAME(nameErr))): + switch nameErr { + case .NOT_FOUND: + showAlert( + NSLocalizedString("Name not found", comment: ""), + message: NSLocalizedString("This SimpleX name is not registered. Please check the name.", comment: "") + ) + case .NO_RESOLVER: + showAlert( + NSLocalizedString("SimpleX name error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Server %@ does not support name resolution. Configure servers, or use a connection link.", comment: ""), serverAddress) + ) + case let .RESOLVER(resolverErr): + showAlert( + NSLocalizedString("SimpleX name error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Resolver error: %@", comment: ""), resolverErr) + ) + } + default: showAlert(connectionErrorAlert(r)) } - case let .errorAgent(.SMP(serverAddress, .NAME(nameErr))): - switch nameErr { - case .NOT_FOUND: - mkAlert( - title: "Name not found", - message: "This SimpleX name is not registered. Please check the name." - ) - case .NO_RESOLVER: - mkAlert( - title: "SimpleX name error", - message: "Server \(serverAddress) does not support name resolution. Configure servers, or use a connection link." - ) - case let .RESOLVER(resolverErr): - mkAlert( - title: "SimpleX name error", - message: "Resolver error: \(resolverErr)" - ) - } - default: connectionErrorAlert(r) } } @@ -1162,36 +1156,34 @@ func connErrorText(_ e: ChatError) -> String { case .errorAgent(.SMP(_, .AUTH)): NSLocalizedString("Connection error (AUTH)", comment: "conn error description") case let .errorAgent(.SMP(_, .BLOCKED(info))): - NSLocalizedString("Connection blocked: \(info.reason.text)", comment: "conn error description") + String.localizedStringWithFormat(NSLocalizedString("Connection blocked: %@", comment: "conn error description"), info.reason.text) case .errorAgent(.SMP(_, .QUOTA)): NSLocalizedString("The connection reached the limit of undelivered messages", comment: "conn error description") default: if getNetworkErrorAlert(e) != nil { NSLocalizedString("Network error", comment: "conn error description") } else { - "\(NSLocalizedString("Error", comment: "conn error description")): \(responseError(e))" + String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "conn error description"), responseError(e)) } } } -func contactAlreadyExistsAlert(_ contact: Contact) -> Alert { - mkAlert( - title: "Contact already exists", - message: "You are already connected to \(contact.displayName)." - ) -} - -private func connectionErrorAlert(_ r: APIResult) -> Alert { - if let networkErrorAlert = networkErrorAlert(r) { - return networkErrorAlert - } else { - return mkAlert( - title: "Connection error", - message: "Error: \(responseError(r.unexpected))" +func contactAlreadyExistsAlert(_ contact: Contact) async { + await MainActor.run { + showAlert( + NSLocalizedString("Contact already exists", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("You are already connected to %@.", comment: ""), contact.displayName) ) } } +private func connectionErrorAlert(_ r: APIResult) -> (title: String, message: String?) { + networkErrorAlert(r) ?? ( + title: NSLocalizedString("Connection error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: ""), responseError(r.unexpected)) + ) +} + func apiPrepareContact(connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, verifiedDomain: SimplexDomain? = nil) async throws -> ChatData { let userId = try currentUserId("apiPrepareContact") let r: ChatResponse1 = try await chatSendCmd(.apiPrepareContact(userId: userId, connLink: connLink, contactShortLinkData: contactShortLinkData, verifiedDomain: verifiedDomain)) @@ -1221,30 +1213,29 @@ func apiChangePreparedGroupUser(groupId: Int64, newUserId: Int64) async throws - func apiConnectPreparedContact(contactId: Int64, incognito: Bool, msg: MsgContent?) async -> Contact? { let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPreparedContact(contactId: contactId, incognito: incognito, msg: msg)) if case let .result(.startedConnectionToContact(_, contact)) = r { return contact } - if let r { AlertManager.shared.showAlert(apiConnectResponseAlert(r)) } + if let r { await apiConnectResponseAlert(r) } return nil } func apiConnectPreparedGroup(groupId: Int64, incognito: Bool, msg: MsgContent?) async -> (GroupInfo, [RelayConnectionResult])? { let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPreparedGroup(groupId: groupId, incognito: incognito, msg: msg)) if case let .result(.startedConnectionToGroup(_, groupInfo, relayResults)) = r { return (groupInfo, relayResults) } - if let r { AlertManager.shared.showAlert(apiConnectResponseAlert(r)) } + if let r { await apiConnectResponseAlert(r) } return nil } -func apiConnectContactViaAddress(incognito: Bool, contactId: Int64) async -> (Contact?, Alert?) { +func apiConnectContactViaAddress(incognito: Bool, contactId: Int64) async -> Contact? { guard let userId = ChatModel.shared.currentUser?.userId else { logger.error("apiConnectContactViaAddress: no current user") - return (nil, nil) + return nil } let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectContactViaAddress(userId: userId, incognito: incognito, contactId: contactId)) - if case let .result(.sentInvitationToContact(_, contact, _)) = r { return (contact, nil) } + if case let .result(.sentInvitationToContact(_, contact, _)) = r { return contact } if let r { logger.error("apiConnectContactViaAddress error: \(responseError(r.unexpected))") - return (nil, connectionErrorAlert(r)) - } else { - return (nil, nil) + await MainActor.run { showAlert(connectionErrorAlert(r)) } } + return nil } func apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async throws { @@ -1365,16 +1356,16 @@ func apiSetProfileAddress(on: Bool) async throws -> User? { } } -// name is the encoded SimplexName (e.g. "@alice.simplex"); nil clears it -// owner-specific SNENoValidLink wording; everything else reuses the general apiConnectResponseAlert -func showSetSimplexNameError(_ r: APIResult, isChannel: Bool) { +func showSetSimplexNameError(_ r: APIResult, isChannel: Bool) async { if case let .error(.simplexDomainNotReady(domain, .noValidLink)) = r.unexpected { let format = isChannel ? NSLocalizedString("The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page.", comment: "alert message") : NSLocalizedString("The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page.", comment: "alert message") - showAlert(NSLocalizedString("Error saving name", comment: "alert title"), message: String.localizedStringWithFormat(format, domain.fullDomainName)) + await MainActor.run { + showAlert(NSLocalizedString("Error saving name", comment: "alert title"), message: String.localizedStringWithFormat(format, domain.fullDomainName)) + } } else { - AlertManager.shared.showAlert(apiConnectResponseAlert(r)) + await apiConnectResponseAlert(r) } } @@ -1385,7 +1376,7 @@ func apiSetUserDomain(_ simplexDomain: String?) async throws -> User { case let .result(.userProfileUpdated(user, _, _, _)): return user case let .result(.userProfileNoChange(user)): return user default: - showSetSimplexNameError(r, isChannel: false) + await showSetSimplexNameError(r, isChannel: false) throw r.unexpected } } @@ -1502,23 +1493,22 @@ func apiSetUserAddressSettings(_ settings: AddressSettings) async throws -> User func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Contact? { let r: APIResult? = await chatApiSendCmdWithRetry(.apiAcceptContact(incognito: incognito, contactReqId: contactReqId)) - let am = AlertManager.shared if case let .result(.acceptingContactRequest(_, contact)) = r { return contact } if case .error(.errorAgent(.SMP(_, .AUTH))) = r { - am.showAlertMsg( - title: "Connection error (AUTH)", - message: "Sender may have deleted the connection request." - ) + await MainActor.run { showAlert( + NSLocalizedString("Connection error (AUTH)", comment: ""), + message: NSLocalizedString("Sender may have deleted the connection request.", comment: "") + ) } } else if let r { if let networkErrorAlert = networkErrorAlert(r) { - am.showAlert(networkErrorAlert) + await MainActor.run { showAlert(networkErrorAlert) } } else { logger.error("apiAcceptContactRequest error: \(String(describing: r))") - am.showAlertMsg( - title: "Error accepting contact request", - message: "Error: \(responseError(r.unexpected))" - ) + await MainActor.run { showAlert( + NSLocalizedString("Error accepting contact request", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: ""), responseError(r.unexpected)) + ) } } } return nil @@ -1762,11 +1752,11 @@ func deleteRemoteCtrl(_ rcId: Int64) async throws { try await sendCommandOkResp(.deleteRemoteCtrl(remoteCtrlId: rcId)) } -func networkErrorAlert(_ res: APIResult) -> Alert? { - if case let .error(e) = res, let alert = getNetworkErrorAlert(e) { - return mkAlert(title: alert.title, message: alert.message) +func networkErrorAlert(_ res: APIResult) -> (title: String, message: String?)? { + if case let .error(e) = res { + getNetworkErrorAlert(e) } else { - return nil + nil } } @@ -2071,7 +2061,7 @@ func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws func apiSetPublicGroupAccess(_ groupId: Int64, access: PublicGroupAccess) async throws -> GroupInfo { let r: APIResult = await chatApiSendCmd(.apiSetPublicGroupAccess(groupId: groupId, access: access)) if case let .result(.groupUpdated(_, toGroup)) = r { return toGroup } - showSetSimplexNameError(r, isChannel: true) + await showSetSimplexNameError(r, isChannel: true) throw r.unexpected } @@ -2129,7 +2119,7 @@ func apiSendMemberContactInvitation(_ contactId: Int64, _ msg: MsgContent) async func apiAcceptMemberContact(contactId: Int64) async -> Contact? { let r: APIResult? = await chatApiSendCmdWithRetry(.apiAcceptMemberContact(contactId: contactId)) if case let .result(.memberContactAccepted(_, contact)) = r { return contact } - if let r { AlertManager.shared.showAlert(apiConnectResponseAlert(r)) } + if let r { await apiConnectResponseAlert(r) } return nil } diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 9988b30d4e..2f76241c00 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -283,8 +283,9 @@ struct ChatInfoView: View { } } catch let e { logger.error("apiContactQueueInfo error: \(responseError(e))") - let a = getErrorAlert(e, "Error") - await MainActor.run { alert = .error(title: a.title, error: a.message) } + await MainActor.run { + showErrorAlert(e, NSLocalizedString("Error", comment: "")) + } } } } @@ -334,7 +335,7 @@ struct ChatInfoView: View { case .syncConnectionForceAlert: return syncConnectionForceAlert({ Task { - if let stats = await syncContactConnection(contact, force: true, showAlert: { alert = .someAlert(alert: $0) }) { + if let stats = await syncContactConnection(contact, force: true) { connectionStats = stats dismiss() } @@ -541,7 +542,7 @@ struct ChatInfoView: View { private func synchronizeConnectionButton() -> some View { Button { Task { - if let stats = await syncContactConnection(contact, force: false, showAlert: { alert = .someAlert(alert: $0) }) { + if let stats = await syncContactConnection(contact, force: false) { connectionStats = stats dismiss() } @@ -618,9 +619,8 @@ struct ChatInfoView: View { } } catch let error { logger.error("switchContactAddress apiSwitchContact error: \(responseError(error))") - let a = getErrorAlert(error, "Error changing address") await MainActor.run { - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error changing address", comment: "")) } } } @@ -636,9 +636,8 @@ struct ChatInfoView: View { } } catch let error { logger.error("abortSwitchContactAddress apiAbortSwitchContact error: \(responseError(error))") - let a = getErrorAlert(error, "Error aborting address change") await MainActor.run { - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error aborting address change", comment: "")) } } } @@ -748,7 +747,7 @@ struct ChatTTLOption: View { } } -func syncContactConnection(_ contact: Contact, force: Bool, showAlert: (SomeAlert) -> Void) async -> ConnectionStats? { +func syncContactConnection(_ contact: Contact, force: Bool) async -> ConnectionStats? { do { let stats = try apiSyncContactRatchet(contact.apiId, force) await MainActor.run { @@ -757,14 +756,8 @@ func syncContactConnection(_ contact: Contact, force: Bool, showAlert: (SomeAler return stats } catch let error { logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))") - let a = getErrorAlert(error, "Error synchronizing connection") await MainActor.run { - showAlert( - SomeAlert( - alert: mkAlert(title: a.title, message: a.message), - id: "syncContactConnection error" - ) - ) + showErrorAlert(error, NSLocalizedString("Error synchronizing connection", comment: "")) } return nil } @@ -844,7 +837,7 @@ private struct CallButton: View { message: Text("Connection requires encryption renegotiation."), primaryButton: .default(Text("Fix")) { Task { - if let stats = await syncContactConnection(contact, force: false, showAlert: showAlert) { + if let stats = await syncContactConnection(contact, force: false) { connectionStats = stats } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift index ec23dc15a4..bdd38cb4df 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift @@ -185,9 +185,8 @@ struct CIRcvDecryptionError: View { } } catch let error { logger.error("syncMemberConnection apiSyncGroupMemberRatchet error: \(responseError(error))") - let a = getErrorAlert(error, "Error synchronizing connection") await MainActor.run { - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error synchronizing connection", comment: "")) } } } @@ -202,9 +201,8 @@ struct CIRcvDecryptionError: View { } } catch let error { logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))") - let a = getErrorAlert(error, "Error synchronizing connection") await MainActor.run { - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error synchronizing connection", comment: "")) } } } diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift index 336b4adfd1..108f4d4306 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -174,8 +174,9 @@ struct AddGroupMembersViewCommon: View { } addedMembersCb(selectedContacts) } catch { - let a = getErrorAlert(error, "Error adding member(s)") - alert = .error(title: a.title, error: a.message) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error adding member(s)", comment: "")) + } } } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift index 43d23878f5..8a9bcaf059 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift @@ -155,8 +155,9 @@ struct GroupLinkView: View { do { groupLink = try await apiGroupLinkMemberRole(groupId, memberRole: groupLinkMemberRole) } catch let error { - let a = getErrorAlert(error, "Error updating group link") - alert = .error(title: a.title, error: a.message) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error updating group link", comment: "")) + } } } } @@ -188,8 +189,7 @@ struct GroupLinkView: View { logger.error("GroupLinkView apiCreateGroupLink: \(responseError(error))") await MainActor.run { creatingLink = false - let a = getErrorAlert(error, "Error creating group link") - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error creating group link", comment: "")) } } } @@ -230,8 +230,7 @@ struct GroupLinkView: View { logger.error("apiAddGroupShortLink: \(responseError(error))") await MainActor.run { creatingLink = false - let a = getErrorAlert(error, "Error adding short link") - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error adding short link", comment: "")) } } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index c87f97089c..5e2713f922 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -278,8 +278,9 @@ struct GroupMemberInfoView: View { } } catch let e { logger.error("apiContactQueueInfo error: \(responseError(e))") - let a = getErrorAlert(e, "Error") - await MainActor.run { alert = .error(title: a.title, error: a.message) } + await MainActor.run { + showErrorAlert(e, NSLocalizedString("Error", comment: "")) + } } } } @@ -473,10 +474,9 @@ struct GroupMemberInfoView: View { } } catch let error { logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))") - let a = getErrorAlert(error, "Error creating member contact") await MainActor.run { progressIndicator = false - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error creating member contact", comment: "")) } } } @@ -752,8 +752,9 @@ struct GroupMemberInfoView: View { } catch let error { newRole = mem.memberRole logger.error("apiMembersRole error: \(responseError(error))") - let a = getErrorAlert(error, "Error changing role") - alert = .error(title: a.title, error: a.message) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error changing role", comment: "")) + } } } }, @@ -774,9 +775,8 @@ struct GroupMemberInfoView: View { } } catch let error { logger.error("switchMemberAddress apiSwitchGroupMember error: \(responseError(error))") - let a = getErrorAlert(error, "Error changing address") await MainActor.run { - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error changing address", comment: "")) } } } @@ -792,9 +792,8 @@ struct GroupMemberInfoView: View { } } catch let error { logger.error("abortSwitchMemberAddress apiAbortSwitchGroupMember error: \(responseError(error))") - let a = getErrorAlert(error, "Error aborting address change") await MainActor.run { - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error aborting address change", comment: "")) } } } @@ -811,9 +810,8 @@ struct GroupMemberInfoView: View { } } catch let error { logger.error("syncMemberConnection apiSyncGroupMemberRatchet error: \(responseError(error))") - let a = getErrorAlert(error, "Error synchronizing connection") await MainActor.run { - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error synchronizing connection", comment: "")) } } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 76734dcb42..0ed78401b0 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -532,9 +532,7 @@ struct ChatListNavLink: View { .frameCompat(height: dynamicRowHeight) .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button { - AlertManager.shared.showAlert(deleteContactConnectionAlert(contactConnection) { a in - AlertManager.shared.showAlertMsg(title: a.title, message: a.message) - }) + AlertManager.shared.showAlert(deleteContactConnectionAlert(contactConnection)) } label: { deleteLabel } @@ -698,7 +696,7 @@ func rejectContactRequestAlert(_ contactRequestId: Int64) -> Alert { ) } -func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, showError: @escaping (ErrorAlert) -> Void, success: @escaping () -> Void = {}) -> Alert { +func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, success: @escaping () -> Void = {}) -> Alert { Alert( title: Text("Delete pending connection?"), message: Text(contactConnection.displayName + "\n\n") @@ -715,7 +713,7 @@ func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, } } catch let error { await MainActor.run { - showError(getErrorAlert(error, "Error deleting connection")) + showErrorAlert(error, NSLocalizedString("Error deleting connection", comment: "")) } } } @@ -725,11 +723,7 @@ func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, } func connectContactViaAddress(_ contactId: Int64, _ incognito: Bool, showAlert: (Alert) -> Void) async -> Bool { - let (contact, alert) = await apiConnectContactViaAddress(incognito: incognito, contactId: contactId) - if let alert = alert { - showAlert(alert) - return false - } else if let contact = contact { + if let contact = await apiConnectContactViaAddress(incognito: incognito, contactId: contactId) { await MainActor.run { ChatModel.shared.updateContact(contact) } @@ -757,8 +751,9 @@ func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) { await onComplete() } catch let error { await onComplete() - let a = getErrorAlert(error, "Error joining group") - AlertManager.shared.showAlertMsg(title: a.title, message: a.message) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error joining group", comment: "")) + } } func deleteGroup() async { @@ -773,12 +768,13 @@ func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) { } } -func getErrorAlert(_ error: Error, _ title: LocalizedStringKey) -> ErrorAlert { +func showErrorAlert(_ error: Error, _ title: String) { + let err = { String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: ""), responseError(error)) } if let r = error as? ChatError, let alert = getNetworkErrorAlert(r) { - return alert + showAlert(alert.title, message: alert.message ?? err()) } else { - return ErrorAlert(title: title, message: "Error: \(responseError(error))") + showAlert(title, message: err()) } } diff --git a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift index 124c5ee7ba..c777eea80c 100644 --- a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift +++ b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift @@ -103,11 +103,7 @@ struct ContactConnectionInfo: View { .alert(item: $alert) { _alert in switch _alert { case .deleteInvitationAlert: - return deleteContactConnectionAlert(contactConnection) { a in - alert = .error(title: a.title, error: a.message) - } success: { - dismiss() - } + return deleteContactConnectionAlert(contactConnection, success: { dismiss() }) case let .error(title, error): return mkAlert(title: title, message: error) } } diff --git a/apps/ios/Shared/Views/Helpers/ShareSheet.swift b/apps/ios/Shared/Views/Helpers/ShareSheet.swift index 82d17cd2b1..039c072c80 100644 --- a/apps/ios/Shared/Views/Helpers/ShareSheet.swift +++ b/apps/ios/Shared/Views/Helpers/ShareSheet.swift @@ -54,6 +54,10 @@ func showAlert( } } +func showAlert(_ a: (title: String, message: String?)) { + showAlert(a.title, message: a.message) +} + func showAlert( _ title: String, message: String? = nil, diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index c2154b2f27..271c3845c3 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -204,8 +204,7 @@ struct NewChatView: View { creatingConnReq = true Task { _ = try? await Task.sleep(nanoseconds: 250_000000) - let (r, apiAlert) = await apiAddContact(incognito: incognitoGroupDefault.get()) - if let (connLink, pcc) = r { + if let (connLink, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) { await MainActor.run { m.updateContactConnection(pcc) m.showingInvitation = ShowingInvitation(pcc: pcc, connChatUsed: false) @@ -215,9 +214,6 @@ struct NewChatView: View { } else { await MainActor.run { creatingConnReq = false - if let apiAlert = apiAlert { - alert = .newChatSomeAlert(alert: SomeAlert(alert: apiAlert, id: "createInvitation error")) - } } } } @@ -434,15 +430,9 @@ private struct ActiveProfilePicker: View { profileSwitchStatus = .idle incognitoEnabled = !incognito logger.error("apiSetConnectionIncognito error: \(responseError(error))") - let err = getErrorAlert(error, "Error changing to incognito!") - - alert = SomeAlert( - alert: Alert( - title: Text(err.title), - message: Text(err.message ?? "Error: \(responseError(error))") - ), - id: "setConnectionIncognitoError" - ) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error changing to incognito!", comment: "")) + } } } } @@ -494,14 +484,7 @@ private struct ActiveProfilePicker: View { if let currentUser = chatModel.currentUser { selectedProfile = currentUser } - let err = getErrorAlert(error, "Error changing connection profile") - alert = SomeAlert( - alert: Alert( - title: Text(err.title), - message: Text(err.message ?? "Error: \(responseError(error))") - ), - id: "changeConnectionUserError" - ) + showErrorAlert(error, NSLocalizedString("Error changing connection profile", comment: "")) } } } @@ -1331,7 +1314,7 @@ func planAndConnect( func connectTask(_ inProgress: BoxedValue) { Task { - let (result, alert) = await apiConnectPlan(connLink: shortOrFullLink, linkOwnerSig: linkOwnerSig, inProgress: inProgress) + let result = await apiConnectPlan(connLink: shortOrFullLink, linkOwnerSig: linkOwnerSig, inProgress: inProgress) await MainActor.run { ConnectProgressManager.shared.stopConnectProgress() } @@ -1623,17 +1606,8 @@ func planAndConnect( cleanup: cleanup ) } - } else { - await MainActor.run { - if let alert { - dismissAllSheets(animated: true) { - AlertManager.shared.showAlert(alert) - cleanup?() - } - } else { - cleanup?() - } - } + } else if let cleanup { + await MainActor.run { cleanup() } } } } diff --git a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift index ab84bed7df..b348057b8a 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift @@ -86,12 +86,10 @@ struct CreateSimpleXAddress: View { await MainActor.run { progressIndicator = false } } catch let error { logger.error("CreateSimpleXAddress create address: \(responseError(error))") - await MainActor.run { progressIndicator = false } - let a = getErrorAlert(error, "Error creating address") - AlertManager.shared.showAlertMsg( - title: a.title, - message: a.message - ) + await MainActor.run { + progressIndicator = false + showErrorAlert(error, NSLocalizedString("Error creating address", comment: "")) + } } } } label: { @@ -156,11 +154,7 @@ struct CreateSimpleXAddress: View { } case let .failure(error): logger.error("CreateSimpleXAddress share via email: \(responseError(error))") - let a = getErrorAlert(error, "Error sending email") - AlertManager.shared.showAlertMsg( - title: a.title, - message: a.message - ) + showErrorAlert(error, NSLocalizedString("Error sending email", comment: "")) } mailViewResult = nil } diff --git a/apps/ios/Shared/Views/Onboarding/YourNetwork.swift b/apps/ios/Shared/Views/Onboarding/YourNetwork.swift index d3727e196e..015a2be491 100644 --- a/apps/ios/Shared/Views/Onboarding/YourNetwork.swift +++ b/apps/ios/Shared/Views/Onboarding/YourNetwork.swift @@ -180,11 +180,9 @@ struct YourNetworkView: View { m.notificationMode = notificationMode } } catch let error { - let a = getErrorAlert(error, "Error enabling notifications") - AlertManager.shared.showAlertMsg( - title: a.title, - message: a.message - ) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error enabling notifications", comment: "")) + } } } } diff --git a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift index 01b25baed8..24ae5cffca 100644 --- a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift +++ b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift @@ -535,8 +535,7 @@ struct ConnectDesktopView: View { } private func errorAlert(_ error: Error) { - let a = getErrorAlert(error, "Error") - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error", comment: "")) } } diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 5d1bea6079..13caf135e9 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -316,9 +316,10 @@ struct UserAddressView: View { } } catch let error { logger.error("UserAddressView apiCreateUserAddress: \(responseError(error))") - let a = getErrorAlert(error, "Error creating address") - alert = .error(title: a.title, error: a.message) - await MainActor.run { progressIndicator = false } + await MainActor.run { + progressIndicator = false + showErrorAlert(error, NSLocalizedString("Error creating address", comment: "")) + } } } } @@ -390,8 +391,7 @@ struct UserAddressView: View { case .success: () case let .failure(error): logger.error("UserAddressView share via email: \(responseError(error))") - let a = getErrorAlert(error, "Error sending email") - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error sending email", comment: "")) } mailViewResult = nil } diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift index ad3b5cdf95..3f3adbcb2d 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift @@ -319,8 +319,9 @@ struct UserProfilesView: View { } } catch let error { logger.error("Error deleting user profile: \(error)") - let a = getErrorAlert(error, "Error deleting user profile") - alert = .error(title: a.title, error: a.message) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error deleting user profile", comment: "")) + } } func deleteUser() async throws { @@ -436,8 +437,9 @@ struct UserProfilesView: View { } } } catch let error { - let a = getErrorAlert(error, "Error updating user privacy") - alert = .error(title: a.title, error: a.message) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error updating user privacy", comment: "")) + } } } } diff --git a/apps/ios/SimpleXChat/ErrorAlert.swift b/apps/ios/SimpleXChat/ErrorAlert.swift index 2920c2383c..796b995138 100644 --- a/apps/ios/SimpleXChat/ErrorAlert.swift +++ b/apps/ios/SimpleXChat/ErrorAlert.swift @@ -34,20 +34,16 @@ public struct ErrorAlert: Error { } public init(_ error: any Error) { - self = if let e = error as? ChatError { - ErrorAlert(e) + self = if let chatError = error as? ChatError { + if let a = getNetworkErrorAlert(chatError) { + ErrorAlert(title: "\(a.title)", message: a.message.map { "\($0)" }) + } else { + ErrorAlert("\(chatErrorString(chatError))") + } } else { ErrorAlert("\(error.localizedDescription)") } } - - public init(_ chatError: ChatError) { - self = if let networkErrorAlert = getNetworkErrorAlert(chatError) { - networkErrorAlert - } else { - ErrorAlert("\(chatErrorString(chatError))") - } - } } extension LocalizedStringKey: @unchecked Sendable { } @@ -83,18 +79,33 @@ extension View { } } -public func getNetworkErrorAlert(_ e: ChatError) -> ErrorAlert? { +public func getNetworkErrorAlert(_ e: ChatError) -> (title: String, message: String?)? { switch e { case let .errorAgent(.BROKER(addr, .TIMEOUT)): - ErrorAlert(title: "Connection timeout", message: "Please check your network connection with \(serverHostname(addr)) and try again.") + ( + title: NSLocalizedString("Connection timeout", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Please check your network connection with %@ and try again.", comment: ""), serverHostname(addr)) + ) case let .errorAgent(.BROKER(addr, .NETWORK(.unknownCAError))): - ErrorAlert(title: "Connection error", message: "Fingerprint in server address does not match certificate: \(serverHostname(addr)).") + ( + title: NSLocalizedString("Connection error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Fingerprint in server address does not match certificate: %@.", comment: ""), serverHostname(addr)) + ) case let .errorAgent(.BROKER(addr, .NETWORK)): - ErrorAlert(title: "Connection error", message: "Please check your network connection with \(serverHostname(addr)) and try again.") + ( + title: NSLocalizedString("Connection error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Please check your network connection with %@ and try again.", comment: ""), serverHostname(addr)) + ) case let .errorAgent(.BROKER(addr, .HOST)): - ErrorAlert(title: "Connection error", message: "Server address is incompatible with network settings: \(serverHostname(addr)).") + ( + title: NSLocalizedString("Connection error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Server address is incompatible with network settings: %@.", comment: ""), serverHostname(addr)) + ) case let .errorAgent(.BROKER(addr, .TRANSPORT(.version))): - ErrorAlert(title: "Connection error", message: "Server version is incompatible with your app: \(serverHostname(addr)).") + ( + title: NSLocalizedString("Connection error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Server version is incompatible with your app: %@.", comment: ""), serverHostname(addr)) + ) case let .errorAgent(.SMP(serverAddress, .PROXY(proxyErr))): smpProxyErrorAlert(proxyErr, serverAddress) case let .errorAgent(.PROXY(proxyServer, relayServer, .protocolError(.PROXY(proxyErr)))): @@ -103,39 +114,72 @@ public func getNetworkErrorAlert(_ e: ChatError) -> ErrorAlert? { } } -private func smpProxyErrorAlert(_ proxyErr: ProxyError, _ srvAddr: String) -> ErrorAlert? { +private func smpProxyErrorAlert(_ proxyErr: ProxyError, _ srvAddr: String) -> (title: String, message: String?)? { switch proxyErr { case .BROKER(brokerErr: .TIMEOUT): - return ErrorAlert(title: "Private routing error", message: "Error connecting to forwarding server \(serverHostname(srvAddr)). Please try later.") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Error connecting to forwarding server %@. Please try later.", comment: ""), serverHostname(srvAddr)) + ) case .BROKER(brokerErr: .NETWORK(.unknownCAError)): - return ErrorAlert(title: "Private routing error", message: "Fingerprint in forwarding server address does not match certificate: \(serverHostname(srvAddr)).") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Fingerprint in forwarding server address does not match certificate: %@.", comment: ""), serverHostname(srvAddr)) + ) case .BROKER(brokerErr: .NETWORK): - return ErrorAlert(title: "Private routing error", message: "Error connecting to forwarding server \(serverHostname(srvAddr)). Please try later.") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Error connecting to forwarding server %@. Please try later.", comment: ""), serverHostname(srvAddr)) + ) case .BROKER(brokerErr: .HOST): - return ErrorAlert(title: "Private routing error", message: "Forwarding server address is incompatible with network settings: \(serverHostname(srvAddr)).") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Forwarding server address is incompatible with network settings: %@.", comment: ""), serverHostname(srvAddr)) + ) case .BROKER(brokerErr: .TRANSPORT(.version)): - return ErrorAlert(title: "Private routing error", message: "Forwarding server version is incompatible with network settings: \(serverHostname(srvAddr)).") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Forwarding server version is incompatible with network settings: %@.", comment: ""), serverHostname(srvAddr)) + ) default: - return nil + nil } } -private func proxyDestinationErrorAlert(_ proxyErr: ProxyError, _ proxyServer: String, _ relayServer: String) -> ErrorAlert? { +private func proxyDestinationErrorAlert(_ proxyErr: ProxyError, _ proxyServer: String, _ relayServer: String) -> (title: String, message: String?)? { switch proxyErr { case .BROKER(brokerErr: .TIMEOUT): - return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Forwarding server %@ failed to connect to destination server %@. Please try later.", comment: ""), serverHostname(proxyServer), serverHostname(relayServer)) + ) case .BROKER(brokerErr: .NETWORK(.unknownCAError)): - return ErrorAlert(title: "Private routing error", message: "Fingerprint in destination server address does not match certificate: \(serverHostname(relayServer)).") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Fingerprint in destination server address does not match certificate: %@.", comment: ""), serverHostname(relayServer)) + ) case .BROKER(brokerErr: .NETWORK): - return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Forwarding server %@ failed to connect to destination server %@. Please try later.", comment: ""), serverHostname(proxyServer), serverHostname(relayServer)) + ) case .NO_SESSION: - return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Forwarding server %@ failed to connect to destination server %@. Please try later.", comment: ""), serverHostname(proxyServer), serverHostname(relayServer)) + ) case .BROKER(brokerErr: .HOST): - return ErrorAlert(title: "Private routing error", message: "Destination server address of \(serverHostname(relayServer)) is incompatible with forwarding server \(serverHostname(proxyServer)) settings.") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Destination server address of %@ is incompatible with forwarding server %@ settings.", comment: ""), serverHostname(relayServer), serverHostname(proxyServer)) + ) case .BROKER(brokerErr: .TRANSPORT(.version)): - return ErrorAlert(title: "Private routing error", message: "Destination server version of \(serverHostname(relayServer)) is incompatible with forwarding server \(serverHostname(proxyServer)).") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Destination server version of %@ is incompatible with forwarding server %@.", comment: ""), serverHostname(relayServer), serverHostname(proxyServer)) + ) default: - return nil + nil } }