ios: review fixes for wrong QR code recognition

Follow-up to "Recognise wrong QR code type in every scanner", from an iOS
review of that commit.

- ScanProtocolServer: a rejected scan now alerts in place instead of
  dismissing, but the sheet had no way out - move its title into the nav bar
  and add a Cancel button. Fixes the preview, which needs AppTheme now that
  the themed background is applied inside the view.
- MigrateToDevice: classify the pasted link off the main thread, as the
  Kotlin paste path already does - pasted text is unbounded and classifying
  a file link parses its YAML description. Guard the result, as the sheet can
  be dismissed while classifying.
- Relay message named the Chat relay screen but not that the address is only
  kept once the relay is tested, so following it lost the pasted address to
  an "Invalid relay name!" alert. The migration message named a label that
  only exists on desktop.
- ContentView: the /r deep link showed the relay alert without the guidance
  added to the other call site; both literals were identical before.
- Document WrongQRCode.swift per apps/ios/CODE.md, and refresh the specs and
  product docs that quote the changed messages.
This commit is contained in:
Narasimha-sc
2026-09-02 19:06:00 +00:00
parent 992f7f9781
commit f48377a3fa
14 changed files with 60 additions and 51 deletions
+1
View File
@@ -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 |
+1 -1
View File
@@ -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()
@@ -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: {
@@ -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
@@ -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))
}
@@ -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<ScanResult, ScanError>) {
@@ -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())
}
}
+6 -6
View File
@@ -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) {
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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/`
---
+12 -3
View File
@@ -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) |
+1
View File
@@ -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 |
@@ -820,10 +820,10 @@
<string name="wrong_link">Wrong link</string>
<string name="wrong_qr_connection_link">This is a %s. To use it, open New chat, then scan or paste it there.</string>
<string name="wrong_qr_server_address">This is a SimpleX server address. To use it, open Network &amp; servers, Your servers, Add server, then Scan server QR code.</string>
<string name="wrong_qr_migration_link">This is a link to migrate to another device. To use it, when setting up a new device, choose to migrate from another device.</string>
<string name="wrong_qr_migration_link">This is a link to migrate to another device. To use it, choose Migrate when setting up a new device.</string>
<string name="wrong_qr_desktop_address">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.</string>
<string name="wrong_qr_security_code">This is a security code. To use it, open the chat, then the contact\'s or member\'s name, then Verify security code.</string>
<string name="wrong_qr_relay_address">This is a %s. To use it, open Network &amp; servers, Your servers, Add server, then Chat relay, and paste the address there.</string>
<string name="wrong_qr_relay_address">This is a %s. To use it, open Network &amp; servers, Your servers, Add server, then Chat relay, paste the address, then Test relay.</string>
<!-- GetImageView -->
<string name="toast_permission_denied">Permission Denied!</string>
@@ -3191,7 +3191,7 @@
<!-- ConnectPlan.kt channel-related -->
<string name="relay_address_alert_title">Relay address</string>
<string name="relay_address_alert_message">This is a chat relay address, it cannot be used to connect. To use it, open Network &amp; servers, Your servers, Add server, then Chat relay, and paste the address there.</string>
<string name="relay_address_alert_message">This is a chat relay address, it cannot be used to connect. To use it, open Network &amp; servers, Your servers, Add server, then Chat relay, paste the address, then Test relay.</string>
<string name="connect_plan_open_channel">Open channel</string>
<string name="connect_plan_open_new_channel">Open new channel</string>
<string name="connect_plan_you_are_subscriber">You are a subscriber</string>