Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
5386569
feat: upgrade paykit auth to rc50
ben-kaufman Aug 31, 2026
c1a03c4
chore: rename changelog fragment
ben-kaufman Aug 31, 2026
586fce5
fix: reconcile paykit auth failures
ben-kaufman Aug 31, 2026
f32b56f
test: cover private endpoint reconciliation
ben-kaufman Sep 1, 2026
b62fedf
test: use grant auth fixtures
ben-kaufman Sep 1, 2026
0e0725a
fix: approve external Pubky grants
ben-kaufman Sep 2, 2026
4befe20
fix: harden Pubky session recovery
ben-kaufman Sep 2, 2026
d383646
test: cover external auth client ID
ben-kaufman Sep 2, 2026
c23748a
fix: bound auth requester label
ben-kaufman Sep 3, 2026
35816dd
fix: clear abandoned paykit sessions
ben-kaufman Sep 3, 2026
d90a6a9
fix: harden Paykit session recovery
ben-kaufman Sep 4, 2026
2777d79
fix: reset forgotten Paykit sessions
ben-kaufman Sep 4, 2026
57dc1cc
fix: preserve payment request history
ben-kaufman Sep 4, 2026
748aaf0
fix: preserve private paykit receiver marker
ben-kaufman Sep 6, 2026
3447208
fix: scope auth recovery to its completed session
ben-kaufman Sep 7, 2026
70c51fa
refactor: remove paykit migration handling
ben-kaufman Sep 7, 2026
2834e7a
feat: support Pubky Ring signup
ben-kaufman Sep 2, 2026
027f28a
fix: complete Pubky Ring signup
ben-kaufman Sep 2, 2026
86e8068
feat: support direct Pubky signup
ben-kaufman Sep 2, 2026
00d8cae
fix: complete Pubky signup handoff
ben-kaufman Sep 3, 2026
1dcc1ce
fix: recover failed pubky signup
ben-kaufman Sep 3, 2026
f1a6adf
fix: recover and verify pubky signup state
ben-kaufman Sep 6, 2026
9320e11
fix: require consent for every pubky signup
ben-kaufman Sep 7, 2026
951c102
fix: preserve pubky homeserver on restore
ben-kaufman Sep 7, 2026
6646af9
refactor: simplify pubky profile setup wiring
ben-kaufman Sep 8, 2026
ae4b584
feat: open Pubky auth links
ovitrif Sep 2, 2026
9dac17b
fix: preserve Pubky Ring handoff
ovitrif Sep 2, 2026
d24cb40
chore: rename changelog fragment
ovitrif Sep 2, 2026
6ce8c65
fix: retain deep links through startup
ovitrif Sep 2, 2026
816ad38
fix: target Bitkit Pubky auth links uniquely
ovitrif Sep 2, 2026
b1bd510
fix: reject malformed Pubky handoff fragments
ovitrif Sep 2, 2026
d78e3d1
fix: preserve Pubky wrapper validation
ovitrif Sep 2, 2026
2687f89
fix: ungated Pubky links; reject dupes
ovitrif Sep 2, 2026
a801819
fix: classify setup routes before node
ovitrif Sep 3, 2026
4b07f97
fix: ungated BIP21 and BOLT11 links
ovitrif Sep 3, 2026
69025cd
fix: route gifts and identify auth relay
ovitrif Sep 5, 2026
02eb8d4
fix: retain scene-delivered deep links
ovitrif Sep 5, 2026
8925916
fix: forward app-delivered deep links
ovitrif Sep 5, 2026
4d0a0b6
chore: format deep-link files
ovitrif Sep 8, 2026
bcc0725
feat: stack marketplace auth on signup
ovitrif Sep 8, 2026
c2de3f0
docs: clarify pubky link contract
ovitrif Sep 8, 2026
cfd1cea
feat: upgrade paykit auth to rc50
ben-kaufman Aug 31, 2026
fdb11b3
chore: rename changelog fragment
ben-kaufman Aug 31, 2026
d3344e5
fix: reconcile paykit auth failures
ben-kaufman Aug 31, 2026
c7180ff
test: cover private endpoint reconciliation
ben-kaufman Sep 1, 2026
cc6f71c
test: use grant auth fixtures
ben-kaufman Sep 1, 2026
ee3df61
fix: approve external Pubky grants
ben-kaufman Sep 2, 2026
5987e14
fix: harden Pubky session recovery
ben-kaufman Sep 2, 2026
ab05014
test: cover external auth client ID
ben-kaufman Sep 2, 2026
c4f8fe2
fix: bound auth requester label
ben-kaufman Sep 3, 2026
7250d9d
fix: clear abandoned paykit sessions
ben-kaufman Sep 3, 2026
33e8df5
fix: harden Paykit session recovery
ben-kaufman Sep 4, 2026
9930806
fix: reset forgotten Paykit sessions
ben-kaufman Sep 4, 2026
5ad9fff
fix: preserve payment request history
ben-kaufman Sep 4, 2026
a63a036
fix: preserve private paykit receiver marker
ben-kaufman Sep 6, 2026
0293e47
fix: scope auth recovery to its completed session
ben-kaufman Sep 7, 2026
e05f181
refactor: remove paykit migration handling
ben-kaufman Sep 7, 2026
b5f5138
feat: support Pubky Ring signup
ben-kaufman Sep 2, 2026
01f819c
fix: complete Pubky Ring signup
ben-kaufman Sep 2, 2026
05aaecf
feat: support direct Pubky signup
ben-kaufman Sep 2, 2026
57f3c43
fix: complete Pubky signup handoff
ben-kaufman Sep 3, 2026
4f3960e
fix: recover failed pubky signup
ben-kaufman Sep 3, 2026
f972d7c
fix: recover and verify pubky signup state
ben-kaufman Sep 6, 2026
a2135a3
fix: require consent for every pubky signup
ben-kaufman Sep 7, 2026
1613c58
fix: preserve pubky homeserver on restore
ben-kaufman Sep 7, 2026
a3ebbc7
refactor: simplify pubky profile setup wiring
ben-kaufman Sep 8, 2026
8fce8ed
fix: prevent overlapping Pubky signup
ben-kaufman Sep 9, 2026
51100a3
fix: bound Pubky signup relay approval
ben-kaufman Sep 9, 2026
0ec6e49
merge: refresh Pubky signup stack
ovitrif Sep 9, 2026
30cc049
merge: refresh master
ovitrif Sep 9, 2026
0108e70
chore: log Pubky signup relay approval failures
ben-kaufman Sep 9, 2026
e509cb3
chore: skip cancelled Pubky relay warnings
ben-kaufman Sep 9, 2026
13dbd5d
fix: reject wrapped Pubky requests and redact signup errors
ben-kaufman Sep 10, 2026
e650086
fix: route wrapped pubky links
ovitrif Sep 10, 2026
16f3129
Merge master into codex/pubky-ring-signup
ben-kaufman Sep 10, 2026
38c0142
fix: reject decoded Pubky URL wrappers
ben-kaufman Sep 10, 2026
c285910
merge: resolve pubky auth conflicts
ovitrif Sep 10, 2026
644dd9f
test: isolate SamRock URL protocol
ovitrif Sep 10, 2026
96fd505
Merge branch 'master' into codex/pubky-ring-signup
ben-kaufman Sep 10, 2026
93dcfa4
merge: refresh Pubky signup stack
ovitrif Sep 10, 2026
902ff39
Merge pull request #722 from synonymdev/codex/715-pubkyauth-links
ovitrif Sep 11, 2026
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
31 changes: 30 additions & 1 deletion Bitkit/AppScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ struct AppScene: View {
PaykitFeatureFlags.enforceBuildAvailability()
ContactPaymentsService.enableAllPaymentOptions()

_app = StateObject(wrappedValue: AppViewModel(sheetViewModel: sheetViewModel, navigationViewModel: navigationViewModel))
_app = StateObject(wrappedValue: AppViewModel(
sheetViewModel: sheetViewModel,
navigationViewModel: navigationViewModel
))
_sheets = StateObject(wrappedValue: sheetViewModel)
_navigation = StateObject(wrappedValue: navigationViewModel)
let feeEstimatesManager = FeeEstimatesManager()
Expand Down Expand Up @@ -151,6 +154,7 @@ struct AppScene: View {
.onChange(of: wallet.nodeLifecycleState) { _, newValue in handleNodeLifecycleChange(newValue) }
.onChange(of: scenePhase, initial: true) { _, newValue in handleScenePhaseChange(newValue) }
.onChange(of: network.isConnected) { _, isConnected in handleNetworkChange(isConnected) }
.onOpenURL { url in app.retainDeepLink(url) }
// Bridge Trezor device state into the watch-only manager without coupling the two:
// TrezorManager bumps devicesRevision on any device/connection change.
.onChange(of: trezorManager.devicesRevision) { _, _ in pushHardwareDevices() }
Expand Down Expand Up @@ -309,6 +313,10 @@ struct AppScene: View {
isPinVerified = true
}

if let url = DeepLinkRouter.shared.consume() {
app.retainDeepLink(url)
}

// Listen for quick action notifications
NotificationCenter.default.addObserver(
forName: .quickActionSelected,
Expand All @@ -317,6 +325,13 @@ struct AppScene: View {
) { notification in
handleQuickAction(notification)
}
NotificationCenter.default.addObserver(
forName: .deepLinkReceived,
object: nil,
queue: .main
) { notification in
handleDeepLinkNotification(notification)
}
}
.onReceive(BackupService.shared.backupFailurePublisher) { intervalMinutes in
handleBackupFailure(intervalMinutes: intervalMinutes)
Expand All @@ -327,6 +342,16 @@ struct AppScene: View {
}
}

private func handleDeepLinkNotification(_ notification: Notification) {
if let retainedURL = DeepLinkRouter.shared.consume() {
app.retainDeepLink(retainedURL)
return
}
if let receivedURL = notification.object as? URL {
app.retainDeepLink(receivedURL)
}
}

private var mainContent: some View {
ZStack {
if Env.isTrezorEmulatorTesting {
Expand Down Expand Up @@ -941,6 +966,10 @@ struct AppScene: View {
wallet.resetSendState(speed: settings.defaultTransactionSpeed)
return
}
} catch ScanHandlingError.pubkyAuthRequest {
guard paykitPaymentRequestManager.isCurrentPresentation(request) else { return }
_ = paykitPaymentRequestManager.markPresentedIfPending(request)
continue
} catch is CancellationError {
if app.ownsContactPaymentContext(contactPaymentContext) {
app.resetSendState()
Expand Down
10 changes: 10 additions & 0 deletions Bitkit/BitkitApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import SwiftUI
/// Communication bridge between delegates and SwiftUI views
extension Notification.Name {
static let quickActionSelected = Notification.Name("quickActionSelected")
static let deepLinkReceived = Notification.Name("deepLinkReceived")
static let paykitSubscriptionPaymentDue = Notification.Name("paykitSubscriptionPaymentDue")
}

Expand Down Expand Up @@ -103,6 +104,15 @@ class AppDelegate: NSObject, UIApplicationDelegate {
return config
}

func application(
_ application: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
DeepLinkRouter.shared.forward(url)
return true
}

// MARK: - App Termination

func applicationWillTerminate(_ application: UIApplication) {
Expand Down
2 changes: 1 addition & 1 deletion Bitkit/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
<string>$(TREZOR_ELECTRUM_URL)</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>pubkyauth</string>
<string>pubkyring</string>
</array>
<key>NSAppTransportSecurity</key>
<dict>
Expand Down
193 changes: 130 additions & 63 deletions Bitkit/MainNavView.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
import SwiftUI

enum PendingProfileSetupResumeState {
case inactive
case waiting
case ready

func shouldResume(didResume: inout Bool) -> Bool {
if self == .inactive {
didResume = false
}
guard self == .ready, !didResume else { return false }
didResume = true
return true
}
}

func resolvePendingProfileSetupResumeState(
isProfileSetupPending: Bool,
isPaykitUIActive: Bool,
isAuthenticated: Bool,
hasActiveSheet: Bool,
isReplacingSheet: Bool,
currentRoute: Route?
) -> PendingProfileSetupResumeState {
guard isProfileSetupPending else { return .inactive }
guard isPaykitUIActive, isAuthenticated, !hasActiveSheet, !isReplacingSheet, currentRoute != .createProfile else {
return .waiting
}
return .ready
}

struct MainNavView: View {
private let canHandleDeepLinks: Bool

@AppStorage(PaykitFeatureFlags.uiEnabledKey) private var isPaykitUIEnabled = false

@EnvironmentObject private var app: AppViewModel
Expand All @@ -21,11 +53,26 @@ struct MainNavView: View {

@State private var showClipboardAlert = false
@State private var clipboardUri: String?
@State private var didResumePendingPubkyProfileSetup = false
init(canHandleDeepLinks: Bool = true) {
self.canHandleDeepLinks = canHandleDeepLinks
}

private var isPaykitUIActive: Bool {
PaykitFeatureFlags.isUIAvailable && isPaykitUIEnabled
}

private var pendingProfileSetupResumeState: PendingProfileSetupResumeState {
Comment thread
ovitrif marked this conversation as resolved.
resolvePendingProfileSetupResumeState(
isProfileSetupPending: pubkyProfile.isProfileSetupPending,
isPaykitUIActive: isPaykitUIActive,
isAuthenticated: pubkyProfile.isAuthenticated,
hasActiveSheet: sheets.activeSheetConfiguration != nil,
isReplacingSheet: sheets.isReplacingSheet,
currentRoute: navigation.currentRoute
)
}

// Delay constants for clipboard processing
private static let nodeReadyDelayNanoseconds: UInt64 = 500_000_000 // 0.5 seconds
private static let statePropagationDelayNanoseconds: UInt64 = 500_000_000 // 0.5 seconds
Expand All @@ -40,6 +87,10 @@ struct MainNavView: View {
navigation.navigate(.spendingHwSigned)
}
}
.onChange(of: pendingProfileSetupResumeState, initial: true) { _, resumeState in
guard resumeState.shouldResume(didResume: &didResumePendingPubkyProfileSetup) else { return }
navigation.navigate(.createProfile)
}
.sheet(
item: $sheets.addTagSheetItem,
onDismiss: {
Expand Down Expand Up @@ -327,69 +378,13 @@ struct MainNavView: View {
notificationManager.unregister()
}
}
.onOpenURL { url in
Task {
Logger.info("Received deeplink: \(sanitizedDeeplinkDescription(url))")

// Web URLs from widgets (e.g. news article tap) bypass payment handling
if let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" {
await UIApplication.shared.open(url)
return
}

if let callback = PubkyRingAuthCallback.parse(url: url) {
guard isPaykitUIActive else {
app.toast(
type: .error,
title: t("profile__auth_error_title"),
description: t("other__qr_error_text")
)
return
}

let handlingResult = await pubkyProfile.handleAuthCallback(callback)

switch handlingResult {
case let .trustedError(message):
app.toast(
type: .error,
title: t("profile__auth_error_title"),
description: message ?? t("other__qr_error_text")
)
case .untrustedError:
app.toast(
type: .error,
title: t("profile__auth_error_title")
)
case .handled, .ignored:
break
}

return
}

do {
try await app.handleScannedData(
url.absoluteString,
alternativeOnchainBalanceSats: hwWalletManager.maximumFundingBalanceSats
)
if shouldOpenPaymentSheet(for: url.absoluteString) {
PaymentNavigationHelper.openPaymentSheet(
app: app,
currency: currency,
settings: settings,
sheetViewModel: sheets
)
}
} catch {
Logger.error(error, context: "Failed to handle deeplink")
app.toast(
type: .error,
title: t("other__qr_error_header"),
description: t("other__qr_error_text")
)
}
}
.task(id: [canHandleDeepLinks, wallet.nodeLifecycleState == .running]) {
guard canHandleDeepLinks else { return }
await handlePendingDeepLink()
}
.onChange(of: app.pendingDeepLinkURL) { _, url in
guard canHandleDeepLinks, url != nil else { return }
Task { await handlePendingDeepLink() }
}
.alert(
t("other__clipboard_redirect_title"),
Expand Down Expand Up @@ -768,6 +763,78 @@ struct MainNavView: View {
!SamRockSetupRequest.isProtocolURL(uri) && !PubkyAuthRequest.isProtocolURL(uri)
}

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

private func handleDeepLink(_ url: URL) async {
Logger.info("Received deeplink: \(sanitizedDeeplinkDescription(url))")

// Web URLs from widgets (e.g. news article tap) bypass payment handling
if let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" {
await UIApplication.shared.open(url)
return
}

if let callback = PubkyRingAuthCallback.parse(url: url) {
guard isPaykitUIActive else {
app.toast(
type: .error,
title: t("profile__auth_error_title"),
description: t("other__qr_error_text")
)
return
}

let handlingResult = await pubkyProfile.handleAuthCallback(callback)

switch handlingResult {
case let .trustedError(message):
app.toast(
type: .error,
title: t("profile__auth_error_title"),
description: message ?? t("other__qr_error_text")
)
case .untrustedError:
app.toast(
type: .error,
title: t("profile__auth_error_title")
)
case .handled, .ignored:
break
}

return
}

do {
try await app.handleScannedData(
url.absoluteString,
alternativeOnchainBalanceSats: hwWalletManager.maximumFundingBalanceSats
)
if shouldOpenPaymentSheet(for: url.absoluteString) {
PaymentNavigationHelper.openPaymentSheet(
app: app,
currency: currency,
settings: settings,
sheetViewModel: sheets
)
}
} catch {
Logger.error(error, context: "Failed to handle deeplink")
app.toast(
type: .error,
title: t("other__qr_error_header"),
description: t("other__qr_error_text")
)
}
}

private func sanitizedDeeplinkDescription(_ url: URL) -> String {
if let description = SamRockSetupRequest.sanitizedDescription(url.absoluteString) {
return description
Expand Down
Loading
Loading