From 836e77449cdd36cd60e227d541397569d6eba22a Mon Sep 17 00:00:00 2001 From: Mariano <132747814+mbelinky@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:12:53 +0000 Subject: [PATCH] iOS onboarding: stop auth step-3 retry loop churn (#19153) Merged via /review-pr -> /prepare-pr -> /merge-pr. Prepared head SHA: a38ec42bdd4cf1bf5743ecd3c1d1f2bcceea91e0 Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com> Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com> Reviewed-by: @mbelinky --- CHANGELOG.md | 1 + apps/ios/Sources/Model/NodeAppModel.swift | 18 +++++++++++++++++- .../Onboarding/OnboardingWizardView.swift | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba0f382bb7..067d04f2b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Voice call/Gateway: prevent overlapping closed-loop turn races with per-call turn locking, route transcript dedupe via source-aware fingerprints with strict cache eviction bounds, and harden `voicecall latency` stats for large logs without spread-operator stack overflow. (#19140) Thanks @mbelinky. +- iOS/Onboarding: stop auth Step 3 retry-loop churn by pausing reconnect attempts on unauthorized/missing-token gateway errors and keeping auth/pairing issue state sticky during manual retry. (#19153) Thanks @mbelinky. - Fix types in all tests. Typecheck the whole repository. - Voice-call: auto-end calls when media streams disconnect to prevent stuck active calls. (#18435) Thanks @JayMishra-source. - Gateway/Channels: wire `gateway.channelHealthCheckMinutes` into strict config validation, treat implicit account status as managed for health checks, and harden channel auto-restart flow (preserve restart-attempt caps across crash loops, propagate enabled/configured runtime flags, and stop pending restart backoff after manual stop). Thanks @steipete. diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift index 75950f55a4..5b59af1585 100644 --- a/apps/ios/Sources/Model/NodeAppModel.swift +++ b/apps/ios/Sources/Model/NodeAppModel.swift @@ -1626,6 +1626,10 @@ private extension NodeAppModel { try? await Task.sleep(nanoseconds: 1_000_000_000) continue } + if !self.gatewayAutoReconnectEnabled { + try? await Task.sleep(nanoseconds: 1_000_000_000) + continue + } if await self.isOperatorConnected() { try? await Task.sleep(nanoseconds: 1_000_000_000) continue @@ -1708,6 +1712,10 @@ private extension NodeAppModel { try? await Task.sleep(nanoseconds: 1_000_000_000) continue } + if !self.gatewayAutoReconnectEnabled { + try? await Task.sleep(nanoseconds: 1_000_000_000) + continue + } if await self.isGatewayConnected() { try? await Task.sleep(nanoseconds: 1_000_000_000) continue @@ -1795,10 +1803,18 @@ private extension NodeAppModel { } GatewayDiagnostics.log("gateway connect error: \(error.localizedDescription)") + // If auth is missing/rejected, pause reconnect churn until the user intervenes. + // Reconnect loops only spam the same failing handshake and make onboarding noisy. + let lower = error.localizedDescription.lowercased() + if lower.contains("unauthorized") || lower.contains("gateway token missing") { + await MainActor.run { + self.gatewayAutoReconnectEnabled = false + } + } + // If pairing is required, stop reconnect churn. The user must approve the request // on the gateway before another connect attempt will succeed, and retry loops can // generate multiple pending requests. - let lower = error.localizedDescription.lowercased() if lower.contains("not_paired") || lower.contains("pairing required") { let requestId: String? = { // GatewayResponseError for connect decorates the message with `(requestId: ...)`. diff --git a/apps/ios/Sources/Onboarding/OnboardingWizardView.swift b/apps/ios/Sources/Onboarding/OnboardingWizardView.swift index 7320099f19..cbe9db2575 100644 --- a/apps/ios/Sources/Onboarding/OnboardingWizardView.swift +++ b/apps/ios/Sources/Onboarding/OnboardingWizardView.swift @@ -818,7 +818,7 @@ struct OnboardingWizardView: View { private func retryLastAttempt() async { self.connectingGatewayID = "retry" - self.issue = .none + // Keep current auth/pairing issue sticky while retrying to avoid Step 3 UI flip-flop. self.connectMessage = "Retrying…" self.statusLine = "Retrying last connection…" defer { self.connectingGatewayID = nil }