Skip to content
Open
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
2 changes: 1 addition & 1 deletion Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1182,7 +1182,7 @@
repositoryURL = "https://github.com/pubky/paykit-rs";
requirement = {
kind = exactVersion;
version = "0.1.0-rc46";
version = "0.1.0-rc51";
};
};
18D65DFE2EB9649F00252335 /* XCRemoteSwiftPackageReference "vss-rust-client-ffi" */ = {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions Bitkit/AppScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -933,19 +933,20 @@ struct AppScene: View {
private func retryPendingPaykitEndpointRemoval() async {
if PublicPaykitService.isCleanupPending {
do {
if UserDefaults.standard.bool(forKey: PublicPaykitService.publishingEnabledKey) {
switch PublicPaykitService.pendingReconciliationMode() {
case .publishEndpoints:
try await PublicPaykitService.syncCurrentPublishedEndpoints(wallet: wallet)
} else {
case .removePublishedState:
try await PublicPaykitService.removePublishedEndpoints()
try await PublicPaykitService.syncLocalReceiverMarker(publicSharingEnabled: false)
try await PublicPaykitService.syncLocalReceiverMarker()
}
PublicPaykitService.setCleanupPending(false)
} catch {
Logger.warn("Failed to reconcile public Paykit state: \(error)", context: "AppScene")
}
}

await PrivatePaykitService.shared.retryPendingEndpointRemoval(
await PrivatePaykitService.shared.retryPendingEndpointReconciliation(
wallet: wallet,
savedPublicKeys: contactsManager.contacts.map(\.publicKey)
)
Expand Down
174 changes: 137 additions & 37 deletions Bitkit/Managers/PubkyProfileManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -309,10 +309,9 @@ class PubkyProfileManager: ObservableObject {
profile = createdProfile
cacheProfileMetadata(createdProfile)
} catch {
try? Keychain.delete(key: .pubkySecretKey)
try? Keychain.delete(key: .paykitSession)
await PubkyService.forceSignOut()
throw error
let profileCreationError = error
await discardAbandonedSession()
throw profileCreationError
}

Logger.info("Pubky identity created for \(publicKeyZ32)", context: "PubkyProfileManager")
Expand Down Expand Up @@ -365,6 +364,7 @@ class PubkyProfileManager: ObservableObject {
Logger.info("Bitkit profile storage already missing, continuing sign out", context: "PubkyProfileManager")
}

Self.clearPaykitSharingAfterProfileDeletion()
try await signOut(cleanPrivatePaykitEndpoints: false)
}

Expand Down Expand Up @@ -505,26 +505,29 @@ class PubkyProfileManager: ObservableObject {
@discardableResult
func completeAuthentication() async throws -> String {
try await completeAuthentication(
completeAuth: { _ = try await PubkyService.completeAuth() },
completeAuth: { try await PubkyService.completeAuth() },
currentPublicKey: { await PubkyService.currentPublicKey() },
clearSessionAccess: { await PubkyService.clearSessionAccess() }
discardSessionAccess: { sessionSecret in
await Task.detached {
await PaykitSdkService.shared.discardCompletedAuthSession(sessionSecret: sessionSecret)
}.value
}
)
}

@discardableResult
private func completeAuthentication(
completeAuth: @escaping () async throws -> Void,
completeAuth: @escaping () async throws -> String,
currentPublicKey: @escaping () async -> String?,
clearSessionAccess: @escaping () async -> Void
discardSessionAccess: @escaping (String) async -> Void
) async throws -> String {
guard let attemptID = activeAuthAttemptID else {
throw CancellationError()
}
var didCompleteAuth = false
var completedSessionSecret: String?

do {
try await completeAuth()
didCompleteAuth = true
completedSessionSecret = try await completeAuth()
try Task.checkCancellation()
guard activeAuthAttemptID == attemptID else {
throw CancellationError()
Expand All @@ -548,14 +551,20 @@ class PubkyProfileManager: ObservableObject {
await loadProfile()
return pk
} catch is CancellationError {
await clearCompletedAuthSessionIfNeeded(didCompleteAuth, clearSessionAccess: clearSessionAccess)
await discardCompletedAuthSessionIfNeeded(
Comment thread
ben-kaufman marked this conversation as resolved.
completedSessionSecret,
discardSessionAccess: discardSessionAccess
)
if activeAuthAttemptID == attemptID {
activeAuthAttemptID = nil
restoreAuthStateAfterAuthFlow()
}
throw CancellationError()
} catch let serviceError as PubkyServiceError {
await clearCompletedAuthSessionIfNeeded(didCompleteAuth, clearSessionAccess: clearSessionAccess)
await discardCompletedAuthSessionIfNeeded(
completedSessionSecret,
discardSessionAccess: discardSessionAccess
)
guard activeAuthAttemptID == attemptID else {
throw CancellationError()
}
Expand All @@ -564,7 +573,10 @@ class PubkyProfileManager: ObservableObject {
restoreAuthStateAfterAuthFlow()
throw serviceError
} catch {
await clearCompletedAuthSessionIfNeeded(didCompleteAuth, clearSessionAccess: clearSessionAccess)
await discardCompletedAuthSessionIfNeeded(
completedSessionSecret,
discardSessionAccess: discardSessionAccess
)
guard activeAuthAttemptID == attemptID else {
throw CancellationError()
}
Expand All @@ -575,9 +587,43 @@ class PubkyProfileManager: ObservableObject {
}
}

private func clearCompletedAuthSessionIfNeeded(_ didCompleteAuth: Bool, clearSessionAccess: @escaping () async -> Void) async {
guard didCompleteAuth else { return }
await clearSessionAccess()
private func discardCompletedAuthSessionIfNeeded(
_ completedSessionSecret: String?,
discardSessionAccess: @escaping (String) async -> Void
) async {
guard let completedSessionSecret else { return }
await discardSessionAccess(completedSessionSecret)
}

private func discardAbandonedSession() async {
await discardAbandonedSession(
revokeSessionAccess: {
try await Task.detached {
try await PubkyService.signOut()
}.value
},
forgetSessionAccess: {
try await Task.detached {
try await PubkyService.forgetSessionAccess()
}.value
}
)
}

private func discardAbandonedSession(
revokeSessionAccess: @escaping () async throws -> Void,
forgetSessionAccess: @escaping () async throws -> Void
) async {
do {
try await revokeSessionAccess()
} catch {
Logger.warn("Failed to revoke abandoned Pubky session: \(error)", context: "PubkyProfileManager")
do {
try await forgetSessionAccess()
} catch {
Logger.warn("Failed to forget abandoned Pubky session access: \(error)", context: "PubkyProfileManager")
}
}
}

func finalizeAuthentication() {
Expand Down Expand Up @@ -612,14 +658,24 @@ class PubkyProfileManager: ObservableObject {

@discardableResult
func completeAuthenticationForTesting(
completeAuth: @escaping () async throws -> Void,
completeAuth: @escaping () async throws -> String,
currentPublicKey: @escaping () async -> String?,
clearSessionAccess: @escaping () async -> Void
discardSessionAccess: @escaping (String) async -> Void
) async throws -> String {
try await completeAuthentication(
completeAuth: completeAuth,
currentPublicKey: currentPublicKey,
clearSessionAccess: clearSessionAccess
discardSessionAccess: discardSessionAccess
)
}

func discardAbandonedSessionForTesting(
revokeSessionAccess: @escaping () async throws -> Void,
forgetSessionAccess: @escaping () async throws -> Void
) async {
await discardAbandonedSession(
revokeSessionAccess: revokeSessionAccess,
forgetSessionAccess: forgetSessionAccess
)
}
#endif
Expand Down Expand Up @@ -666,11 +722,17 @@ class PubkyProfileManager: ObservableObject {
// MARK: - Sign Out

static func clearLocalState() async {
do {
try await PubkyService.forgetSessionAccess()
} catch {
Logger.warn("Failed to forget local Pubky session access: \(error)", context: "PubkyProfileManager")
}
await clearLocalAppState()
}

private static func clearLocalAppState() async {
await PrivatePaykitService.shared.closeAndClear()
await PrivatePaykitAddressReservationStore.shared.clearContactAssignments()
await PubkyService.forceSignOut()
try? Keychain.delete(key: .paykitSession)
try? Keychain.delete(key: .pubkySecretKey)
await PubkyImageCache.shared.clear()
UserDefaults.standard.removeObject(forKey: cachedNameKey)
UserDefaults.standard.removeObject(forKey: cachedImageUriKey)
Expand Down Expand Up @@ -747,22 +809,56 @@ class PubkyProfileManager: ObservableObject {
}

private func signOut(cleanPrivatePaykitEndpoints: Bool) async throws {
try await Task.detached {
if cleanPrivatePaykitEndpoints {
try await Self.removePrivatePaykitEndpoints(context: "PubkyProfileManager.signOut")
}
await Self.removePublicPaykitEndpointsBestEffort(context: "PubkyProfileManager.signOut")
do {
let publicSharingEnabled = UserDefaults.standard.bool(forKey: PublicPaykitService.publishingEnabledKey)
let privateSharingEnabled = UserDefaults.standard.bool(forKey: PrivatePaykitService.publishingEnabledKey)

do {
try await Task.detached {
if cleanPrivatePaykitEndpoints {
try await Self.removePrivatePaykitEndpoints(context: "PubkyProfileManager.signOut")
}
await Self.removePublicPaykitEndpointsBestEffort(context: "PubkyProfileManager.signOut")
try await PubkyService.signOut()
} catch {
Logger.warn("Server sign out failed, forcing local sign out: \(error)", context: "PubkyProfileManager")
}
await Self.clearLocalState()
}.value
await Self.clearLocalAppState()
}.value
} catch {
Self.markPaykitReconciliationPendingAfterFailedSignOut(
Comment thread
ben-kaufman marked this conversation as resolved.
publicSharingEnabled: publicSharingEnabled,
privateSharingEnabled: privateSharingEnabled
)
throw error
}

clearAuthenticatedState()
}

static func markPaykitReconciliationPendingAfterFailedSignOut(
publicSharingEnabled: Bool,
privateSharingEnabled: Bool,
setPublicReconciliationPending: (Bool) -> Void = PublicPaykitService.setCleanupPending,
setPrivateReconciliationPending: (Bool) -> Void = PrivatePaykitService.setContactSharingCleanupPending
) {
if publicSharingEnabled || privateSharingEnabled {
setPublicReconciliationPending(true)
}
if privateSharingEnabled {
setPrivateReconciliationPending(true)
}
}

static func clearPaykitSharingAfterProfileDeletion(
defaults: UserDefaults = .standard,
setPublicReconciliationPending: (Bool) -> Void = PublicPaykitService.setCleanupPending
) {
let hadPublishedState = defaults.bool(forKey: PublicPaykitService.publishingEnabledKey) ||
defaults.bool(forKey: PrivatePaykitService.publishingEnabledKey)
defaults.set(false, forKey: PublicPaykitService.publishingEnabledKey)
defaults.set(false, forKey: PrivatePaykitService.publishingEnabledKey)
if hadPublishedState {
setPublicReconciliationPending(true)
}
}

func refreshSessionIfPossible(after error: Error) async -> Bool {
await Self.refreshSessionIfPossible(
after: error,
Expand Down Expand Up @@ -871,8 +967,8 @@ class PubkyProfileManager: ObservableObject {
deleteKeychainValue: (KeychainEntryType) throws -> Void = {
try Keychain.delete(key: $0)
},
clearSessionAccess: @escaping () async -> Void = {
await PubkyService.clearSessionAccess()
forgetSessionAccess: @escaping () async throws -> Void = {
try await PubkyService.forgetSessionAccess()
},
signInWithSecretKey: @escaping (String) async throws -> String = {
try await PubkyService.signIn(secretKeyHex: $0)
Expand All @@ -881,7 +977,11 @@ class PubkyProfileManager: ObservableObject {
try await PubkyService.importExternalSession(secret: $0)
}
) async throws {
await clearSessionAccess()
do {
try await forgetSessionAccess()
} catch {
Logger.warn("Failed to forget existing Pubky session before restore: \(error)", context: "PubkyProfileManager")
}

switch backup?.kind {
case .none:
Expand Down
2 changes: 2 additions & 0 deletions Bitkit/Models/PubkyAuthRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ struct PubkyAuthPermission {
struct PubkyAuthRequest {
let rawUrl: String
let kind: Paykit.PubkyAuthRequestKind
let clientID: String
let relay: String
let capabilities: String
let permissions: [PubkyAuthPermission]
Expand All @@ -75,6 +76,7 @@ struct PubkyAuthRequest {
return PubkyAuthRequest(
rawUrl: url,
kind: details.kind,
clientID: details.clientId,
relay: details.relayUrl ?? "",
capabilities: capabilities,
permissions: permissions,
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,7 @@
"pubky_auth__title" = "Authorize";
"pubky_auth__description_prefix" = "A service is requesting permission to access and edit your ";
"pubky_auth__description_suffix" = " data.";
"pubky_auth__requester" = "Requester ID: {clientId}";
"pubky_auth__requested_permissions" = "REQUESTED PERMISSIONS";
"pubky_auth__watch_only_account_default_name" = "{service} account";
"pubky_auth__watch_only_account_fallback_name" = "Paykit server account";
Expand Down
30 changes: 29 additions & 1 deletion Bitkit/Services/PrivatePaykitService+Contacts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ import Paykit
// MARK: - Saved Contacts

extension PrivatePaykitService {
enum FullCleanupReconciliationMode: Equatable {
case restoreSavedContacts
case removePublishedState
}

static func fullCleanupReconciliationMode(defaults: UserDefaults = .standard) -> FullCleanupReconciliationMode {
return defaults.bool(forKey: publishingEnabledKey) ? .restoreSavedContacts : .removePublishedState
}

@discardableResult
func prepareSavedContacts(
_ publicKeys: [String],
Expand Down Expand Up @@ -255,9 +264,28 @@ extension PrivatePaykitService {
}
}

func retryPendingEndpointRemoval(wallet _: WalletViewModel, savedPublicKeys publicKeys: [String]) async {
func retryPendingEndpointReconciliation(wallet: WalletViewModel, savedPublicKeys publicKeys: [String]) async {
let savedKeys = Set(normalizedSavedContactKeys(publicKeys))
let isFullCleanupPending = UserDefaults.standard.bool(forKey: Self.cleanupPendingKey)
if isFullCleanupPending,
Self.fullCleanupReconciliationMode() == .restoreSavedContacts
{
let restoreKeys = savedKeys.union(knownSavedContactKeys)
guard !restoreKeys.isEmpty else { return }

let error = await prepareSavedContacts(
Comment thread
ben-kaufman marked this conversation as resolved.
Array(restoreKeys),
wallet: wallet,
requireImmediatePublication: true
)
if let error {
Logger.warn("Failed to reconcile private Paykit endpoints: \(error)", context: "PrivatePaykit")
} else {
Self.setContactSharingCleanupPending(false)
}
return
}

let cleanupKeys = isFullCleanupPending
? Set(knownSavedContactKeys).union(state.contacts.keys).union(Self.pendingDeletedContactCleanupKeys())
: Set(pendingPrivateEndpointRemovalKeys(savedPublicKeys: publicKeys))
Expand Down
Loading
Loading