diff --git a/apps/ios/CODE.md b/apps/ios/CODE.md index 5a8356f656..0aaa3a8136 100644 --- a/apps/ios/CODE.md +++ b/apps/ios/CODE.md @@ -178,6 +178,7 @@ After completing all changes (code + documentation), you MUST run an adversarial | Shared/Views/Chat/Group/ChannelRelaysView.swift | spec/client/chat-view.md | product/views/group-info.md | | Shared/Views/NewChat/NewChatView.swift | spec/client/navigation.md | product/views/new-chat.md | | Shared/Views/NewChat/QRCode.swift | spec/client/navigation.md | product/views/new-chat.md | +| Shared/Views/NewChat/WrongQRCode.swift | spec/client/navigation.md | product/views/new-chat.md | | Shared/Views/Call/ActiveCallView.swift | spec/services/calls.md | product/views/call.md | | Shared/Views/Call/CallController.swift | spec/services/calls.md | product/flows/calling.md | | Shared/Views/Call/WebRTCClient.swift | spec/services/calls.md | product/flows/calling.md | diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index 22d0da3829..e9feec224a 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -466,7 +466,7 @@ struct ContentView: View { if path == "/r" { showAlert( NSLocalizedString("Relay address", comment: "alert title"), - message: NSLocalizedString("This is a chat relay address, it cannot be used to connect.", comment: "alert message") + message: NSLocalizedString("This is a chat relay address, it cannot be used to connect. To use it, open Network & servers, Your servers, Add server, then Chat relay, paste the address, then Test relay.", comment: "alert message") ) } else if (path == "/contact" || path == "/invitation" || path == "/a" || path == "/c" || path == "/g" || path == "/i") { path.removeFirst() diff --git a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift index 992c4d821a..d8ab1528c0 100644 --- a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift +++ b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift @@ -211,7 +211,6 @@ struct MigrateToDevice: View { case nil: alert = .error(title: "Invalid link", error: "The text you pasted is not a SimpleX link.") case let type?: - // shared with the paste button, so the title is neutral ("Wrong link") alert = .error(title: "Wrong link", error: wrongQRCodeMessage(type)) } case let .failure(e): @@ -237,13 +236,19 @@ struct MigrateToDevice: View { Button { if let str = UIPasteboard.general.string { let trimmed = str.trimmingCharacters(in: .whitespacesAndNewlines) - switch checkLink(trimmed) { - case .some(.fileDescription): - migrationState = .linkDownloading(link: trimmed) - case nil: - alert = .error(title: "Invalid link", error: "The text you pasted is not a SimpleX link.") - case let type?: - alert = .error(title: "Wrong link", error: wrongQRCodeMessage(type)) + Task { + let linkType = await checkLinkAsync(trimmed) + // the sheet can be dismissed while classifying; applying the result then + // resurrects it (SimpleXInfo) or skips the paste screen (CreateProfile) + guard case .some(.pasteOrScanLink) = migrationState else { return } + switch linkType { + case .some(.fileDescription): + migrationState = .linkDownloading(link: trimmed) + case nil: + alert = .error(title: "Invalid link", error: "The text you pasted is not a SimpleX link.") + case let type?: + alert = .error(title: "Wrong link", error: wrongQRCodeMessage(type)) + } } } } label: { diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index b40bbccdad..ac52c57ef7 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -1327,7 +1327,7 @@ func planAndConnect( if linkType == .relay { showAlert( NSLocalizedString("Relay address", comment: "alert title"), - message: NSLocalizedString("This is a chat relay address, it cannot be used to connect. To use it, open Network & servers, Your servers, Add server, then Chat relay, and paste the address there.", comment: "alert message") + message: NSLocalizedString("This is a chat relay address, it cannot be used to connect. To use it, open Network & servers, Your servers, Add server, then Chat relay, paste the address, then Test relay.", comment: "alert message") ) cleanup?() return diff --git a/apps/ios/Shared/Views/NewChat/WrongQRCode.swift b/apps/ios/Shared/Views/NewChat/WrongQRCode.swift index 55f676ee72..46597ad9ab 100644 --- a/apps/ios/Shared/Views/NewChat/WrongQRCode.swift +++ b/apps/ios/Shared/Views/NewChat/WrongQRCode.swift @@ -2,31 +2,23 @@ // WrongQRCode.swift // SimpleX // -// iOS mirror of the Kotlin WrongQRCode.kt (apps/multiplatform/.../views/newchat/WrongQRCode.kt). -// -// NOTE: written without an Xcode/macOS toolchain and NOT compiled here — build/verify on CI or a Mac. -// import Foundation import SwiftUI import SimpleXChat -// Message shown when a scanner (or the Migrate paste field) is handed a valid -// SimpleX code of a kind it does not accept: name what it actually is and where -// to use it. The type comes from the core classifier (checkLink), so this is -// purely presentation — no parsing here. iOS uses %@ (not %s) for the substitution. func wrongQRCodeMessage(_ type: ScannedLinkType) -> String { switch type { case let .connection(linkType): if linkType == .relay { - return String.localizedStringWithFormat(NSLocalizedString("This is a %@. To use it, open Network & servers, Your servers, Add server, then Chat relay, and paste the address there.", comment: "wrong QR code alert"), linkType.description) + return String.localizedStringWithFormat(NSLocalizedString("This is a %@. To use it, open Network & servers, Your servers, Add server, then Chat relay, paste the address, then Test relay.", comment: "wrong QR code alert"), linkType.description) } else { return String.localizedStringWithFormat(NSLocalizedString("This is a %@. To use it, open New chat, then scan or paste it there.", comment: "wrong QR code alert"), linkType.description) } case .server: return NSLocalizedString("This is a SimpleX server address. To use it, open Network & servers, Your servers, Add server, then Scan server QR code.", comment: "wrong QR code alert") case .fileDescription: - return NSLocalizedString("This is a link to migrate to another device. To use it, when setting up a new device, choose to migrate from another device.", comment: "wrong QR code alert") + return NSLocalizedString("This is a link to migrate to another device. To use it, choose Migrate when setting up a new device.", comment: "wrong QR code alert") case .desktopCtrl: return NSLocalizedString("This is an address to connect to a desktop app. To use it, open Use from desktop and scan the QR code shown in the desktop app.", comment: "wrong QR code alert") case .verificationCode: @@ -34,9 +26,7 @@ func wrongQRCodeMessage(_ type: ScannedLinkType) -> String { } } -// The shared "Wrong QR code" alert. Callers present it through their own alert -// state so it appears within the scanner's presentation (a global/root alert -// may not show over a modal scanner). +// Returned rather than shown, because a global/root alert may not appear over a modal scanner. func wrongQRCodeAlert(_ message: String) -> Alert { Alert(title: Text("Wrong QR code"), message: Text(verbatim: message)) } diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ScanProtocolServer.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ScanProtocolServer.swift index b21f708917..d9699a89e8 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ScanProtocolServer.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ScanProtocolServer.swift @@ -19,19 +19,22 @@ struct ScanProtocolServer: View { @State private var scanAlert: SomeAlert? var body: some View { - VStack(alignment: .leading) { - Text("Scan server QR code") - .font(.largeTitle) - .bold() - .padding(.vertical) + NavigationView { CodeScannerView(codeTypes: [.qr], scanMode: .oncePerCode, completion: processQRCode) .aspectRatio(1, contentMode: .fit) .cornerRadius(12) - .padding(.top) + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .navigationTitle("Scan server QR code") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + } + .alert(item: $scanAlert) { $0.alert } } - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) - .alert(item: $scanAlert) { $0.alert } } func processQRCode(_ resp: Result) { @@ -44,7 +47,6 @@ struct ScanProtocolServer: View { server.server = trimmed addServer(server, $userServers, $serverErrors, $serverWarnings, dismiss) case nil: - // unrecognised: scanner-local alert (deliberate iOS change from the pre-PR dismiss-then-global flow) scanAlert = SomeAlert( alert: mkAlert(title: "Invalid server address!", message: "Check server address and try again."), id: "invalidServerAddress" @@ -66,5 +68,6 @@ struct ScanProtocolServer_Previews: PreviewProvider { serverErrors: Binding.constant([]), serverWarnings: Binding.constant([]) ) + .environmentObject(CurrentColors.toAppTheme()) } } diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift index 85153e1cea..554c485c9f 100644 --- a/apps/ios/SimpleXChat/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -187,10 +187,6 @@ struct ParsedServerAddress: Decodable { var parseError: String } -// The kind of SimpleX QR code / link a scanned string turned out to be, as -// determined by the core (chat_check_link). Public: it crosses the -// SimpleXChat -> app module boundary. Case names and the connection payload -// label must match the Haskell ScannedLinkType wire tags. public enum ScannedLinkType: Decodable { case connection(linkType: SimplexLinkType) case server @@ -199,8 +195,6 @@ public enum ScannedLinkType: Decodable { case verificationCode } -// Wrapper stays internal — it never leaves the framework. A missing linkType -// key (the "not a SimpleX code" case) decodes to nil. struct CheckedLink: Decodable { var linkType: ScannedLinkType? } @@ -219,6 +213,12 @@ public func checkLink(_ s: String) -> ScannedLinkType? { return nil } +// nonisolated async, so callers on the main actor offload to the global executor: +// pasted text is unbounded, and classifying a file link parses its YAML description +public func checkLinkAsync(_ s: String) async -> ScannedLinkType? { + checkLink(s) +} + public func parseSanitizeUri(_ s: String, safe: Bool) -> ParsedUri? { var c = s.cString(using: .utf8)! if let cjson = chat_parse_uri(&c, safe ? 1 : 0) { diff --git a/apps/ios/product/flows/connection.md b/apps/ios/product/flows/connection.md index 7073e07070..c24b8fe800 100644 --- a/apps/ios/product/flows/connection.md +++ b/apps/ios/product/flows/connection.md @@ -104,7 +104,7 @@ Establishing contact between two SimpleX Chat users. SimpleX uses no user identi ### 7a. Relay Link Rejection 1. User scans, pastes, or opens a relay address link (URL path `/r` or `SimplexLinkType.relay`). -2. In `ContentView.connectViaUrl_()`: early return with alert "Relay address" / "This is a chat relay address, it cannot be used to connect." +2. In `ContentView.connectViaUrl_()`: intercepted with alert "Relay address" / "This is a chat relay address, it cannot be used to connect. To use it, open Network & servers, Your servers, Add server, then Chat relay, paste the address, then Test relay." 3. In `NewChatView.planAndConnect()`: `.simplexLink(_, .relay, _, _)` pattern triggers the same alert. 4. The link is NOT processed further. No connection is attempted. diff --git a/apps/ios/product/views/chat-list.md b/apps/ios/product/views/chat-list.md index 04d19bef9e..39fbb39edf 100644 --- a/apps/ios/product/views/chat-list.md +++ b/apps/ios/product/views/chat-list.md @@ -81,7 +81,7 @@ When a group has `groupInfo.useRelays == true` (channel): ### Relay URL Handling -When a relay address link (`/r` path) is opened via URL deep link, `ContentView.connectViaUrl_()` intercepts it and shows an alert: "Relay address" / "This is a chat relay address, it cannot be used to connect." The link is not processed further. +When a relay address link (`/r` path) is opened via URL deep link, `ContentView.connectViaUrl_()` intercepts it and shows an alert: "Relay address" / "This is a chat relay address, it cannot be used to connect. To use it, open Network & servers, Your servers, Add server, then Chat relay, paste the address, then Test relay." The link is not processed further. ### Swipe Actions diff --git a/apps/ios/product/views/new-chat.md b/apps/ios/product/views/new-chat.md index 0d1e384325..b262cd86d2 100644 --- a/apps/ios/product/views/new-chat.md +++ b/apps/ios/product/views/new-chat.md @@ -111,7 +111,7 @@ Calls `apiNewPublicGroup(incognito:relayIds:groupProfile:)` which returns `publi ### Relay Link Blocking -When `planAndConnect` encounters a `.simplexLink(_, .relay, _, _)`, it shows a "Relay address" alert: "This is a chat relay address, it cannot be used to connect." Connection is blocked. +When `planAndConnect` encounters a `.simplexLink(_, .relay, _, _)`, it shows a "Relay address" alert: "This is a chat relay address, it cannot be used to connect. To use it, open Network & servers, Your servers, Add server, then Chat relay, paste the address, then Test relay." Connection is blocked. ### Channel Prepare/Join Alerts diff --git a/apps/ios/spec/architecture.md b/apps/ios/spec/architecture.md index 8e651a0433..8b7ff35f22 100644 --- a/apps/ios/spec/architecture.md +++ b/apps/ios/spec/architecture.md @@ -5,7 +5,7 @@ > Related specs: [README](README.md) | [API Reference](api.md) | [State Management](state.md) | [Database](database.md) > Related product: [Product Overview](../product/README.md) -**Source:** [`SimpleXApp.swift`](../Shared/SimpleXApp.swift#L1-L183) | [`AppDelegate.swift`](../Shared/AppDelegate.swift#L1-L209) | [`ContentView.swift`](../Shared/ContentView.swift#L1-L513) | [`ChatModel.swift`](../Shared/Model/ChatModel.swift#L1-L1373) | [`SimpleXAPI.swift`](../Shared/Model/SimpleXAPI.swift#L1-L2915) | [`AppAPITypes.swift`](../Shared/Model/AppAPITypes.swift#L1-L2357) | [`APITypes.swift`](../SimpleXChat/APITypes.swift#L1-L1071) | [`API.swift`](../SimpleXChat/API.swift#L1-L388) +**Source:** [`SimpleXApp.swift`](../Shared/SimpleXApp.swift#L1-L183) | [`AppDelegate.swift`](../Shared/AppDelegate.swift#L1-L209) | [`ContentView.swift`](../Shared/ContentView.swift#L1-L513) | [`ChatModel.swift`](../Shared/Model/ChatModel.swift#L1-L1373) | [`SimpleXAPI.swift`](../Shared/Model/SimpleXAPI.swift#L1-L2915) | [`AppAPITypes.swift`](../Shared/Model/AppAPITypes.swift#L1-L2357) | [`APITypes.swift`](../SimpleXChat/APITypes.swift#L1-L1071) | [`API.swift`](../SimpleXChat/API.swift#L1-L429) --- @@ -255,7 +255,7 @@ The Share Extension (`SimpleX SE/`) allows sharing content (text, images, files) --- -## [7. Remote Desktop Control](../Shared/Views/RemoteAccess/ConnectDesktopView.swift#L1-L545) +## [7. Remote Desktop Control](../Shared/Views/RemoteAccess/ConnectDesktopView.swift#L1-L556) Optional desktop pairing allows controlling the mobile app from a desktop client: @@ -263,7 +263,7 @@ Optional desktop pairing allows controlling the mobile app from a desktop client - **Commands**: [`connectRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1613), [`findKnownRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1620), [`confirmRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1624), [`verifyRemoteCtrlSession`](../Shared/Model/SimpleXAPI.swift#L1630), [`listRemoteCtrls`](../Shared/Model/SimpleXAPI.swift#L1636), [`stopRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1642), [`deleteRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1646) - **State**: [`ChatModel.remoteCtrlSession`](../Shared/Model/ChatModel.swift#L395)`: RemoteCtrlSession?` tracks the active session - **Transport**: Encrypted reverse HTTP transport between mobile and desktop -- **Source**: [`Shared/Views/RemoteAccess/ConnectDesktopView.swift`](../Shared/Views/RemoteAccess/ConnectDesktopView.swift#L1-L545), see `Remote.hs` in `../../src/Simplex/Chat/` +- **Source**: [`Shared/Views/RemoteAccess/ConnectDesktopView.swift`](../Shared/Views/RemoteAccess/ConnectDesktopView.swift#L1-L556), see `Remote.hs` in `../../src/Simplex/Chat/` --- diff --git a/apps/ios/spec/client/navigation.md b/apps/ios/spec/client/navigation.md index 64d0940e39..386fbb1543 100644 --- a/apps/ios/spec/client/navigation.md +++ b/apps/ios/spec/client/navigation.md @@ -329,14 +329,22 @@ In `connectViaUrl_()`, relay address links (URL path `/r`) are intercepted befor ```swift if path == "/r" { showAlert(NSLocalizedString("Relay address", ...), - message: NSLocalizedString("This is a chat relay address, it cannot be used to connect.", ...)) - return + message: NSLocalizedString("This is a chat relay address, it cannot be used to connect. To use it, open Network & servers, Your servers, Add server, then Chat relay, paste the address, then Test relay.", ...)) } ``` Similarly, in `planAndConnect()` (`NewChatView.swift`), `.simplexLink(_, .relay, _, _)` patterns trigger the same alert and block connection. -## 11. Channel-Specific NewChatView Behavior +## 11. Scanned Link Classification + +**Source:** [`Shared/Views/NewChat/WrongQRCode.swift`](../../Shared/Views/NewChat/WrongQRCode.swift) + +Each scanner, and the Migrate-to-device paste button, routes its input through +`checkLink()` first. A recognised code of the wrong kind is reported by +`wrongQRCodeMessage(_:)` / `wrongQRCodeAlert(_:)`, which name what the code is and +where to use it; an unrecognised string falls back to that scanner's own error. + +## 12. Channel-Specific NewChatView Behavior **Source:** [`Shared/Views/NewChat/NewChatView.swift`](../../Shared/Views/NewChat/NewChatView.swift) @@ -372,6 +380,7 @@ For channels (`groupInfo.useRelays`): the title is "Open channel"; for groups, " | New chat view | [`Shared/Views/NewChat/NewChatView.swift`](../../Shared/Views/NewChat/NewChatView.swift) | | Channel creation | [`Shared/Views/NewChat/AddChannelView.swift`](../../Shared/Views/NewChat/AddChannelView.swift) | | New chat menu | [`Shared/Views/NewChat/NewChatMenuButton.swift`](../../Shared/Views/NewChat/NewChatMenuButton.swift) | +| Wrong QR code alert | [`Shared/Views/NewChat/WrongQRCode.swift`](../../Shared/Views/NewChat/WrongQRCode.swift) | | Settings view | [`Shared/Views/UserSettings/SettingsView.swift`](../../Shared/Views/UserSettings/SettingsView.swift) | | User profiles | [`Shared/Views/UserSettings/UserProfilesView.swift`](../../Shared/Views/UserSettings/UserProfilesView.swift) | | Onboarding view | [`Shared/Views/Onboarding/OnboardingView.swift`](../../Shared/Views/Onboarding/OnboardingView.swift) | diff --git a/apps/ios/spec/impact.md b/apps/ios/spec/impact.md index 74acec789e..66444d8040 100644 --- a/apps/ios/spec/impact.md +++ b/apps/ios/spec/impact.md @@ -64,6 +64,7 @@ | Shared/Views/Chat/Group/GroupMemberInfoView.swift | PC3, PC14, PC16, PC30, PC31 | Medium | Member details and role management; rejected-by-operator status row for relay members | | Shared/Views/NewChat/NewChatView.swift | PC12, PC31 | High | New connection creation — onramp for all contacts and channels | | Shared/Views/NewChat/QRCode.swift | PC12 | Low | QR code display/scanning utility | +| Shared/Views/NewChat/WrongQRCode.swift | PC12, PC13, PC25, PC26, PC27 | Low | Wrong-QR-code alert message shared by all scanners | | Shared/Views/Call/ActiveCallView.swift | PC17 | Medium | Call UI rendering | | Shared/Views/Call/CallController.swift | PC17 | High | CallKit integration — call lifecycle | | Shared/Views/Call/WebRTCClient.swift | PC17 | High | WebRTC session management | diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 35d4d3fcde..8d38308d67 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -820,10 +820,10 @@ Wrong link This is a %s. To use it, open New chat, then scan or paste it there. This is a SimpleX server address. To use it, open Network & servers, Your servers, Add server, then Scan server QR code. - This is a link to migrate to another device. To use it, when setting up a new device, choose to migrate from another device. + This is a link to migrate to another device. To use it, choose Migrate when setting up a new device. This is an address to connect to a desktop app. To use it, open Use from desktop and scan the QR code shown in the desktop app. This is a security code. To use it, open the chat, then the contact\'s or member\'s name, then Verify security code. - This is a %s. To use it, open Network & servers, Your servers, Add server, then Chat relay, and paste the address there. + This is a %s. To use it, open Network & servers, Your servers, Add server, then Chat relay, paste the address, then Test relay. Permission Denied! @@ -3191,7 +3191,7 @@ Relay address - This is a chat relay address, it cannot be used to connect. To use it, open Network & servers, Your servers, Add server, then Chat relay, and paste the address there. + This is a chat relay address, it cannot be used to connect. To use it, open Network & servers, Your servers, Add server, then Chat relay, paste the address, then Test relay. Open channel Open new channel You are a subscriber