Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
30 changes: 2 additions & 28 deletions Bitkit/Managers/ContactsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ class ContactsManager: ObservableObject {
_ = try await PubkyService.removeContact(publicKey: $0)
},
removeContactRecords: @escaping @Sendable (_ publicKeys: [String]) async throws -> [ContactRecord] = {
try await PubkyService.removeContacts(publicKeys: $0)
try await PubkyService.removeContactsForProfile(publicKeys: $0)
},
forgetRemovedContacts: @escaping @Sendable (_ publicKeys: [String]) async -> Void = {
await PrivatePaykitService.shared.removeSavedContacts(publicKeys: $0)
Expand Down Expand Up @@ -972,22 +972,7 @@ class ContactsManager: ObservableObject {
stopPendingProfileLookups()
let contactRecords = contactRecords
let removeContactRecords = removeContactRecords
let records: [ContactRecord]
do {
records = try await Task.detached {
try await contactRecords()
}.value
} catch {
guard Self.isMissingContactsDataError(error) else {
throw error
}

Self.clearContactProfileOverrides()
await PrivatePaykitService.shared.pruneUnsavedContactState(savedPublicKeys: [])
contacts.removeAll()
Logger.info("Contacts storage missing, treating delete-all as empty", context: "ContactsManager")
return
}
let records = try await Task.detached { try await contactRecords() }.value

let keys = records.compactMap { PubkyPublicKeyFormat.normalized($0.publicKey) }
let removed = try await Task.detached { try await removeContactRecords(keys) }.value
Expand All @@ -1006,17 +991,6 @@ class ContactsManager: ObservableObject {
Logger.info("Deleted all contacts", context: "ContactsManager")
}

func deleteAllContactsBestEffort() async {
do {
try await deleteAllContacts()
} catch {
Logger.warn("Continuing after contact cleanup failed: \(error)", context: "ContactsManager")
Self.clearContactProfileOverrides()
await PrivatePaykitService.shared.pruneUnsavedContactState(savedPublicKeys: [])
contacts.removeAll()
}
}

// MARK: - Remote Contact Discovery

/// Looks every follow's profile up on the interactive read lane: the user waits on the choice screen until the import
Expand Down
54 changes: 16 additions & 38 deletions Bitkit/Managers/PubkyProfileManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ class PubkyProfileManager: ObservableObject {
private let remoteProfileResolver: RemoteProfileResolver
private let profilePublisher: ProfilePublisher
private let avatarUploader: AvatarUploader
private let profileDeletionValidator: ProfileDeleter
private let profileDeleter: ProfileDeleter
/// Bumped whenever a profile save starts, `profile` is written or the identity changes, so a remote read that started
/// earlier is dropped.
Expand Down Expand Up @@ -169,13 +170,17 @@ class PubkyProfileManager: ObservableObject {
avatarUploader: @escaping AvatarUploader = {
try await PubkyService.uploadProfileAvatar(bytes: $0, contentType: "image/jpeg", expectedIdentity: $1)
},
profileDeletionValidator: @escaping ProfileDeleter = {
try await PubkyService.requireNoActiveSubscriptions(expectedIdentity: $0)
},
profileDeleter: @escaping ProfileDeleter = {
try await PubkyService.deletePaykitProfile(expectedIdentity: $0)
}
) {
self.remoteProfileResolver = remoteProfileResolver
self.profilePublisher = profilePublisher
self.avatarUploader = avatarUploader
self.profileDeletionValidator = profileDeletionValidator
self.profileDeleter = profileDeleter
cachedName = UserDefaults.standard.string(forKey: Self.cachedNameKey)
cachedImageUri = UserDefaults.standard.string(forKey: Self.cachedImageUriKey)
Expand Down Expand Up @@ -1067,6 +1072,12 @@ class PubkyProfileManager: ObservableObject {
await PrivatePaykitService.shared.endProfileDeletion()
}

func validateProfileDeletion(for session: SignedInSession) async throws {
try requireCurrentSession(session)
try await profileDeletionValidator(session.publicKey)
try requireCurrentSession(session)
}

func deleteProfile(cleanPrivatePaykitEndpoints: Bool = true, expectedSession: SignedInSession? = nil) async throws {
guard let session = expectedSession ?? currentSession else { throw PubkyServiceError.sessionNotActive }
try requireCurrentSession(session)
Expand All @@ -1082,26 +1093,18 @@ class PubkyProfileManager: ObservableObject {
}

private func deleteProfile(for session: SignedInSession, cleanPrivatePaykitEndpoints: Bool) async throws {
try requireCurrentSession(session)
try await validateProfileDeletion(for: session)
if cleanPrivatePaykitEndpoints {
await Self.removePrivatePaykitEndpointsBestEffort(
try await Self.removePrivatePaykitEndpoints(
context: "PubkyProfileManager.deleteProfile", deferAppSync: true,
isSessionCurrent: { self.currentSession == session }
)
}
try requireCurrentSession(session)
let delete = profileDeleter
do {
try await Task.detached {
try await delete(session.publicKey)
}.value
} catch {
guard Self.isMissingBitkitProfileStorageError(error) else {
throw error
}

Logger.info("Bitkit profile storage already missing, continuing sign out", context: "PubkyProfileManager")
}
try await Task.detached {
try await delete(session.publicKey)
}.value

try requireCurrentSession(session)
invalidateProfileLoads()
Expand Down Expand Up @@ -1971,31 +1974,6 @@ class PubkyProfileManager: ObservableObject {
return normalized
}

nonisolated static func isMissingBitkitProfileStorageError(_ error: Error) -> Bool {
if case .profileNotFound = error as? PubkyServiceError {
return true
}

let errorText = [
(error as? AppError)?.debugMessage,
error.localizedDescription,
String(describing: error),
]
.compactMap { $0?.lowercased() }

if errorText.contains(where: { $0.contains("404 not found") || $0.contains("directory not found") }) {
return true
}

let nsError = error as NSError
if nsError.domain == NSCocoaErrorDomain {
let cocoaCode = CocoaError.Code(rawValue: nsError.code)
return cocoaCode == .fileNoSuchFile || cocoaCode == .fileReadNoSuchFile
}

return false
}

nonisolated static func resolveSessionInitialization(
savedSessionSecret: String?,
storedSecretKeyHex: String?,
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/ar.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,7 @@
"profile__edit_delete_section" = "حذف";
"profile__edit_unlink_section" = "إلغاء الربط";
"profile__edit_public_note" = "يرجى ملاحظة أن معلومات الملف الشخصي مخزنة في ملفات عامة.";
"profile__delete_active_subscription" = "تمنع الاشتراكات النشطة إزالة هذا الملف الشخصي.";
"profile__delete_title" = "حذف الملف الشخصي؟";
"profile__delete_description" = "يؤدي حذف ملفك الشخصي إلى إزالته من Pubky. قد تحتفظ جهات الاتصال بمعلومات الملف الشخصي والدفع التي شاركتها معهم سابقًا.";
"profile__delete_confirm" = "نعم، احذف";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/ca.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,7 @@
"profile__edit_delete_section" = "ELIMINAR";
"profile__edit_unlink_section" = "DESVINCULAR";
"profile__edit_public_note" = "Tingues en compte que la informació del perfil es desa en fitxers públics.";
"profile__delete_active_subscription" = "Les subscripcions actives impedeixen eliminar aquest perfil.";
"profile__delete_title" = "Eliminar el perfil?";
"profile__delete_description" = "Eliminar el teu perfil l'esborra de Pubky. És possible que els contactes conservin la informació de perfil i de pagament que s'hi hagi compartit abans.";
"profile__delete_confirm" = "Sí, elimina";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/cs.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,7 @@
"profile__edit_delete_section" = "SMAZAT";
"profile__edit_unlink_section" = "ZRUŠIT PROPOJENÍ";
"profile__edit_public_note" = "Upozorňujeme, že informace o profilu jsou uloženy ve veřejných souborech.";
"profile__delete_active_subscription" = "Aktivní předplatná brání odstranění tohoto profilu.";
"profile__delete_title" = "Smazat profil?";
"profile__delete_description" = "Smazáním profilu ho odstraníte z Pubky. Kontakty si mohou ponechat informace o profilu a platbách, které jste s nimi dříve sdíleli.";
"profile__delete_confirm" = "Ano, smazat";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/de.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1166,6 +1166,7 @@
"profile__edit_delete_section" = "LÖSCHEN";
"profile__edit_unlink_section" = "VERKNÜPFUNG AUFHEBEN";
"profile__edit_public_note" = "Bitte beachte, dass Profilinformationen in öffentlichen Dateien gespeichert werden.";
"profile__delete_active_subscription" = "Aktive Abonnements verhindern das Entfernen dieses Profils.";
"profile__delete_title" = "Profil löschen?";
"profile__delete_description" = "Durch das Löschen deines Profils wird es von Pubky entfernt. Kontakte können zuvor mit ihnen geteilte Profil- und Zahlungsinformationen weiterhin behalten.";
"profile__delete_confirm" = "Ja, löschen";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/el.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,7 @@
"profile__edit_delete_section" = "ΔΙΑΓΡΑΦΗ";
"profile__edit_unlink_section" = "ΑΠΟΣΥΝΔΕΣΗ";
"profile__edit_public_note" = "Τα στοιχεία του προφίλ αποθηκεύονται σε δημόσια αρχεία.";
"profile__delete_active_subscription" = "Οι ενεργές συνδρομές εμποδίζουν την αφαίρεση αυτού του προφίλ.";
"profile__delete_title" = "Διαγραφή Προφίλ;";
"profile__delete_description" = "Η διαγραφή του προφίλ σας το αφαιρεί από το Pubky. Οι επαφές μπορεί να διατηρούν ακόμα πληροφορίες προφίλ και πληρωμών που τους κοινοποιήθηκαν παλαιότερα.";
"profile__delete_confirm" = "Ναι, Διαγραφή";
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 @@ -1181,6 +1181,7 @@
"profile__edit_delete_section" = "DELETE";
"profile__edit_unlink_section" = "UNLINK";
"profile__edit_public_note" = "Please note profile information is stored in public files.";
"profile__delete_active_subscription" = "Active subscriptions prevent removing this profile.";
"profile__delete_title" = "Delete Profile?";
"profile__delete_description" = "Deleting your profile removes it from Pubky. Contacts may still retain profile and payment information previously shared with them.";
"profile__delete_confirm" = "Yes, Delete";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1171,6 +1171,7 @@
"profile__edit_delete_section" = "ELIMINAR";
"profile__edit_unlink_section" = "DESVINCULAR";
"profile__edit_public_note" = "Tenga en cuenta que la información del perfil se guarda en archivos públicos.";
"profile__delete_active_subscription" = "Las suscripciones activas impiden eliminar este perfil.";
"profile__delete_title" = "¿Eliminar perfil?";
"profile__delete_description" = "Al eliminar su perfil, se borra de Pubky. Es posible que sus contactos conserven la información de perfil y de pago que compartió con ellos anteriormente.";
"profile__delete_confirm" = "Sí, eliminar";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/es.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,7 @@
"profile__edit_delete_section" = "ELIMINAR";
"profile__edit_unlink_section" = "DESVINCULAR";
"profile__edit_public_note" = "Tenga en cuenta que la información del perfil se guarda en archivos públicos.";
"profile__delete_active_subscription" = "Las suscripciones activas impiden eliminar este perfil.";
"profile__delete_title" = "¿Eliminar el perfil?";
"profile__delete_description" = "Al eliminar su perfil, se elimina de Pubky. Es posible que sus contactos conserven la información de perfil y de pago que compartió con ellos anteriormente.";
"profile__delete_confirm" = "Sí, eliminar";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/fr.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,7 @@
"profile__edit_delete_section" = "SUPPRIMER";
"profile__edit_unlink_section" = "DISSOCIER";
"profile__edit_public_note" = "Veuillez noter que les informations de profil sont stockées dans des fichiers publics.";
"profile__delete_active_subscription" = "Les abonnements actifs empêchent la suppression de ce profil.";
"profile__delete_title" = "Supprimer le profil ?";
"profile__delete_description" = "La suppression de votre profil le retire de Pubky. Vos contacts peuvent encore conserver les informations de profil et de paiement que vous leur avez partagées.";
"profile__delete_confirm" = "Oui, supprimer";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/it.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1145,6 +1145,7 @@
"profile__edit_delete_section" = "ELIMINA";
"profile__edit_unlink_section" = "SCOLLEGA";
"profile__edit_public_note" = "Tieni presente che le informazioni del profilo sono salvate in file pubblici.";
"profile__delete_active_subscription" = "Gli abbonamenti attivi impediscono la rimozione di questo profilo.";
"profile__delete_title" = "Eliminare il Profilo?";
"profile__delete_description" = "Eliminando il profilo lo rimuovi da Pubky. I contatti potrebbero conservare le informazioni di profilo e di pagamento condivise in precedenza con loro.";
"profile__delete_confirm" = "Sì, Elimina";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/nl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,7 @@
"profile__edit_delete_section" = "VERWIJDEREN";
"profile__edit_unlink_section" = "ONTKOPPELEN";
"profile__edit_public_note" = "Let op: profielgegevens worden opgeslagen in openbare bestanden.";
"profile__delete_active_subscription" = "Actieve abonnementen verhinderen het verwijderen van dit profiel.";
"profile__delete_title" = "Profiel verwijderen?";
"profile__delete_description" = "Als u uw profiel verwijdert, wordt het van Pubky verwijderd. Contacten kunnen eerder gedeelde profiel- en betaalgegevens nog bewaren.";
"profile__delete_confirm" = "Ja, verwijderen";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/pl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,7 @@
"profile__edit_delete_section" = "USUWANIE";
"profile__edit_unlink_section" = "ODŁĄCZANIE";
"profile__edit_public_note" = "Pamiętaj, że dane profilu są przechowywane w publicznych plikach.";
"profile__delete_active_subscription" = "Aktywne subskrypcje uniemożliwiają usunięcie tego profilu.";
"profile__delete_title" = "Usunąć profil?";
"profile__delete_description" = "Usunięcie profilu usuwa go z Pubky. Kontakty mogą nadal przechowywać udostępnione im wcześniej dane profilu i płatności.";
"profile__delete_confirm" = "Tak, usuń";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,7 @@
"profile__edit_delete_section" = "EXCLUIR";
"profile__edit_unlink_section" = "DESVINCULAR";
"profile__edit_public_note" = "Observe que as informações do perfil são armazenadas em arquivos públicos.";
"profile__delete_active_subscription" = "As assinaturas ativas impedem a remoção deste perfil.";
"profile__delete_title" = "Excluir Perfil?";
"profile__delete_description" = "Excluir seu perfil o remove do Pubky. Os contatos ainda podem manter informações de perfil e de pagamento compartilhadas anteriormente com eles.";
"profile__delete_confirm" = "Sim, Excluir";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/pt.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,7 @@
"profile__edit_delete_section" = "ELIMINAR";
"profile__edit_unlink_section" = "DESASSOCIAR";
"profile__edit_public_note" = "Tenha em atenção que as informações do perfil são guardadas em ficheiros públicos.";
"profile__delete_active_subscription" = "As subscrições ativas impedem a remoção deste perfil.";
"profile__delete_title" = "Eliminar perfil?";
"profile__delete_description" = "Eliminar o seu perfil remove-o do Pubky. Os contactos podem manter as informações de perfil e pagamento partilhadas anteriormente com eles.";
"profile__delete_confirm" = "Sim, Excluir";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/ru.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,7 @@
"profile__edit_delete_section" = "УДАЛЕНИЕ";
"profile__edit_unlink_section" = "ОТВЯЗКА";
"profile__edit_public_note" = "Обратите внимание: данные профиля хранятся в публичных файлах.";
"profile__delete_active_subscription" = "Активные подписки не позволяют удалить этот профиль.";
"profile__delete_title" = "Удалить Профиль?";
"profile__delete_description" = "Удаление профиля удалит его из Pubky. У контактов могут остаться данные профиля и платёжные данные, которыми вы делились ранее.";
"profile__delete_confirm" = "Да, Удалить";
Expand Down
40 changes: 40 additions & 0 deletions Bitkit/Services/PubkyService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,10 @@ enum PubkyService {
try await PaykitSdkService.shared.uploadAvatar(bytes: bytes, contentType: contentType, expectedIdentity: expectedIdentity)
}

static func requireNoActiveSubscriptions(expectedIdentity: String) async throws {
try await PaykitSdkService.shared.requireNoActiveSubscriptions(expectedIdentity: expectedIdentity)
}

static func deletePaykitProfile(expectedIdentity: String? = nil) async throws {
try await PaykitSdkService.shared.deletePaykitProfile(expectedIdentity: expectedIdentity)
}
Expand Down Expand Up @@ -393,6 +397,10 @@ enum PubkyService {
try await PaykitSdkService.shared.removeContact(publicKey: publicKey)
}

static func removeContactsForProfile(publicKeys: [String]) async throws -> [Paykit.ContactRecord] {
try await PaykitSdkService.shared.removeContactsForProfile(publicKeys: publicKeys)
}

static func removeContacts(publicKeys: [String]) async throws -> [Paykit.ContactRecord] {
try await PaykitSdkService.shared.removeContacts(publicKeys: publicKeys)
}
Expand Down Expand Up @@ -818,11 +826,35 @@ actor PaykitSdkService {
return record.uri
}

func requireNoActiveSubscriptions(expectedIdentity: String) async throws {
try await withStateRevisionTracking { sdk in
try await Self.requireSignedInIdentity(expectedIdentity, in: sdk)
try await Self.requireNoActiveSubscriptions(in: sdk)
}
}

private static func requireNoActiveSubscriptions(in sdk: PaykitSdk) async throws {
let requests = try await sdk.paymentRequests()
try Task.checkCancellation()
let now = Date()
let activeSubscriptions = requests.filter {
$0.state == .activeRecurring &&
($0.terms?.recurrence?.endsAt.flatMap(PaykitPaymentRequest.parseDate).map { $0 > now } ?? true)
}
guard !activeSubscriptions.isEmpty else { return }
let endDates = activeSubscriptions.compactMap {
$0.terms?.recurrence?.endsAt.flatMap(PaykitPaymentRequest.parseDate)
}
let latestEndDate = endDates.count == activeSubscriptions.count ? endDates.max() : nil
throw PubkyServiceError.activeSubscription(endsAt: latestEndDate)
}

func deletePaykitProfile(expectedIdentity: String? = nil) async throws {
try await withStateRevisionTracking { sdk in
if let expectedIdentity {
try await Self.requireSignedInIdentity(expectedIdentity, in: sdk)
}
try await Self.requireNoActiveSubscriptions(in: sdk)
guard let publicKey = try await sdk.identityStatus()?.publicKey else { throw PubkyServiceError.sessionNotActive }
guard let current = try await sdk.fetchPaykitProfile(publicKey: publicKey) else { return }
try await sdk.deletePaykitProfile(expectedRevision: current.revision)
Expand Down Expand Up @@ -943,6 +975,14 @@ actor PaykitSdkService {
}
}

func removeContactsForProfile(publicKeys: [String]) async throws -> [Paykit.ContactRecord] {
try await withStateRevisionTracking { sdk in
try await Self.requireNoActiveSubscriptions(in: sdk)
guard !publicKeys.isEmpty else { return [] }
return try await sdk.removeContactsAndBlockPeers(publicKeys: publicKeys)
}
}

func removeContacts(publicKeys: [String]) async throws -> [Paykit.ContactRecord] {
guard !publicKeys.isEmpty else { return [] }
return try await withStateRevisionTracking { sdk in
Expand Down
Loading
Loading