Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5057d6e
feat: reuse pubky ring identities
Jasonvdb Jul 24, 2026
835c786
chore: rename changelog fragment
Jasonvdb Jul 24, 2026
26f99f3
fix: preserve bare pubkys beginning with prefix
Jasonvdb Jul 24, 2026
6494510
fix: align Pubky import overview with designs
Jasonvdb Jul 24, 2026
677938a
fix: treat a shared Pubky source as a stored identity
Jasonvdb Sep 17, 2026
ad82277
fix: serialize Pubky signup with identity lifecycle changes
Jasonvdb Sep 17, 2026
4a1e141
fix: keep stripping the pubky prefix from non-canonical display keys
Jasonvdb Sep 17, 2026
3db6cf4
fix: refuse pubky signup over an unrecoverable session
Jasonvdb Sep 17, 2026
fbbf510
fix: never publish a receiver marker for a borrowed pubky
Jasonvdb Sep 17, 2026
d59bb19
fix: erase every Bitkit-owned shared mirror before deleting private s…
Jasonvdb Sep 17, 2026
1dbe1e0
fix: revalidate a borrowed pubky source before deleting the profile
Jasonvdb Sep 17, 2026
efd3661
fix: revalidate a borrowed pubky source before erasing contacts
Jasonvdb Sep 17, 2026
a4e4d83
refactor: remove obsolete ring requester flow
Jasonvdb Sep 18, 2026
acd5269
chore: merge master into shared pubky ring
Jasonvdb Sep 18, 2026
ab2d476
fix: harden borrowed pubky access and recovery
Jasonvdb Sep 21, 2026
5cdcb0c
chore: merge master into shared pubky ring
Jasonvdb Sep 21, 2026
930ed30
fix: revalidate borrowed pubky writes
Jasonvdb Sep 21, 2026
f738e63
fix: exclude borrowed paykit backup state
Jasonvdb Sep 21, 2026
9b7cde6
fix: wait for pubky discovery before create
Jasonvdb Sep 21, 2026
950899d
fix: gate foreground paykit maintenance
Jasonvdb Sep 21, 2026
d7e860e
fix: clear borrowed paykit state on disconnect
Jasonvdb Sep 21, 2026
ac9207b
fix: remove paykit endpoints before shared teardown
Jasonvdb Sep 21, 2026
f0266f1
fix: replay gated paykit channel refresh
Jasonvdb Sep 21, 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
124 changes: 96 additions & 28 deletions Bitkit/AppScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,29 @@ struct PaykitPaymentRequestPollingSchedule {
}
}

enum OrphanedKeychainCleanup {
static func perform(
hasNativeKeychain: Bool,
hasOrphanedRNKeychain: Bool,
deleteBitkitSharedIdentities: () throws -> Void,
wipePrivateKeychain: () throws -> Void,
cleanupRNKeychain: () -> Void
) throws {
guard hasNativeKeychain || hasOrphanedRNKeychain else {
return
}

// A reinstall must never orphan a shared Bitkit credential. Ring-owned
// accounts are outside this deletion's scope.
try deleteBitkitSharedIdentities()
try wipePrivateKeychain()

if hasOrphanedRNKeychain {
cleanupRNKeychain()
}
}
}

struct AppScene: View {
private static let initialPaykitSyncRetryDelays = Array(repeating: Duration.seconds(2), count: 14)

Expand Down Expand Up @@ -386,7 +409,13 @@ struct AppScene: View {

private var appEventContent: some View {
configuredContent
.onChange(of: pubkyProfile.authState, initial: true) { _, authState in
.onChange(of: pubkyProfile.authState, initial: true) { previousAuthState, authState in
if let isAllowed = Self.paykitMaintenancePermission(
previousAuthState: previousAuthState,
authState: authState
) {
wallet.setPaykitMaintenanceAllowed(isAllowed)
}
if authState == .authenticated, let pk = pubkyProfile.publicKey {
paykitPaymentRequestManager.activate(identity: pk)
Task {
Expand Down Expand Up @@ -757,7 +786,7 @@ struct AppScene: View {
/// Handle orphaned keychain entries from previous app installs.
/// If the installation marker doesn't exist but keychain has data, the app was reinstalled
/// and the keychain data is orphaned (corresponding wallet data was deleted with the app).
private func handleOrphanedKeychain() {
private func handleOrphanedKeychain() throws {
// If marker exists, app was installed before - keychain is valid
if InstallationMarker.exists() {
Logger.debug("Installation marker exists, skipping orphaned keychain check", context: "AppScene")
Expand All @@ -772,26 +801,30 @@ struct AppScene: View {

if hasNativeKeychain || hasOrphanedRNKeychain {
Logger.warn("Orphaned keychain detected, wiping", context: "AppScene")
try? Keychain.wipeEntireKeychain()

if hasOrphanedRNKeychain {
MigrationsService.shared.cleanupRNKeychain()
}
try OrphanedKeychainCleanup.perform(
hasNativeKeychain: hasNativeKeychain,
hasOrphanedRNKeychain: hasOrphanedRNKeychain,
deleteBitkitSharedIdentities: {
try SharedPubkyIdentityVault.deleteAllBitkitIdentities()
},
wipePrivateKeychain: {
try Keychain.wipeEntireKeychain()
},
cleanupRNKeychain: {
MigrationsService.shared.cleanupRNKeychain()
}
)
}

// Create marker for this installation
do {
try InstallationMarker.create()
} catch {
Logger.error("Failed to create installation marker: \(error)", context: "AppScene")
}
try InstallationMarker.create()
}

@Sendable
private func setupTask() async {
do {
// Handle orphaned keychain before anything else
handleOrphanedKeychain()
try handleOrphanedKeychain()

await checkAndPerformRNMigration()
try wallet.setWalletExistsState()
Expand Down Expand Up @@ -941,33 +974,68 @@ struct AppScene: View {
}

if newPhase == .active {
wallet.setPaykitMaintenanceAllowed(false)
let sharedIdentityValidation = Task {
let canUsePaykit = await pubkyProfile.validateSharedIdentitySourceIfNeeded()
wallet.setPaykitMaintenanceAllowed(canUsePaykit)
return canUsePaykit
}
// Reconnect a known hardware device so its connection indicator turns green again;
if isPinVerified || !settings.pinEnabled {
Task { await trezorManager.autoReconnect() }
}
if wallet.walletExists == true {
Task {
await clearDeliveredNotifications()
await LightningService.shared.reconnectPeers()
try? await wallet.sync()
await retryPendingPaykitEndpointRemoval()
await wallet.refreshPublicPaykitEndpointsOnForeground()
if PaykitFeatureFlags.isUIEnabled {
await refreshPrivateOnlyPaykitReceiverMarker()
let contactPublicKeys = contactsManager.contacts.map(\.publicKey)
await PrivatePaykitService.shared.startInitialLinkBurst(
for: contactPublicKeys,
savedPublicKeys: contactPublicKeys,
wallet: wallet,
reason: "foreground"
)
await refreshIncomingPaykitPaymentRequests()
}
await Self.performForegroundMaintenance(
waitForSharedIdentityValidation: { await sharedIdentityValidation.value },
walletMaintenance: { canUsePaykit in
await LightningService.shared.reconnectPeers()
try? await wallet.sync(allowPaykitMaintenance: canUsePaykit)
},
paykitMaintenance: {
await retryPendingPaykitEndpointRemoval()
await wallet.refreshPublicPaykitEndpointsOnForeground()
if PaykitFeatureFlags.isUIEnabled {
await refreshPrivateOnlyPaykitReceiverMarker()
let contactPublicKeys = contactsManager.contacts.map(\.publicKey)
await PrivatePaykitService.shared.startInitialLinkBurst(
for: contactPublicKeys,
savedPublicKeys: contactPublicKeys,
wallet: wallet,
reason: "foreground"
)
await refreshIncomingPaykitPaymentRequests()
}
}
)
}
}
}
}

static func paykitMaintenancePermission(
previousAuthState: PubkyAuthState,
authState: PubkyAuthState
) -> Bool? {
if previousAuthState != .authenticated, authState == .authenticated {
return true
}
return authState == .idle ? false : nil
}

@MainActor
static func performForegroundMaintenance(
waitForSharedIdentityValidation: () async -> Bool,
walletMaintenance: (Bool) async -> Void,
paykitMaintenance: () async -> Void
) async {
let canUsePaykit = await waitForSharedIdentityValidation()
await walletMaintenance(canUsePaykit)
guard canUsePaykit else { return }
await paykitMaintenance()
}

private func refreshPrivateOnlyPaykitReceiverMarker() async {
let publicSharingEnabled = UserDefaults.standard.bool(forKey: PublicPaykitService.publishingEnabledKey)
let privateSharingEnabled = UserDefaults.standard.bool(forKey: PrivatePaykitService.publishingEnabledKey)
Expand Down
16 changes: 16 additions & 0 deletions Bitkit/Assets.xcassets/icons/key.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "key.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
5 changes: 5 additions & 0 deletions Bitkit/Assets.xcassets/icons/key.imageset/key.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions Bitkit/Bitkit.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<string>$(AppIdentifierPrefix)to.bitkit.signet</string>
<string>$(AppIdentifierPrefix)to.bitkit.testnet</string>
<string>$(AppIdentifierPrefix)to.bitkit.regtest</string>
<string>$(AppIdentifierPrefix)pubky.shared</string>
</array>
</dict>
</plist>
14 changes: 12 additions & 2 deletions Bitkit/Components/Button/Button.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ struct CustomButton: View {
let isDisabled: Bool
let isLoading: Bool
let shouldExpand: Bool
let labelKerning: CGFloat
let background: AnyView?
let action: (() async -> Void)?
let destination: AnyView?
Expand All @@ -85,6 +86,7 @@ struct CustomButton: View {
isDisabled: Bool = false,
isLoading: Bool = false,
shouldExpand: Bool = false,
labelKerning: CGFloat = 0.4,
background: (any View)? = nil
) {
self.title = title
Expand All @@ -94,6 +96,7 @@ struct CustomButton: View {
self.isDisabled = isDisabled
self.isLoading = isLoading
self.shouldExpand = shouldExpand
self.labelKerning = labelKerning
self.background = background.map { AnyView($0) }
action = nil
destination = nil
Expand All @@ -108,6 +111,7 @@ struct CustomButton: View {
isDisabled: Bool = false,
isLoading: Bool = false,
shouldExpand: Bool = false,
labelKerning: CGFloat = 0.4,
background: (any View)? = nil,
action: @escaping () async -> Void
) {
Expand All @@ -118,6 +122,7 @@ struct CustomButton: View {
self.isDisabled = isDisabled
self.isLoading = isLoading
self.shouldExpand = shouldExpand
self.labelKerning = labelKerning
self.background = background.map { AnyView($0) }
self.action = action
destination = nil
Expand All @@ -132,6 +137,7 @@ struct CustomButton: View {
isDisabled: Bool = false,
isLoading: Bool = false,
shouldExpand: Bool = false,
labelKerning: CGFloat = 0.4,
background: (any View)? = nil,
destination: some View
) {
Expand All @@ -142,6 +148,7 @@ struct CustomButton: View {
self.isDisabled = isDisabled
self.isLoading = isLoading
self.shouldExpand = shouldExpand
self.labelKerning = labelKerning
self.background = background.map { AnyView($0) }
action = nil
self.destination = AnyView(destination)
Expand All @@ -158,6 +165,7 @@ struct CustomButton: View {
isLoading: isLoading,
isPressed: isPressed,
shouldExpand: shouldExpand,
labelKerning: labelKerning,
background: background
))
case .secondary:
Expand All @@ -168,13 +176,15 @@ struct CustomButton: View {
isDisabled: effectiveIsDisabled,
isPressed: isPressed,
isLoading: isLoading,
shouldExpand: shouldExpand
shouldExpand: shouldExpand,
labelKerning: labelKerning
))
case .tertiary:
AnyView(TertiaryButtonView(
title: title,
icon: icon,
isPressed: isPressed
isPressed: isPressed,
labelKerning: labelKerning
))
}
}
Expand Down
3 changes: 2 additions & 1 deletion Bitkit/Components/Button/PrimaryButtonView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ struct PrimaryButtonView: View {
let isLoading: Bool
let isPressed: Bool
let shouldExpand: Bool
let labelKerning: CGFloat
let background: AnyView?

var body: some View {
Expand All @@ -24,7 +25,7 @@ struct PrimaryButtonView: View {
if size == .small {
CaptionBText(title, textColor: .textPrimary)
} else {
BodySSBText(title, textColor: .textPrimary)
BodySSBText(title, textColor: .textPrimary, kerning: labelKerning)
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion Bitkit/Components/Button/SecondaryButtonView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ struct SecondaryButtonView: View {
let isPressed: Bool
var isLoading: Bool = false
let shouldExpand: Bool
let labelKerning: CGFloat

var body: some View {
HStack(spacing: 8) {
Expand All @@ -22,7 +23,7 @@ struct SecondaryButtonView: View {
} else if size == .small {
CaptionBText(title, textColor: textColor)
} else {
BodySSBText(title, textColor: textColor)
BodySSBText(title, textColor: textColor, kerning: labelKerning)
}
}
.frame(maxWidth: (size == .large || shouldExpand) ? .infinity : nil)
Expand Down
3 changes: 2 additions & 1 deletion Bitkit/Components/Button/TertiaryButtonView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ struct TertiaryButtonView: View {
let title: String
let icon: AnyView?
let isPressed: Bool
let labelKerning: CGFloat

var body: some View {
HStack(spacing: 8) {
if let icon {
icon
}

BodySSBText(title, textColor: textColor)
BodySSBText(title, textColor: textColor, kerning: labelKerning)
}
.frame(maxWidth: .infinity)
.frame(height: CustomButton.Size.large.height)
Expand Down
2 changes: 2 additions & 0 deletions Bitkit/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SharedPubkyKeychainAccessGroup</key>
<string>$(AppIdentifierPrefix)pubky.shared</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
Expand Down
18 changes: 1 addition & 17 deletions Bitkit/MainNavView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -791,23 +791,7 @@ struct MainNavView: View {
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
}
pubkyProfile.handleAuthCallback(callback)

return
}
Expand Down
Loading
Loading