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
2 changes: 1 addition & 1 deletion Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1169,7 +1169,7 @@
repositoryURL = "https://github.com/pubky/paykit-rs";
requirement = {
kind = exactVersion;
version = "0.1.0-rc31";
version = "0.1.0-rc33";
};
};
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.

35 changes: 27 additions & 8 deletions Bitkit/AppScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ struct AppScene: View {
if authState == .authenticated, let pk = pubkyProfile.publicKey {
Task {
try? await contactsManager.loadContacts(for: pk)
await refreshPrivateOnlyPaykitReceiverMarker()
if !PaykitFeatureFlags.isUIEnabled, wallet.walletExists == true {
await retryPendingPaykitEndpointRemoval()
}
Expand Down Expand Up @@ -632,6 +633,7 @@ struct AppScene: View {
await retryPendingPaykitEndpointRemoval()
}
guard PaykitFeatureFlags.isUIEnabled else { return }
await refreshPrivateOnlyPaykitReceiverMarker()
await PrivatePaykitAddressReservationStore.shared.reconcileReservedIndexesWithLdk()
await PrivatePaykitService.shared.prepareSavedContacts(
contactsManager.contacts.map(\.publicKey),
Expand Down Expand Up @@ -671,8 +673,11 @@ struct AppScene: View {
await retryPendingPaykitEndpointRemoval()
await wallet.refreshPublicPaykitEndpointsOnForeground()
if PaykitFeatureFlags.isUIEnabled {
await refreshPrivateOnlyPaykitReceiverMarker()
let contactPublicKeys = contactsManager.contacts.map(\.publicKey)
await PrivatePaykitService.shared.refreshSavedContactEndpoints(
for: contactsManager.contacts.map(\.publicKey),
for: contactPublicKeys,
savedPublicKeys: contactPublicKeys,
wallet: wallet
)
}
Expand All @@ -681,17 +686,31 @@ struct AppScene: View {
}
}

private func refreshPrivateOnlyPaykitReceiverMarker() async {
let publicSharingEnabled = UserDefaults.standard.bool(forKey: PublicPaykitService.publishingEnabledKey)
let privateSharingEnabled = UserDefaults.standard.bool(forKey: PrivatePaykitService.publishingEnabledKey)
guard privateSharingEnabled, !publicSharingEnabled else { return }
guard await PubkyService.currentPublicKey() != nil else { return }

do {
try await PublicPaykitService.syncLocalReceiverMarker()
} catch {
Logger.warn("Failed to refresh private Paykit receiver marker: \(error)", context: "AppScene")
}
}

private func retryPendingPaykitEndpointRemoval() async {
if PublicPaykitService.isCleanupPending {
if UserDefaults.standard.bool(forKey: PublicPaykitService.publishingEnabledKey) {
PublicPaykitService.setCleanupPending(false)
} else {
do {
do {
if UserDefaults.standard.bool(forKey: PublicPaykitService.publishingEnabledKey) {
try await PublicPaykitService.syncCurrentPublishedEndpoints(wallet: wallet)
} else {
try await PublicPaykitService.removePublishedEndpoints()
PublicPaykitService.setCleanupPending(false)
} catch {
Logger.warn("Failed to retry public Paykit endpoint cleanup: \(error)", context: "AppScene")
try await PublicPaykitService.syncLocalReceiverMarker(publicSharingEnabled: false)
}
PublicPaykitService.setCleanupPending(false)
} catch {
Logger.warn("Failed to reconcile public Paykit state: \(error)", context: "AppScene")
}
}

Expand Down
3 changes: 3 additions & 0 deletions Bitkit/MainNavView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,9 @@ struct MainNavView: View {
contacts: contactsManager.contacts
) {
navigation.navigate(route)
if case let .contactDetail(publicKey) = route {
await contactsManager.refreshContactReceiverPaths(publicKey: publicKey, wallet: wallet)
}
clipboardUri = nil
return
}
Expand Down
57 changes: 51 additions & 6 deletions Bitkit/Managers/ContactsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -280,9 +280,8 @@ class ContactsManager: ObservableObject {
try await resolveContactProfile(publicKey: prefixedKey, includePlaceholder: true)
}

try await Task.detached {
_ = try await PubkyService.saveContact(publicKey: prefixedKey, label: profile.name)
}.value
let receiverPaths = try await Self.relevantReceiverPaths(for: prefixedKey)
_ = try await PubkyService.saveContact(publicKey: prefixedKey, label: profile.name, receiverPaths: receiverPaths)

Logger.info("Added contact \(PubkyPublicKeyFormat.redacted(prefixedKey))", context: "ContactsManager")

Expand All @@ -291,6 +290,29 @@ class ContactsManager: ObservableObject {
contacts.sort { $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending }
}

func refreshContactReceiverPaths(publicKey: String, wallet: WalletViewModel) async {
guard let prefixedKey = PubkyPublicKeyFormat.normalized(publicKey),
let contact = contacts.first(where: { PubkyPublicKeyFormat.matches($0.publicKey, prefixedKey) })
else { return }

do {
let receiverPaths = try await Self.relevantReceiverPaths(for: prefixedKey)
_ = try await PubkyService.saveContact(publicKey: prefixedKey, label: contact.profile.name, receiverPaths: receiverPaths)
await PrivatePaykitService.shared.refreshSavedContactEndpoints(
for: [prefixedKey],
savedPublicKeys: contacts.map(\.publicKey),
wallet: wallet
)
} catch is CancellationError {
return
} catch {
Logger.warn(
"Failed to refresh contact receiver paths for \(PubkyPublicKeyFormat.redacted(prefixedKey)): \(error)",
context: "ContactsManager"
)
}
}

// MARK: - Import Contacts

func importContacts(publicKeys: [String]) async throws {
Expand All @@ -302,8 +324,11 @@ class ContactsManager: ObservableObject {
group.addTask { [self] in
do {
let profile = try await resolveContactProfile(publicKey: key, includePlaceholder: true)
_ = try await PubkyService.saveContact(publicKey: key, label: profile.name)
let receiverPaths = try await Self.relevantReceiverPaths(for: key)
_ = try await PubkyService.saveContact(publicKey: key, label: profile.name, receiverPaths: receiverPaths)
return .success(PubkyContact(publicKey: key, profile: profile))
} catch is CancellationError {
return .failure(CancellationError())
} catch {
Logger.warn("Failed to save imported contact '\(PubkyPublicKeyFormat.redacted(key))': \(error)", context: "ContactsManager")
return .failure(error)
Expand All @@ -328,6 +353,8 @@ class ContactsManager: ObservableObject {
return (results, failures, firstError)
}

try Task.checkCancellation()

if !prefixedKeys.isEmpty, loadedResult.contacts.isEmpty {
throw loadedResult.firstError ?? PubkyServiceError.profileNotFound
}
Expand Down Expand Up @@ -379,12 +406,12 @@ class ContactsManager: ObservableObject {
try await Task.detached {
_ = try await PubkyService.removeContact(publicKey: prefixedKey)
}.value
await PrivatePaykitService.shared.removeSavedContact(publicKey: prefixedKey)
Self.removeContactProfileOverride(publicKey: prefixedKey)

Logger.info("Removed contact \(PubkyPublicKeyFormat.redacted(prefixedKey))", context: "ContactsManager")

contacts.removeAll { $0.publicKey == prefixedKey }
await PrivatePaykitService.shared.removeSavedContact(publicKey: prefixedKey)
}

func deleteAllContacts() async throws {
Expand Down Expand Up @@ -423,13 +450,17 @@ class ContactsManager: ObservableObject {

if let firstError {
if !deletedKeys.isEmpty {
contacts.removeAll { deletedKeys.contains($0.publicKey) }
await PrivatePaykitService.shared.removeSavedContacts(publicKeys: Array(deletedKeys))
for publicKey in deletedKeys {
Self.removeContactProfileOverride(publicKey: publicKey)
}
contacts.removeAll { deletedKeys.contains($0.publicKey) }
}
throw firstError
}

// All remote deletes succeeded, so clear any local-only contacts too.
await PrivatePaykitService.shared.removeSavedContacts(publicKeys: Array(deletedKeys))
Self.clearContactProfileOverrides()
await PrivatePaykitService.shared.pruneUnsavedContactState(savedPublicKeys: [])
contacts.removeAll()
Expand Down Expand Up @@ -593,6 +624,20 @@ class ContactsManager: ObservableObject {
throw PubkyServiceError.profileNotFound
}

private nonisolated static func relevantReceiverPaths(for publicKey: String) async throws -> [String] {
do {
return try await PubkyService.discoverRelevantReceiverPaths(publicKey: publicKey)
} catch is CancellationError {
throw CancellationError()
} catch {
Logger.warn(
"Failed to discover Paykit receivers for '\(PubkyPublicKeyFormat.redacted(publicKey))': \(error)",
context: "ContactsManager"
)
return [PaykitReceiverPath.wallet]
}
}

private nonisolated static let contactProfileOverridesKey = "pubkyContactProfileOverrides"

private nonisolated static func contact(
Expand Down
21 changes: 17 additions & 4 deletions Bitkit/Managers/PubkyProfileManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ class PubkyProfileManager: ObservableObject {
}

func deleteProfile() async throws {
await Self.removePrivatePaykitEndpointsBestEffort(context: "PubkyProfileManager.deleteProfile")
try await Self.removePrivatePaykitEndpoints(context: "PubkyProfileManager.deleteProfile")
Comment thread
jvsena42 marked this conversation as resolved.
do {
try await Task.detached {
try await PubkyService.deletePaykitProfile()
Expand Down Expand Up @@ -684,13 +684,26 @@ class PubkyProfileManager: ObservableObject {
}

static func removePublicPaykitEndpoints(context: String) async throws {
var firstError: Error?
do {
try await PublicPaykitService.removePublishedEndpoints()
} catch PubkyServiceError.sessionNotActive {
Logger.debug("Skipping public Paykit endpoint cleanup because no session is active", context: context)
} catch {
Logger.warn("Failed to remove public Paykit endpoints before clearing session: \(error)", context: context)
throw error
firstError = error
}

do {
try await PublicPaykitService.syncLocalReceiverMarker(publicSharingEnabled: false, privateSharingEnabled: false)
} catch PubkyServiceError.sessionNotActive {
Logger.debug("Skipping Paykit receiver marker cleanup because no session is active", context: context)
} catch {
firstError = firstError ?? error
}

if let firstError {
Logger.warn("Failed to remove public Paykit state before clearing session: \(firstError)", context: context)
throw firstError
}
}

Expand Down Expand Up @@ -730,7 +743,7 @@ class PubkyProfileManager: ObservableObject {
private func signOut(cleanPrivatePaykitEndpoints: Bool) async throws {
try await Task.detached {
if cleanPrivatePaykitEndpoints {
await Self.removePrivatePaykitEndpointsBestEffort(context: "PubkyProfileManager.signOut")
try await Self.removePrivatePaykitEndpoints(context: "PubkyProfileManager.signOut")
Comment thread
jvsena42 marked this conversation as resolved.
}
await Self.removePublicPaykitEndpointsBestEffort(context: "PubkyProfileManager.signOut")
do {
Expand Down
13 changes: 12 additions & 1 deletion Bitkit/Managers/ScannerManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class ScannerManager: ObservableObject {
private var navigation: NavigationViewModel?
private var pubkyProfile: PubkyProfileManager?
private var sheets: SheetViewModel?
private var wallet: WalletViewModel?

func configure(
app: AppViewModel,
Expand All @@ -26,7 +27,8 @@ class ScannerManager: ObservableObject {
settings: SettingsViewModel? = nil,
navigation: NavigationViewModel? = nil,
pubkyProfile: PubkyProfileManager? = nil,
sheets: SheetViewModel? = nil
sheets: SheetViewModel? = nil,
wallet: WalletViewModel? = nil
) {
self.app = app
self.contactsManager = contactsManager
Expand All @@ -35,6 +37,7 @@ class ScannerManager: ObservableObject {
self.navigation = navigation
self.pubkyProfile = pubkyProfile
self.sheets = sheets
self.wallet = wallet
}

func handleScan(_ uri: String, context: ScannerContext) async {
Expand Down Expand Up @@ -109,6 +112,14 @@ class ScannerManager: ObservableObject {
sheets?.hideSheetIfActive(sheetId, reason: reason)
}
navigation.navigate(route)
if case let .contactDetail(publicKey) = route,
let contactsManager,
let wallet
{
Task {
await contactsManager.refreshContactReceiverPaths(publicKey: publicKey, wallet: wallet)
}
}
return true
}

Expand Down
Loading
Loading