From b414d7579a609333d4d2741bb4e1fb16ecbe64b3 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:00:32 +0000 Subject: [PATCH] ios: fix connection dialogue not shown if app is opened via link before onboarding (#7301) * ios: fix connection dialogue not shown if app is opened via link before onboarding * wip * debug * wip * wip * wip * wip * wip * wip * wip * wip * comment --- apps/ios/Shared/ContentView.swift | 64 +++++++++++-------- apps/ios/Shared/Model/NtfManager.swift | 5 +- .../Onboarding/ChooseServerOperators.swift | 9 ++- 3 files changed, 48 insertions(+), 30 deletions(-) diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index ba49c767da..22d0da3829 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -257,26 +257,33 @@ struct ContentView: View { ChatListView(activeUserPickerSheet: $chatListUserPickerSheet) .redacted(reason: appSheetState.redactionReasons(protectScreen)) .onAppear { - requestNtfAuthorization() - // Local Authentication notice is to be shown on next start after onboarding is complete - if (!prefLANoticeShown && prefShowLANotice && chatModel.chats.count > 2) { - prefLANoticeShown = true - alertManager.showAlert(laNoticeAlert()) - } else if !chatModel.showCallView && CallController.shared.activeCallInvitation == nil { - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - if !noticesShown { - let showWhatsNew = shouldShowWhatsNew() - let showUpdatedConditions = chatModel.conditions.conditionsAction?.showNotice ?? false - noticesShown = showWhatsNew || showUpdatedConditions - if showWhatsNew || showUpdatedConditions { - noticesSheetItem = .whatsNew(updatedConditions: showUpdatedConditions) + // Connect only after the notifications prompt is resolved: the system prompt suspends + // the app (scene .inactive), which kills an in-flight connect and makes + // getTopViewController() nil. Deferring keeps the URL until the app is active again. + let openingViaLink = pendingConnectUrl != nil + requestNtfAuthorization(showDeniedAlert: !openingViaLink) { + connectViaUrl() + } + if !openingViaLink { + // Local Authentication notice is to be shown on next start after onboarding is complete + if (!prefLANoticeShown && prefShowLANotice && chatModel.chats.count > 2) { + prefLANoticeShown = true + alertManager.showAlert(laNoticeAlert()) + } else if !chatModel.showCallView && CallController.shared.activeCallInvitation == nil { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + if !noticesShown { + let showWhatsNew = shouldShowWhatsNew() + let showUpdatedConditions = chatModel.conditions.conditionsAction?.showNotice ?? false + noticesShown = showWhatsNew || showUpdatedConditions + if showWhatsNew || showUpdatedConditions { + noticesSheetItem = .whatsNew(updatedConditions: showUpdatedConditions) + } } } } + showReRegisterTokenAlert() } prefShowLANotice = true - connectViaUrl() - showReRegisterTokenAlert() } .onChange(of: chatModel.appOpenUrl) { _ in connectViaUrl() } .onChange(of: chatModel.reRegisterTknStatus) { _ in showReRegisterTokenAlert() } @@ -383,15 +390,16 @@ struct ContentView: View { } } - func requestNtfAuthorization() { + func requestNtfAuthorization(showDeniedAlert: Bool = true, whenDone: (() -> Void)? = nil) { NtfManager.shared.requestAuthorization( onDeny: { - if (!notificationAlertShown) { + if showDeniedAlert, !notificationAlertShown { notificationAlertShown = true alertManager.showAlert(notificationAlert()) } }, - onAuthorized: { notificationAlertShown = false } + onAuthorized: { notificationAlertShown = false }, + whenDone: { if let whenDone { DispatchQueue.main.async(execute: whenDone) } } ) } @@ -436,16 +444,20 @@ struct ContentView: View { } // Spec: spec/client/navigation.md#connectViaUrl + // a URL opened via link that is ready to be connected now (appOpenUrl immediately, or + // appOpenUrlLater once the app is active — see .onChange(of: scenePhase) in SimpleXApp) + private var pendingConnectUrl: URL? { + let m = ChatModel.shared + if let url = m.appOpenUrl { return url } + if let url = m.appOpenUrlLater, AppChatState.shared.value == .active, scenePhase == .active { return url } + return nil + } + func connectViaUrl() { let m = ChatModel.shared - if let url = m.appOpenUrl { - m.appOpenUrl = nil - connectViaUrl_(url) - } else if let url = m.appOpenUrlLater, AppChatState.shared.value == .active, scenePhase == .active { - // correcting branch in case .onChange(of: scenePhase) in SimpleXApp doesn't trigger and transfer appOpenUrlLater into appOpenUrl - m.appOpenUrlLater = nil - connectViaUrl_(url) - } + guard let url = pendingConnectUrl else { return } + if m.appOpenUrl != nil { m.appOpenUrl = nil } else { m.appOpenUrlLater = nil } + connectViaUrl_(url) } func connectViaUrl_(_ url: URL) { diff --git a/apps/ios/Shared/Model/NtfManager.swift b/apps/ios/Shared/Model/NtfManager.swift index c6c6e88d8c..efd28d0ea5 100644 --- a/apps/ios/Shared/Model/NtfManager.swift +++ b/apps/ios/Shared/Model/NtfManager.swift @@ -212,16 +212,18 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { } // Spec: spec/services/notifications.md#requestAuthorization - func requestAuthorization(onDeny denied: (()-> Void)? = nil, onAuthorized authorized: (()-> Void)? = nil) { + func requestAuthorization(onDeny denied: (()-> Void)? = nil, onAuthorized authorized: (()-> Void)? = nil, whenDone: (() -> Void)? = nil) { logger.debug("NtfManager.requestAuthorization") let center = UNUserNotificationCenter.current() center.getNotificationSettings { settings in switch settings.authorizationStatus { case .denied: denied?() + whenDone?() case .authorized: self.granted = true authorized?() + whenDone?() default: center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in if let error = error { @@ -230,6 +232,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { self.granted = granted authorized?() } + whenDone?() } } } diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift index 425f6e022f..2c21682dba 100644 --- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -184,10 +184,13 @@ struct OnboardingConditionsView: View { private func completeOnboarding() { let m = ChatModel.shared onboardingStageDefault.set(.onboardingComplete) - // dismiss any presented onboarding sheet and defer the swap, so the deep onboarding - // navigation stack isn't torn down mid-transition (crashes UIKit on completion) + // defer the stage swap off the Accept handler's call stack so the deep onboarding nav stack + // isn't torn down from inside its own event handling (UIKit crash on completion); the inner + // async guarantees this even if dismissAllSheets runs its completion synchronously. dismissAllSheets(animated: false) { - m.onboardingStage = .onboardingComplete + DispatchQueue.main.async { + m.onboardingStage = .onboardingComplete + } } }