Repository navigation
feat: share paykit state across apps #856
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6d68812
7ae5bee
10de74f
9190f18
8d81890
6ec60b7
0982385
c112976
0231e81
b34d564
bdd9344
cf55252
17e73a0
38dee7f
99e84eb
1ac9f07
61203c8
05c3234
ef1f2ab
225ec3e
ac90e65
48b6b8f
2f9afe0
f2d69b3
0e7da00
c5ae314
f8f45ca
d78aaa3
dc785b4
89c4fd8
27e921a
9a52ba0
778d9f2
f5ca801
21a7a60
4dd4d60
157e04c
c334a6f
10b85a1
39b2d62
134dce1
7bdfe8d
97fbbf5
8b51eb4
ee467cf
f2349c9
ec2071f
89b1c24
e2b7326
f23151b
407c282
897740b
cd921ac
47e2745
7953166
e3cae1a
2de4fd5
486d795
976538f
f361e34
060d7a0
ec786b1
9cb87b8
1d49735
00e8d6b
39a97f4
6c86c83
591ac12
90799f2
f09f1c5
32fcc1d
f79cfcb
f553d46
a223fad
a684677
cb23093
a04232f
ec6d7f6
fea84da
2a70766
31c74d8
76f8a47
5dd730b
7e6bc13
05ec22d
d99d5a6
291328d
b633684
480b8d2
3420760
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -150,11 +150,12 @@ | |
| private let fetchRemoteProfile: @Sendable (_ publicKey: String, _ priority: PaykitPublicReadPriority) async throws -> PubkyProfile? | ||
| private let saveContactLabel: @Sendable (_ publicKey: String, _ label: String, _ expectedIdentity: String) async throws -> Void | ||
| private let removeContactRecord: @Sendable (_ publicKey: String) async throws -> Void | ||
| private let removeContactRecords: @Sendable (_ publicKeys: [String]) async throws -> [ContactRecord] | ||
| private let forgetRemovedContacts: @Sendable (_ publicKeys: [String]) async -> Void | ||
| private let currentDate: @Sendable () -> Date | ||
|
|
||
| init( | ||
| contactRecords: @escaping @Sendable () async throws -> [ContactRecord] = PubkyService.contactRecords, | ||
|
Check warning on line 158 in Bitkit/Managers/ContactsManager.swift
|
||
| fetchFollows: @escaping @Sendable (String) async throws -> [String] = { try await PubkyService.getContacts(publicKey: $0) }, | ||
| fetchRemoteProfile: @escaping @Sendable (_ publicKey: String, _ priority: PaykitPublicReadPriority) async throws -> PubkyProfile? = { | ||
| try await ContactsManager.remoteContactProfile(publicKey: $0, priority: $1) | ||
|
|
@@ -165,6 +166,9 @@ | |
| removeContactRecord: @escaping @Sendable (_ publicKey: String) async throws -> Void = { | ||
| _ = try await PubkyService.removeContact(publicKey: $0) | ||
| }, | ||
| removeContactRecords: @escaping @Sendable (_ publicKeys: [String]) async throws -> [ContactRecord] = { | ||
| try await PubkyService.removeContacts(publicKeys: $0) | ||
| }, | ||
| forgetRemovedContacts: @escaping @Sendable (_ publicKeys: [String]) async -> Void = { | ||
| await PrivatePaykitService.shared.removeSavedContacts(publicKeys: $0) | ||
| }, | ||
|
|
@@ -175,6 +179,7 @@ | |
| self.fetchRemoteProfile = fetchRemoteProfile | ||
| self.saveContactLabel = saveContactLabel | ||
| self.removeContactRecord = removeContactRecord | ||
| self.removeContactRecords = removeContactRecords | ||
| self.forgetRemovedContacts = forgetRemovedContacts | ||
| self.currentDate = currentDate | ||
| } | ||
|
|
@@ -598,11 +603,9 @@ | |
| try await resolveContactProfile(publicKey: prefixedKey, includePlaceholder: true, retryTransient: true) | ||
| } | ||
|
|
||
| let receiverPaths = try await Self.relevantReceiverPaths(for: prefixedKey) | ||
| _ = try await PubkyService.saveContact( | ||
| publicKey: prefixedKey, | ||
| label: profile.name, | ||
| receiverPaths: receiverPaths, | ||
| restorePrivateConnection: true | ||
| ) | ||
|
|
||
|
|
@@ -614,25 +617,23 @@ | |
| contacts.sort(by: Self.isOrderedByName) | ||
| } | ||
|
|
||
| func refreshContactReceiverPaths(publicKey: String, wallet: WalletViewModel) async { | ||
| func refreshContactLink(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.startInitialLinkBurst( | ||
| _ = try await PubkyService.saveContact(publicKey: prefixedKey, label: contact.profile.name) | ||
| await PrivatePaykitService.shared.refreshSavedContactEndpoints( | ||
| for: [prefixedKey], | ||
| savedPublicKeys: contacts.map(\.publicKey), | ||
| wallet: wallet, | ||
| reason: "contact receiver refresh" | ||
| wallet: wallet | ||
| ) | ||
| } catch is CancellationError { | ||
| return | ||
| } catch { | ||
| Logger.warn( | ||
| "Failed to refresh contact receiver paths for \(PubkyPublicKeyFormat.redacted(prefixedKey)): \(error)", | ||
| "Failed to refresh contact link for \(PubkyPublicKeyFormat.redacted(prefixedKey)): \(error)", | ||
| context: "ContactsManager" | ||
| ) | ||
| } | ||
|
|
@@ -643,41 +644,33 @@ | |
| /// Remembers the profiles it saves for this session, so the Contacts list shows them at once rather than only their | ||
| /// saved labels. A placeholder for a follow whose lookup failed is not remembered, so that profile is still looked up. | ||
| /// An import outlives its screens, so a reset or another identity's load while it runs, such as after a sign-out, | ||
| /// stops it quietly: it saves nothing more, adds nothing to the next session's list and reports no error. | ||
| /// drops its result quietly: it adds nothing to the next session's list and reports no error. | ||
| func importContacts( | ||
| contacts selected: [PubkyContact], | ||
| saveContact: (String, String) async throws -> Void = { publicKey, label in | ||
| _ = try await PubkyService.saveContact(publicKey: publicKey, label: label, restorePrivateConnection: true) | ||
| saveContacts: ([ContactUpdate], String?) async throws -> Void = { updates, expectedIdentity in | ||
| _ = try await PubkyService.saveContacts(updates: updates, expectedIdentity: expectedIdentity) | ||
| } | ||
| ) async throws { | ||
| let profilesGeneration = resolvedProfilesGeneration | ||
| var imported: [PubkyContact] = [] | ||
| let expectedIdentity = resolvedProfilesOwner | ||
| var existingKeys = Set(contacts.map(\.publicKey)) | ||
| var firstError: Error? | ||
| let imported = selected.filter { existingKeys.insert($0.publicKey).inserted } | ||
| try Task.checkCancellation() | ||
| guard !imported.isEmpty else { return } | ||
|
|
||
| for contact in selected { | ||
| do { | ||
| try await saveContacts(imported.map { ContactUpdate(publicKey: $0.publicKey, label: $0.displayName) }, expectedIdentity) | ||
| } catch is CancellationError { | ||
| throw CancellationError() | ||
| } catch { | ||
| try Task.checkCancellation() | ||
| guard profilesGeneration == resolvedProfilesGeneration else { break } | ||
| guard !existingKeys.contains(contact.publicKey) else { continue } | ||
| do { | ||
| // The preview already resolved this profile. Receiver discovery runs during contact refresh. | ||
| try await saveContact(contact.publicKey, contact.displayName) | ||
| imported.append(contact) | ||
| existingKeys.insert(contact.publicKey) | ||
| } catch is CancellationError { | ||
| throw CancellationError() | ||
| } catch { | ||
| firstError = firstError ?? error | ||
| Logger.warn( | ||
| "Failed to save imported contact '\(PubkyPublicKeyFormat.redacted(contact.publicKey))': \(error)", | ||
| context: "ContactsManager" | ||
| ) | ||
| } | ||
| guard profilesGeneration == resolvedProfilesGeneration else { return } | ||
| throw error | ||
| } | ||
|
|
||
| try Task.checkCancellation() | ||
| guard profilesGeneration == resolvedProfilesGeneration else { | ||
| Logger.info("Stopped a contact import that a reset overtook after \(imported.count) saves", context: "ContactsManager") | ||
| Logger.info("Discarded a contact import that a reset overtook", context: "ContactsManager") | ||
| return | ||
| } | ||
| for contact in imported { | ||
|
|
@@ -686,9 +679,6 @@ | |
| let currentKeys = Set(contacts.map(\.publicKey)) | ||
| contacts = (contacts + imported.filter { !currentKeys.contains($0.publicKey) }).sorted(by: Self.isOrderedByName) | ||
| Logger.info("Imported \(imported.count) new contacts", context: "ContactsManager") | ||
| if let firstError { | ||
| throw firstError | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Update Contact | ||
|
|
@@ -867,7 +857,7 @@ | |
| stopProfileRefresh() | ||
| stopPendingProfileLookups() | ||
| let contactRecords = contactRecords | ||
| let removeContactRecord = removeContactRecord | ||
| let removeContactRecords = removeContactRecords | ||
| let records: [ContactRecord] | ||
| do { | ||
| records = try await Task.detached { | ||
|
|
@@ -885,35 +875,17 @@ | |
| return | ||
| } | ||
|
|
||
| var deletedKeys = Set<String>() | ||
| var firstError: Error? | ||
|
|
||
| for record in records { | ||
| guard let contactKey = PubkyPublicKeyFormat.normalized(record.publicKey) else { continue } | ||
| do { | ||
| try await Task.detached { | ||
| try await removeContactRecord(contactKey) | ||
| }.value | ||
| deletedKeys.insert(contactKey) | ||
| } catch { | ||
| firstError = firstError ?? error | ||
| Logger.warn("Failed to delete contact '\(PubkyPublicKeyFormat.redacted(contactKey))': \(error)", context: "ContactsManager") | ||
| } | ||
| } | ||
|
|
||
| if let firstError { | ||
| if !deletedKeys.isEmpty { | ||
| await forgetRemovedContacts(Array(deletedKeys)) | ||
| for publicKey in deletedKeys { | ||
| Self.removeContactProfileOverride(publicKey: publicKey) | ||
| } | ||
| contacts.removeAll { deletedKeys.contains($0.publicKey) } | ||
| } | ||
| throw firstError | ||
| let keys = records.compactMap { PubkyPublicKeyFormat.normalized($0.publicKey) } | ||
| let removed = try await Task.detached { try await removeContactRecords(keys) }.value | ||
| let deletedKeys = Set(removed.compactMap { PubkyPublicKeyFormat.normalized($0.publicKey) }) | ||
| await forgetRemovedContacts(Array(deletedKeys)) | ||
| Self.removeContactProfileOverrides(publicKeys: deletedKeys) | ||
| contacts.removeAll { deletedKeys.contains($0.publicKey) } | ||
| guard deletedKeys.isSuperset(of: keys) else { | ||
| throw PrivatePaykitError.privateUnavailable | ||
| } | ||
|
|
||
| // All remote deletes succeeded, so clear any local-only contacts too. | ||
| await forgetRemovedContacts(Array(deletedKeys)) | ||
| Self.clearContactProfileOverrides() | ||
| await PrivatePaykitService.shared.pruneUnsavedContactState(savedPublicKeys: []) | ||
| contacts.removeAll() | ||
|
|
@@ -1160,20 +1132,6 @@ | |
| return true | ||
| } | ||
|
|
||
| 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 isOrderedByName(_ lhs: PubkyContact, _ rhs: PubkyContact) -> Bool { | ||
|
|
@@ -1211,9 +1169,12 @@ | |
|
|
||
| private nonisolated static func removeContactProfileOverride(publicKey: String) { | ||
| guard let prefixedKey = PubkyPublicKeyFormat.normalized(publicKey) else { return } | ||
| var overrides = loadContactProfileOverrides() | ||
| overrides.removeValue(forKey: prefixedKey) | ||
| saveContactProfileOverrides(overrides) | ||
| removeContactProfileOverrides(publicKeys: [prefixedKey]) | ||
| } | ||
|
|
||
| private nonisolated static func removeContactProfileOverrides(publicKeys: Set<String>) { | ||
| guard !publicKeys.isEmpty else { return } | ||
| saveContactProfileOverrides(loadContactProfileOverrides().filter { !publicKeys.contains($0.key) }) | ||
| } | ||
|
|
||
| private nonisolated static func clearContactProfileOverrides() { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.