feat: add profile and contacts fetching from pubky#476
feat: add profile and contacts fetching from pubky#476ben-kaufman wants to merge 55 commits intomasterfrom
Conversation
ovitrif
left a comment
There was a problem hiding this comment.
LGTM with some nit comments.
all tests worked great, tested:
- open profile from topbar + drawer
- 2x auth successes
- button to download ring
- share pk button(s)
- signout button
This comment has been minimized.
This comment has been minimized.
| image.draw(in: CGRect(origin: .zero, size: newSize)) | ||
| } | ||
| // Compress as JPEG | ||
| return resized.jpegData(compressionQuality: 0.8) ?? Data() |
There was a problem hiding this comment.
Bug: compressAvatar silently uploads empty data
If jpegData(compressionQuality:) returns nil (which can happen for certain image types or edge cases), the method falls back to Data() — an empty byte array — which is then uploaded to the homeserver with no error thrown.
bitkit-ios/Bitkit/Managers/PubkyProfileManager.swift
Lines 183 to 190 in d7861fc
The empty blob URI is persisted in profile.json via writeProfile, and cached locally via cacheProfileMetadata. Every subsequent attempt to load the avatar will fail silently with a decoding error, leaving the user with a permanently broken avatar and no indication of what went wrong during upload.
Suggested fix — propagate the failure rather than swallowing it:
| return resized.jpegData(compressionQuality: 0.8) ?? Data() | |
| guard let jpegData = resized.jpegData(compressionQuality: 0.8) else { | |
| throw NSError(domain: "PubkyProfileManager", code: -1, | |
| userInfo: [NSLocalizedDescriptionKey: "Failed to encode avatar as JPEG"]) | |
| } | |
| return jpegData |
| Logger.info("Added contact \(prefixedKey)", context: "ContactsManager") | ||
|
|
||
| let contact = PubkyContact(publicKey: prefixedKey, profile: profile) | ||
| contacts.append(contact) |
There was a problem hiding this comment.
Bug: addContact does not deduplicate before appending
The contact is appended unconditionally, so calling addContact twice with the same public key results in two entries in the contacts array. By contrast, importContacts at line 236–238 correctly guards against this with a Set-based filter.
bitkit-ios/Bitkit/Managers/ContactsManager.swift
Lines 185 to 193 in d7861fc
Downstream, updateContact (which uses firstIndex) would silently update only the first copy, leaving stale duplicates in the list.
Suggested fix:
| contacts.append(contact) | |
| guard !contacts.contains(where: { $0.publicKey == prefixedKey }) else { return } | |
| let contact = PubkyContact(publicKey: prefixedKey, profile: profile) | |
| contacts.append(contact) |
| } | ||
|
|
||
| @MainActor | ||
| class PubkyProfileManager: ObservableObject { |
There was a problem hiding this comment.
CLAUDE.md: Use @Observable class, not ObservableObject
Per CLAUDE.md:
"Use
@Observable classfor shared business logic instead of ViewModels… This project follows modern SwiftUI patterns and explicitly AVOIDS traditional MVVM with ViewModels."
Both PubkyProfileManager and ContactsManager use ObservableObject + @Published, and are injected in AppScene.swift via @StateObject / .environmentObject — all three are the legacy pattern this project is migrating away from.
The correct approach:
// Manager
@Observable
@MainActor
class PubkyProfileManager {
var authState: PubkyAuthState = .idle
var profile: PubkyProfile?
// ... (no @Published needed)
}
// AppScene.swift
@State private var pubkyProfile = PubkyProfileManager()
// ...
.environment(pubkyProfile)
// Consuming views
@Environment(PubkyProfileManager.self) private var pubkyProfileThe same applies to ContactsManager.
| // MARK: - ContactsManager | ||
|
|
||
| @MainActor | ||
| class ContactsManager: ObservableObject { |
There was a problem hiding this comment.
CLAUDE.md: Use @Observable class, not ObservableObject
Same issue as PubkyProfileManager — see the comment there for the full context and the prescribed migration pattern. ContactsManager should be converted to @Observable class with its properties losing the @Published wrapper, and its injection in AppScene.swift changed from @StateObject/.environmentObject to @State/.environment.
- Guard against adding a contact that already exists in the list - compressAvatar now throws instead of returning empty Data on failure Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Some observations from intial testing:
Screen.Recording.2026-04-08.at.16.57.05.mov
Screen.Recording.2026-04-08.at.18.06.43.movManaged to import pubky with Ring:
Screen.Recording.2026-04-08.at.17.29.53.mov |
ovitrif
left a comment
There was a problem hiding this comment.
Review Pass 1: Testing profile & contacts
1. Getting inconsistent number of contacts imported
vs. Android and also vs. previous attempts (via disconnect + reimport)
2. Disconnect Profile instead of Delete Profile
Android shows Delete, Figma also.
Dialog texts are also different on Android, but neither of the 2 uses the texts from Figma.
2.1 Missing footnote
iOS doesn't have the 2-line text about profile info being public.
3. Delete Button UI not updated to latest design
Added remarks on the image:
- Contact Detail: Remove Contact icon button was dropped
- Contact Edit: Button style mismatch
4. Profile/ Contacts Links should be editable
Currently they look like like a disabled input field, should behave like editable inputs.
Should update android PR with this as well:
5. Nit: Missing bottom gradient overlay above scrollview
In Edit Profile/ Contact
6. Delete Profile should navigate back to profile onboarding
Currently we have Disconnect, that should be changed, once we have Delete instead, we should navigate to profile onboarding instead of wallet home after profile removal completes.
7. Contacts Counter in avatar group needs polish
It doesn't render border and text is not fully visible.
Overall solid work, looks a bit more stable than Android at this point.
My biggest concern is the variation in number of imported contacts.
Android keeps importing 100, but even that 100 is probably capped incorrectly. This can be addressed afterwards too.
I will have a look at the code too, aiming to approve, based on agreements in Slack: we can address polishing and fixes in new PRs.
# Conflicts: # CHANGELOG.md
Create Profile flow testing
1. Create profile
Screen.Recording.2026-04-10.at.10.13.53.mov2. Edit/Delete Profile
Screen.Recording.2026-04-10.at.11.16.21.mov
Screen.Recording.2026-04-10.at.10.50.21.mov3. Add contact
Screen.Recording.2026-04-10.at.11.34.19.mov
Screen.Recording.2026-04-10.at.11.48.28.mov4. Edit / Delete contact
|
…iting Adds PubkyPublicKeyFormat for consistent key validation/normalization, prevents adding yourself as a contact, improves error states in add contact flow, enables photo picker for contact avatars, and cleans up profile view sign-out button placement. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Mirrors the Android implementation (bitkit-android#824) to ensure contacts are cleaned up on the homeserver before the profile is deleted, preventing orphaned contact data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Additional observations (on 04d06fc): 1.
Expected:
Actual:
2. It would be great if we could have same behavior on pubky validation when adding contacts (currently differs on iOS - validation on pubky input vs Android - validation after taping add, on Android also subsequent Retry button does not work). It would make things consistent + easier for e2e tests. 🙏 Screen.Recording.2026-04-10.at.15.59.16.mov3. Seems not able to add other contact that was created in Android. Was able to add contact using pubky from pubky-ring. bitkit_logs_2026-04-10_14-05-50.zip Screen.Recording.2026-04-10.at.16.00.30.mov4. Contacts - shows "C" instead of CONTACTS
|
|
About last point, C is for the letter of the contact name, ie like in phone contacts list the subtitle is the first letter of all contacts in that name |
Got it, still it is missing "Contacts" as per Figma. Edit: I guess also the alphabetical section labels (C here) do not match Figma. 🤷♂️ |



Summary
Integrates Pubky decentralized identity into Bitkit, allowing users to create a Bitkit-managed profile or connect via Pubky Ring authentication. Once connected, the user's profile name and avatar appear on the home screen header, a full profile page shows their bio, links, tags, and shareable QR code, and a contacts section lists people they follow on the Pubky network.
What's included
pubkyauth://)PubkyService— service layer wrappingpaykit-ffi(profile/contacts/payments) andbitkit-core(auth relay, PKDNS file fetching, key derivation)PubkyProfileManager— manages auth state, session lifecycle, key derivation, profile creation/editing, and remote profile fetchingContactsManager— fetches contacts in parallel viawithTaskGroup, groups alphabetically, supports add/edit/delete/import, discovers remote contacts from pubky.appPubkyImagecomponent for loadingpubky://URIs with two-tier (memory + disk) cachingPubkyProfileDatabackward-compatible decoding —tagsfield defaults to empty array when missing from JSON (older profiles, other clients)New dependencies
paykit-rs(SPM, pinned revision) — Pubky SDK for profile, contacts, and payment operationsCoreBluetoothframework (linker flag, required by paykit-rs)Key new files
Services/PubkyService.swiftManagers/PubkyProfileManager.swiftManagers/ContactsManager.swiftModels/PubkyProfile.swiftModels/PubkyAuthRequest.swiftComponents/PubkyImage.swiftpubky://image loader with disk+memory cacheViews/Profile/ProfileView.swiftViews/Profile/CreateProfileView.swiftViews/Profile/EditProfileView.swiftViews/Profile/PayContactsView.swiftViews/Profile/PubkyChoiceView.swiftViews/Profile/PubkyRingAuthView.swiftViews/Profile/AddLinkSheet.swiftViews/Profile/AddProfileTagSheet.swiftViews/Contacts/ContactsListView.swiftViews/Contacts/ContactDetailView.swiftViews/Contacts/ContactImportOverviewView.swiftViews/Contacts/ContactImportSelectView.swiftViews/Contacts/AddContactView.swiftViews/Contacts/EditContactView.swiftViews/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swiftBitkitTests/PubkyModelTests.swiftBitkitTests/PubkyProfileManagerTests.swiftBitkitTests/PubkyAuthRequestTests.swiftTest plan
🤖 Generated with Claude Code