nvrsion: Add fix for QR Code Scan Failure in AppStore, Sync, and Tests; explore connection failures over local network; update Info.plist, Models, and SettingsView.
Nucleic-Promote: 1 Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
@@ -6,6 +6,13 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>_nucleic._tcp</string>
|
<string>_nucleic._tcp</string>
|
||||||
</array>
|
</array>
|
||||||
|
<!-- iOS 14+ gates ALL local-network access — both Bonjour discovery (_nucleic._tcp above)
|
||||||
|
and a direct NWConnection to the Mac's LAN IP from the pairing QR — behind the
|
||||||
|
local-network permission, which only prompts if this usage string is present. Without
|
||||||
|
it the grant never happens and every LAN connection fails silently, so pairing and
|
||||||
|
reconnect appear to "do nothing." -->
|
||||||
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
|
<string>Nucleic connects to your Mac on the local network to pair and to sync your sessions and approvals.</string>
|
||||||
<key>NucleicChannel</key>
|
<key>NucleicChannel</key>
|
||||||
<string>$(NUCLEIC_CHANNEL)</string>
|
<string>$(NUCLEIC_CHANNEL)</string>
|
||||||
<!-- The short git commit this build came from, shown in the non-release banner to match
|
<!-- The short git commit this build came from, shown in the non-release banner to match
|
||||||
|
|||||||
@@ -695,7 +695,35 @@ final class RemoteStore: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func makeChannel(_ endpoint: NWEndpoint) -> NWFrameChannel {
|
private func makeChannel(_ endpoint: NWEndpoint) -> NWFrameChannel {
|
||||||
NWFrameChannel(endpoint: endpoint)
|
let channel = NWFrameChannel(endpoint: endpoint)
|
||||||
|
// Surface a transport-level failure with an actionable message. Without this, a refused
|
||||||
|
// connection or a denied local-network permission finished the frame stream and only
|
||||||
|
// showed up as a generic "handshake closed" — hiding the real (usually fixable) cause.
|
||||||
|
channel.onFailed = { [weak self] error in
|
||||||
|
Task { @MainActor in self?.handleTransportFailure(error) }
|
||||||
|
}
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map an `NWConnection` failure to a connectivity state the pairing/onboarding UI can act on.
|
||||||
|
/// Only meaningful while we're still establishing the link; once `.connected`, a drop is the
|
||||||
|
/// normal `.closed` → reconnect path's job.
|
||||||
|
private func handleTransportFailure(_ error: String) {
|
||||||
|
guard connectivity != .connected else { return }
|
||||||
|
connectivity = .failed(Self.friendlyTransportError(error))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turn a raw `NWError` string into a short, fixable hint. The common onboarding failures are
|
||||||
|
/// a denied Local Network permission (EPERM / -65555) and the Mac not listening (refused).
|
||||||
|
private static func friendlyTransportError(_ error: String) -> String {
|
||||||
|
let lower = error.lowercased()
|
||||||
|
if lower.contains("denied") || lower.contains("not permitted") || lower.contains("65555") {
|
||||||
|
return "Can't reach the local network. In Settings ▸ Nucleic, allow Local Network access, then try again."
|
||||||
|
}
|
||||||
|
if lower.contains("refused") {
|
||||||
|
return "Your Mac refused the connection. Check that remote access is still on in Nucleic ▸ Settings."
|
||||||
|
}
|
||||||
|
return "Couldn't connect to your Mac — make sure it's on the same Wi‑Fi and remote access is on."
|
||||||
}
|
}
|
||||||
|
|
||||||
private func resolveEndpoint(fingerprint: String?, lanHost: String?, lanPort: UInt16?) -> NWEndpoint? {
|
private func resolveEndpoint(fingerprint: String?, lanHost: String?, lanPort: UInt16?) -> NWEndpoint? {
|
||||||
|
|||||||
@@ -126,6 +126,10 @@ struct PairingIntroView: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
|
|
||||||
|
// Feedback after a scan: the whole UI is still the intro until pairing completes, so
|
||||||
|
// without this a failed or in-flight pair looks like "nothing happened."
|
||||||
|
pairingStatus
|
||||||
|
|
||||||
VStack(spacing: 6) {
|
VStack(spacing: 6) {
|
||||||
Button { store.enterDemo() } label: {
|
Button { store.enterDemo() } label: {
|
||||||
Label("Explore a demo — no Mac needed", systemImage: "play.circle")
|
Label("Explore a demo — no Mac needed", systemImage: "play.circle")
|
||||||
@@ -151,4 +155,26 @@ struct PairingIntroView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A status line reflecting an in-flight or failed pairing attempt. Silent in the resting
|
||||||
|
/// `.unpaired` state (the intro copy already covers that).
|
||||||
|
@ViewBuilder
|
||||||
|
private var pairingStatus: some View {
|
||||||
|
switch store.connectivity {
|
||||||
|
case .connecting, .reconnecting:
|
||||||
|
Label("Connecting to your Mac…", systemImage: "arrow.triangle.2.circlepath")
|
||||||
|
.font(.footnote).foregroundStyle(.secondary)
|
||||||
|
case .hostOffline:
|
||||||
|
Label("Can't reach your Mac. Make sure it's on the same Wi‑Fi and remote access is on.",
|
||||||
|
systemImage: "wifi.exclamationmark")
|
||||||
|
.font(.footnote).foregroundStyle(.orange)
|
||||||
|
.multilineTextAlignment(.center).padding(.horizontal, 32)
|
||||||
|
case .failed(let message):
|
||||||
|
Label(message, systemImage: "exclamationmark.triangle.fill")
|
||||||
|
.font(.footnote).foregroundStyle(.red)
|
||||||
|
.multilineTextAlignment(.center).padding(.horizontal, 32)
|
||||||
|
case .connected, .unpaired:
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user