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
This commit is contained in:
spaced4ndy
2026-07-27 18:00:32 +01:00
committed by GitHub
parent 6152f2a7ee
commit b414d7579a
3 changed files with 48 additions and 30 deletions
+38 -26
View File
@@ -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) {
+4 -1
View File
@@ -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?()
}
}
}
@@ -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
}
}
}