From 6e7f1a6a1bb765061924178afbb8d73c7e360c26 Mon Sep 17 00:00:00 2001 From: Mariano <132747814+mbelinky@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:39:41 +0000 Subject: [PATCH] iOS onboarding: prevent pairing flicker during auto-resume (#20310) Merged via /review-pr -> /prepare-pr -> /merge-pr. Prepared head SHA: 691808b747625e94158c4e5ec9d418edab593083 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 + .../Onboarding/OnboardingWizardView.swift | 27 +++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39c54a169d7..a52ab9be370 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- iOS/Onboarding: prevent pairing-status flicker during auto-resume by keeping resumed state transitions stable. (#20310) Thanks @mbelinky. - OpenClawKit/Protocol: preserve JSON boolean literals (`true`/`false`) when bridging through `AnyCodable` so Apple client RPC params no longer re-encode booleans as `1`/`0`. Thanks @mbelinky. - iOS/Onboarding: stabilize pairing and reconnect behavior by resetting stale pairing request state on manual retry, disconnecting both operator and node gateways on operator failure, and avoiding duplicate pairing loops from operator transport identity attachment. (#20056) Thanks @mbelinky. - Browser/Relay: reuse an already-running extension relay when the relay port is occupied by another OpenClaw process, while still failing on non-relay port collisions to avoid masking unrelated listeners. (#20035) Thanks @mbelinky. diff --git a/apps/ios/Sources/Onboarding/OnboardingWizardView.swift b/apps/ios/Sources/Onboarding/OnboardingWizardView.swift index 2dda00f1852..c0e872b2ceb 100644 --- a/apps/ios/Sources/Onboarding/OnboardingWizardView.swift +++ b/apps/ios/Sources/Onboarding/OnboardingWizardView.swift @@ -1,4 +1,5 @@ import CoreImage +import Combine import OpenClawKit import PhotosUI import SwiftUI @@ -68,6 +69,7 @@ struct OnboardingWizardView: View { @State private var scannerError: String? @State private var selectedPhoto: PhotosPickerItem? @State private var lastPairingAutoResumeAttemptAt: Date? + private static let pairingAutoResumeTicker = Timer.publish(every: 2.0, on: .main, in: .common).autoconnect() let allowSkip: Bool let onClose: () -> Void @@ -271,6 +273,7 @@ struct OnboardingWizardView: View { } .onChange(of: self.appModel.gatewayServerName) { _, newValue in guard newValue != nil else { return } + self.showQRScanner = false self.statusLine = "Connected." if !self.didMarkCompleted, let selectedMode { OnboardingStateStore.markCompleted(mode: selectedMode) @@ -282,6 +285,9 @@ struct OnboardingWizardView: View { guard newValue == .active else { return } self.attemptAutomaticPairingResumeIfNeeded() } + .onReceive(Self.pairingAutoResumeTicker) { _ in + self.attemptAutomaticPairingResumeIfNeeded() + } } @ViewBuilder @@ -685,7 +691,16 @@ struct OnboardingWizardView: View { Task { await self.retryLastAttempt() } } + private func resumeAfterPairingApprovalInBackground() { + // Keep the pairing issue sticky to avoid visual flicker while we probe for approval. + self.appModel.gatewayAutoReconnectEnabled = true + self.appModel.gatewayPairingPaused = false + self.appModel.gatewayPairingRequestId = nil + Task { await self.retryLastAttempt(silent: true) } + } + private func attemptAutomaticPairingResumeIfNeeded() { + guard self.scenePhase == .active else { return } guard self.step == .auth else { return } guard self.issue.needsPairing else { return } guard self.connectingGatewayID == nil else { return } @@ -695,7 +710,7 @@ struct OnboardingWizardView: View { return } self.lastPairingAutoResumeAttemptAt = now - self.resumeAfterPairingApproval() + self.resumeAfterPairingApprovalInBackground() } private func detectQRCode(from data: Data) -> String? { @@ -837,11 +852,13 @@ struct OnboardingWizardView: View { await self.gatewayController.connectManual(host: host, port: self.manualPort, useTLS: self.manualTLS) } - private func retryLastAttempt() async { - self.connectingGatewayID = "retry" + private func retryLastAttempt(silent: Bool = false) async { + self.connectingGatewayID = silent ? "retry-auto" : "retry" // Keep current auth/pairing issue sticky while retrying to avoid Step 3 UI flip-flop. - self.connectMessage = "Retrying…" - self.statusLine = "Retrying last connection…" + if !silent { + self.connectMessage = "Retrying…" + self.statusLine = "Retrying last connection…" + } defer { self.connectingGatewayID = nil } await self.gatewayController.connectLastKnown() }