Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation

/// The minimal pairing-QR grammars for Iroh identity and Tailscale routes.
///
/// Current Iroh codes carry only the stable EndpointID:
/// Retained Iroh codes carry only the stable EndpointID:
/// `cmux-ios://attach?v=3&i=<endpoint-id>`.
///
/// The EndpointID is the only value the phone needs before dialing. The
Expand Down Expand Up @@ -41,13 +41,12 @@ import Foundation
/// Plain text is also smaller, which lowers the QR version (fewer, larger
/// modules) and makes the code scan faster from a Mac screen.
///
/// Compatibility: these grammars only ever appear in the Mac's pairing QR.
/// v2 remains decodable; an older iPhone presented with a v3 Iroh code gets
/// the existing update-app error and can use the Tailscale compatibility code
/// when one is available. Workspace-scoped tickets, dev loopback tickets, and
/// every RPC consumer
/// keep the compact v1 JSON payload (``CmxAttachTicketCompactCoder``), and the
/// decoder keeps accepting both that and the legacy full-key grammar.
/// Compatibility: the Mac pairing window emits only a Tailscale pairing
/// payload. v3 remains decodable for existing Iroh links and explicit
/// device-attach flows. Workspace-scoped tickets, dev loopback tickets, and
/// every RPC consumer keep the compact v1 JSON payload
/// (``CmxAttachTicketCompactCoder``), and the decoder keeps accepting both that
/// and the legacy full-key grammar.
public struct CmxPairingQRCode: Sendable {
/// The newest grammar version this build can decode.
///
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
/// The private-route disclosure policy for a scannable attach payload.
///
/// Callers must choose explicitly so adding a route to a ticket cannot silently
/// add it to a QR code. The legacy mode exists only while released clients still
/// require Tailscale host routes during the Iroh migration.
/// add it to a QR code. The compatibility name is retained because its grammar
/// remains readable by released clients; the Mac pairing window uses it only
/// for the user-selected Tailscale path.
public enum CmxPairingRouteDisclosureMode: Equatable, Sendable {
/// Encode only Iroh EndpointIDs. All Iroh hints and every host/port or URL
/// route are removed.
case irohIdentityOnly
/// Preserve the pre-Iroh compact route grammar for released clients.
/// This may disclose private-network routes and must not become a default.
/// Preserve the pre-Iroh compact route grammar for a Tailscale pairing
/// code. This discloses the selected tailnet destination.
case legacyPrivateNetworkCompatibility
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import Foundation
/// decode here with both intentionally dropped: a pairing QR never expires,
/// and the Mac's name is read post-handshake from `mobile.host.status`.
/// Compact Iroh fallbacks disclose only EndpointID identity. The primary
/// scannable Iroh code uses ``CmxPairingQRCode`` instead; the explicit
/// compatibility mode temporarily retains released clients' legacy routes.
/// retained Iroh attach-code path uses ``CmxPairingQRCode`` instead; the
/// explicit compatibility mode temporarily retains released clients' legacy
/// routes.
struct CompactAttachTicket: Codable {
let v: Int
let w: String?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ extension MobilePairingFailureCategory {
case .invalidCode:
return L10n.string(
"mobile.pairing.invalidCode",
defaultValue: "This isn't a cmux pairing QR. Scan the code shown in the Pair iPhone window on your Mac."
defaultValue: "This isn't a cmux pairing QR. Scan the code shown in Tailscale Pairing on your Mac."
)
case .unrecognizedVersion:
return L10n.string(
Expand All @@ -267,7 +267,10 @@ extension MobilePairingFailureCategory {
case .loopbackRejected:
return L10n.string(
"mobile.pairing.loopbackRejected",
defaultValue: "This code points at the Mac itself (localhost), so your iPhone can't use it. Update cmux on the Mac and scan its Iroh code."
defaultValue: """
This code points at the Mac itself (localhost), so your iPhone can't use it. \
Open Tailscale Pairing on the Mac and scan a fresh code.
"""
)
case .macUpdateRequired:
return L10n.string(
Expand Down Expand Up @@ -356,7 +359,7 @@ extension MobilePairingFailureCategory {
case .ticketExpired, .unsupportedRoute, .noSupportedRoute:
return L10n.string(
"mobile.pairing.guidance.rescanFresh",
defaultValue: "Open the pairing window on your Mac and scan a fresh QR or link."
defaultValue: "Open Tailscale Pairing on your Mac and scan a fresh QR or link."
)
case .unrecognizedVersion:
return L10n.string(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ import Testing
// wrong "code" was entered.
let message = MobilePairingFailureCategory.invalidCode.message
#expect(!message.lowercased().contains("pairing code"))
#expect(message.localizedCaseInsensitiveContains("Tailscale"))
#expect(!message.isEmpty)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ struct MobileConnectionMethodSection: View {
case .tailscale:
L10n.string(
"mobile.settings.connectionMethod.tailscaleFooter",
defaultValue: "Connects over your Tailscale network first. Scan the Tailscale pairing code shown on your Mac once (cmux Settings → Pair iPhone) to enable it for that Mac."
defaultValue: """
Install Tailscale on this iPhone and your Mac, then connect both to the same Tailscale network. \
On your Mac, open Tailscale Pairing in cmux to show the QR, then scan it here once.
"""
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,12 @@ struct MobilePairingScannerPreview: View {
.frame(maxWidth: 280, maxHeight: 280)
.aspectRatio(1, contentMode: .fit)

Text(L10n.string(
"mobile.pairing.scannerInstruction",
defaultValue: "Position the Mac's QR code in the frame."
))
Text(MobilePairingScannerGuidanceCopy.text)
.font(.headline)
.multilineTextAlignment(.center)
.foregroundStyle(.white)
.padding(.horizontal, 32)
.accessibilityIdentifier("MobilePairingScannerGuidance")

Spacer(minLength: 24)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,26 @@ struct MobilePairingScannerSheet: View {
} else {
switch authorizationStatus {
case .authorized:
QRCodeScannerView { code in
dismiss()
onCode(code)
ZStack(alignment: .bottom) {
QRCodeScannerView { code in
dismiss()
onCode(code)
}
.ignoresSafeArea(edges: .bottom)

Text(MobilePairingScannerGuidanceCopy.text)
.font(.footnote.weight(.medium))
.multilineTextAlignment(.center)
.foregroundStyle(.white)
.padding(.horizontal, 14)
.padding(.vertical, 10)
.background(
.black.opacity(0.72),
in: RoundedRectangle(cornerRadius: 12, style: .continuous)
)
.padding(16)
.accessibilityIdentifier("MobilePairingScannerGuidance")
}
.ignoresSafeArea(edges: .bottom)
case .notDetermined:
ProgressView()
.accessibilityIdentifier("MobilePairingScannerPermissionProgress")
Expand Down Expand Up @@ -165,3 +180,15 @@ struct MobilePairingScannerSheet: View {
}
}
#endif

enum MobilePairingScannerGuidanceCopy {
static var text: String {
L10n.string(
"mobile.pairing.scannerInstruction",
defaultValue: """
On your Mac, open Tailscale Pairing in cmux to show the QR. \
Install Tailscale on both devices and connect them to the same Tailscale network first.
"""
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ struct OnboardingConnectionView: View {
if connectionMethod == .tailscale {
return L10n.string(
"mobile.onboarding.connect.tailscaleBody",
defaultValue: "Connect over your Tailscale network. Scan the pairing code shown on your Mac."
defaultValue: """
Install Tailscale on this iPhone and your Mac, then connect both to the same Tailscale network. \
On your Mac, open Tailscale Pairing in cmux to show the QR, then scan it here.
"""
)
}
return L10n.string(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,11 @@ struct PairingView: View {
} footer: {
Text(L10n.string(
"mobile.addDevice.help",
defaultValue: "Scan the Mac's Iroh QR. Manual host and port is only for loopback development in the simulator."
defaultValue: """
Install Tailscale on both devices and connect them to the same Tailscale network. \
On your Mac, open Tailscale Pairing in cmux to show the QR, then scan it here. \
Manual host and port entry is an advanced fallback for reconnecting an already paired Mac.
"""
Comment thread
azooz2003-bit marked this conversation as resolved.
))
}
.overlay(alignment: .topLeading) {
Expand Down Expand Up @@ -352,7 +356,7 @@ struct PairingView: View {
}
return L10n.string(
"mobile.addDevice.manualRouteWarning",
defaultValue: "Manual host and port bypasses Iroh. Account credentials are allowed only over simulator loopback; use the Mac's Iroh QR for physical devices."
defaultValue: "Manual credentials work only in the simulator. On a device, choose Tailscale and scan the Mac QR."
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ struct SetupHelpGateContent {
title: L10n.string("mobile.setupHelp.macAppTitle", defaultValue: "Run cmux on your computer"),
body: L10n.string(
"mobile.setupHelp.macAppBody",
defaultValue: "Install cmux on your computer, sign in to the same account, and leave it running. The computer then appears on this phone automatically. If it does not, open Pair iPhone in cmux on the computer and scan its QR code."
defaultValue: """
Install cmux on your computer, sign in to the same account, and leave it running. \
The computer then appears on this phone automatically. \
To pair through Tailscale, install Tailscale on both devices \
and connect them to the same Tailscale network. \
Then open Tailscale Pairing in cmux on the computer to show the QR and scan it here.
"""
),
link: nil,
identifierSuffix: "signedInNeverPaired",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,15 @@ extension Array where Element == CuratedSettingEntry {
.init(section: .sidebarAppearance, id: "right-max-width", title: "Dock Max Width", synonyms: "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit"),

// Mobile
.init(section: .mobile, id: "pairDevice", title: "Pair a Device", synonyms: "pair pairing add device qr qr code scan iphone ipad ios mobile tailscale connect onboarding sign in"),
.init(
section: .mobile,
id: "pairDevice",
title: String(localized: "settings.mobile.pairDevice", defaultValue: "Tailscale Pairing"),
synonyms: """
pair pairing add device qr qr code scan iphone ipad ios mobile \
tailscale connect onboarding sign in
"""
),
.init(section: .mobile, id: "iOSPairingHost", title: "iOS Pairing", synonyms: "ios iphone ipad mobile pairing local network permission sync"),
.init(section: .mobile, id: "iOSPairingPort", title: String(localized: "settings.mobile.port", defaultValue: "Pairing Port"), synonyms: "mobile.iOSPairingHost.port ios iphone mobile pairing port tcp listener firewall conflict"),
.init(section: .mobile, id: "iOSPairingDisplayName", title: String(localized: "settings.mobile.displayName", defaultValue: "Display Name"), synonyms: "mobile.iOSPairingHost.displayName ios iphone mobile pairing display name mac hostname device label"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,16 @@ public struct MobileSection: View {
SettingsCardRow(
configurationReview: .action,
searchAnchorID: "setting:mobile:pairDevice",
String(localized: "settings.mobile.pairDevice", defaultValue: "Pair a Device"),
String(localized: "settings.mobile.pairDevice", defaultValue: "Tailscale Pairing"),
subtitle: String(
localized: "settings.mobile.pairDevice.subtitle",
defaultValue: "Devices signed in to the same account connect automatically. Show a QR code to pair manually."
defaultValue: """
Devices signed in to the same account connect automatically. \
Use this QR only to pair through Tailscale.
"""
)
) {
Button(String(localized: "settings.mobile.pairDevice.button", defaultValue: "Pair…")) {
Button(String(localized: "settings.mobile.pairDevice.button", defaultValue: "Show Tailscale QR…")) {
hostActions.openMobilePairingWindow()
}
.buttonStyle(.bordered)
Expand Down
Loading