Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 109 additions & 6 deletions Bitkit/MainNavView.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
import SwiftUI

func canRoutePubkyContactLink(
isPaykitUIActive: Bool,
isPubkyInitialized: Bool,
hasLoadedContacts: Bool
) -> Bool {
!isPaykitUIActive || (isPubkyInitialized && hasLoadedContacts)
}

func pubkyContactPublicKeyForRouting(from url: URL, isPaykitUIActive: Bool) throws -> String? {
guard isPaykitUIActive else { return nil }
guard let publicKey = PubkyContactLink.publicKey(from: url) else {
throw ContactsManagerError.invalidPublicKey
}
return publicKey
}

@MainActor
func prepareAndRoutePendingDeepLink(
preparation: () async -> Void,
routing: () async -> Void
) async {
await preparation()
guard !Task.isCancelled else { return }
await routing()
}

enum PendingProfileSetupResumeState {
case inactive
case waiting
Expand Down Expand Up @@ -42,6 +68,7 @@ struct MainNavView: View {
@EnvironmentObject private var navigation: NavigationViewModel
@EnvironmentObject private var notificationManager: PushNotificationManager
@EnvironmentObject private var pubkyProfile: PubkyProfileManager
@EnvironmentObject private var scannerManager: ScannerManager
@EnvironmentObject private var settings: SettingsViewModel
@EnvironmentObject private var sheets: SheetViewModel
@EnvironmentObject private var wallet: WalletViewModel
Expand All @@ -54,6 +81,7 @@ struct MainNavView: View {
@State private var showClipboardAlert = false
@State private var clipboardUri: String?
@State private var didResumePendingPubkyProfileSetup = false
@State private var isPreparingPendingContactDeepLink = false
init(canHandleDeepLinks: Bool = true) {
self.canHandleDeepLinks = canHandleDeepLinks
}
Expand All @@ -62,6 +90,18 @@ struct MainNavView: View {
PaykitFeatureFlags.isUIAvailable && isPaykitUIEnabled
}

private var isContactDeepLinkReady: Bool {
canRoutePubkyContactLink(
isPaykitUIActive: isPaykitUIActive,
isPubkyInitialized: pubkyProfile.isInitialized,
hasLoadedContacts: contactsManager.hasLoaded
)
}

private var canPrepareContactDeepLink: Bool {
!isPaykitUIActive || pubkyProfile.isInitialized
}

private var pendingProfileSetupResumeState: PendingProfileSetupResumeState {
resolvePendingProfileSetupResumeState(
isProfileSetupPending: pubkyProfile.isProfileSetupPending,
Expand Down Expand Up @@ -378,14 +418,30 @@ struct MainNavView: View {
notificationManager.unregister()
}
}
.task(id: [canHandleDeepLinks, wallet.nodeLifecycleState == .running]) {
.task(id: canHandleDeepLinks) {
guard canHandleDeepLinks else { return }
await handlePendingDeepLink()
}
.onChange(of: app.pendingDeepLinkURL) { _, url in
guard canHandleDeepLinks, url != nil else { return }
Task { await handlePendingDeepLink() }
}
.task(id: wallet.nodeLifecycleState == .running) {
guard canHandleDeepLinks,
wallet.nodeLifecycleState == .running,
let url = app.pendingDeepLinkURL,
!PubkyContactLink.matches(url)
else { return }
await handlePendingDeepLink()
}
.onChange(of: canPrepareContactDeepLink) { _, canPrepare in
guard canHandleDeepLinks, canPrepare else { return }
Task { await handlePendingDeepLink() }
}
.onChange(of: isContactDeepLinkReady) { _, isReady in
guard canHandleDeepLinks, isReady else { return }
Task { await handlePendingDeepLink() }
}
.alert(
t("other__clipboard_redirect_title"),
isPresented: $showClipboardAlert
Expand Down Expand Up @@ -764,11 +820,38 @@ struct MainNavView: View {
}

private func handlePendingDeepLink() async {
await app.routePendingDeepLinkIfReady(
canHandleDeepLinks,
nodeIsRunning: wallet.nodeLifecycleState == .running
) { url in
await handleDeepLink(url)
await prepareAndRoutePendingDeepLink {
await loadContactsForPendingDeepLinkIfNeeded()
} routing: {
await app.routePendingDeepLinkIfReady(
canHandleDeepLinks,
nodeIsRunning: wallet.nodeLifecycleState == .running,
pubkyContactsAreReady: isContactDeepLinkReady
) { url in
await handleDeepLink(url)
}
}
}

private func loadContactsForPendingDeepLinkIfNeeded() async {
guard isPaykitUIActive,
pubkyProfile.isInitialized,
!contactsManager.hasLoaded,
!isPreparingPendingContactDeepLink,
let url = app.pendingDeepLinkURL,
let contactPublicKey = PubkyContactLink.publicKey(from: url)
else { return }

let contactsOwnerPublicKey = pubkyProfile.publicKey ?? contactPublicKey
isPreparingPendingContactDeepLink = true
defer { isPreparingPendingContactDeepLink = false }

do {
try await contactsManager.loadContactsIfNeeded(for: contactsOwnerPublicKey)
} catch is CancellationError {
return
} catch {
Logger.warn("Failed to load contacts before routing contact link: \(error)", context: "MainNavView")
}
}

Expand Down Expand Up @@ -813,6 +896,26 @@ struct MainNavView: View {
}

do {
if PubkyContactLink.matches(url) {
guard let publicKey = try pubkyContactPublicKeyForRouting(from: url, isPaykitUIActive: isPaykitUIActive) else { return }
guard pubkyProfile.initializationErrorMessage == nil,
contactsManager.hasLoaded
else { throw ContactsManagerError.invalidPublicKey }

scannerManager.configure(
app: app,
contactsManager: contactsManager,
currency: currency,
settings: settings,
navigation: navigation,
pubkyProfile: pubkyProfile,
sheets: sheets,
wallet: wallet,
hwWalletManager: hwWalletManager
)
await scannerManager.handleScan(publicKey, context: .main)
return
}
try await app.handleScannedData(
url.absoluteString,
alternativeOnchainBalanceSats: hwWalletManager.maximumFundingBalanceSats
Expand Down
8 changes: 6 additions & 2 deletions Bitkit/Managers/ContactsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,13 @@ class ContactsManager: ObservableObject {

func loadContactsIfNeeded(for publicKey: String) async throws {
while !hasLoaded {
try Task.checkCancellation()
if isLoading {
for await isLoading in $isLoading.values where !isLoading {
break
for await isLoading in $isLoading.values {
try Task.checkCancellation()
if !isLoading {
break
}
}
} else {
try await loadContacts(for: publicKey)
Expand Down
20 changes: 20 additions & 0 deletions Bitkit/Models/PubkyContactLink.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Foundation

enum PubkyContactLink {
static func matches(_ url: URL) -> Bool {
url.scheme?.lowercased() == "bitkit" && url.host?.lowercased() == "contact"
}

static func publicKey(from url: URL) -> String? {
guard matches(url),
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
components.user == nil, components.password == nil, components.port == nil,
components.path.isEmpty, components.fragment == nil,
let items = components.queryItems, items.count == 1,
items[0].name == "pubky", let key = items[0].value,
key.count <= PubkyPublicKeyFormat.maximumInputLength
else { return nil }

return PubkyPublicKeyFormat.normalized(key)
}
}
13 changes: 12 additions & 1 deletion Bitkit/ViewModels/AppViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,16 @@ class AppViewModel: ObservableObject {
pendingDeepLinkURL = url
}

func routePendingDeepLinkIfReady(_ isReady: Bool, nodeIsRunning: Bool = false, handler: (URL) async -> Void) async {
func routePendingDeepLinkIfReady(
_ isReady: Bool,
nodeIsRunning: Bool = false,
pubkyContactsAreReady: Bool = true,
handler: (URL) async -> Void
) async {
guard isReady, let url = pendingDeepLinkURL else { return }
if PubkyContactLink.publicKey(from: url) != nil, !pubkyContactsAreReady {
return
}
if Self.requiresLightningNode(url), !nodeIsRunning {
return
}
Expand All @@ -171,6 +179,9 @@ class AppViewModel: ObservableObject {
if PubkyRingAuthCallback.parse(url: url) != nil {
return false
}
if PubkyContactLink.matches(url) {
return false
}
if url.scheme?.lowercased() == "bitkit",
url.host?.lowercased() == "pubky-auth",
url.path == "/setup"
Expand Down
Loading
Loading