Skip to content

feat: add profile and contacts fetching from pubky#824

Open
ben-kaufman wants to merge 59 commits intomasterfrom
feat/pubky-profile
Open

feat: add profile and contacts fetching from pubky#824
ben-kaufman wants to merge 59 commits intomasterfrom
feat/pubky-profile

Conversation

@ben-kaufman
Copy link
Copy Markdown
Contributor

@ben-kaufman ben-kaufman commented Mar 5, 2026

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.

Description

  • Dual profile creation flow — create a new profile from BIP39 seed derivation, or import via Pubky Ring deep links (pubkyauth://)
  • Profile restore — when creating a profile, if one already exists on the network for the derived keys, it is detected and pre-populated (name, bio, links, tags preserved)
  • Ring auth import flow — after Ring authentication, discover existing profile and contacts from pubky.app, present import overview with "Import all" or "Select" options, then proceed to Pay Contacts
    onboarding
  • Pay Contacts onboarding — new step after both create and Ring import flows, with toggle for sharing payment data
  • Profile page displaying name, bio, links, tags, and a QR code with the profile picture overlaid
  • Profile editing — edit name, bio, avatar, links, and tags with suggestion sheets
  • Contacts list with search, add button, and flat "Contacts" section header
  • Contact detail page showing name, truncated public key, bio, links, tags, and copy/share/edit/delete actions
  • Contact import — overview screen showing found profile & contacts count with avatar stack, selection screen with select all/none, batch import
  • Add contact — bottom sheet with pubky input or QR scan, loading animation with rotating ellipses, profile preview with privacy notice, save/discard
  • Edit contact — edit name, bio, links, tags with delete option
  • Home screen integration showing the authenticated user's name and avatar in the header
  • Session restoration — automatic re-sign-in using stored secret key when session import fails (Bitkit-managed); session preserved on failure for retry on next launch (Ring-managed); toast notification when
    restoration fails
  • PubkyAuth approval — handles pubkyauth:// URLs from QR scans and deep links, biometric auth before approval, three-state sheet (authorize → authorizing → success)
  • PubkyService — service layer wrapping bitkit-core (auth relay, PKDNS file fetching, key derivation, homeserver operations) and paykit (session management)
  • PubkyRepo — manages auth state, session lifecycle, key derivation, profile creation/editing, contact CRUD, homeserver file operations, and auth approval
  • PubkyProfileData backward-compatible decoding — tags field defaults to empty array when missing from JSON (older profiles, other clients)
  • Suggestion card auto-dismiss when user is already authenticated
  • Bumped bitkit-core to v0.1.56 — adds key derivation, homeserver session operations, auth approval FFI functions

Key new files

File Purpose
services/PubkyService.kt FFI bridge to bitkit-core and paykit
repositories/PubkyRepo.kt Auth state, session persistence, key derivation, profile/contact CRUD
models/PubkyProfile.kt Profile/contact data models with backward-compatible JSON decoding
models/PubkyAuthRequest.kt Auth request capability parsing
ui/components/CenteredProfileHeader.kt Reusable centered profile header (avatar, name, key, bio)
ui/components/ProfileEditForm.kt Reusable form for profile/contact editing
ui/components/AddLinkSheet.kt Add link bottom sheet with suggestions
ui/components/AddTagSheet.kt Add tag bottom sheet with suggestions
ui/screens/profile/PubkyChoiceScreen.kt Create vs Ring import choice
ui/screens/profile/CreateProfileScreen.kt Profile creation with restore detection
ui/screens/profile/EditProfileScreen.kt Profile editing with avatar upload
ui/screens/profile/PayContactsScreen.kt Pay Contacts onboarding step
ui/screens/profile/PubkyAuthApprovalSheet.kt Auth approval as authenticator
ui/screens/contacts/AddContactScreen.kt Add contact by public key or QR
ui/screens/contacts/EditContactScreen.kt Edit contact details
ui/screens/contacts/ContactImportOverviewScreen.kt Import overview (profile + contacts summary)
ui/screens/contacts/ContactImportSelectScreen.kt Selectable contact list for import
tests/PubkyProfileDataTest.kt JSON encode/decode and backward compatibility tests
tests/PubkyAuthRequestTest.kt Capability parsing tests

QA Notes

  • Fresh install: tap Profile in drawer → shows intro → continue → choice screen (Create / Import with Ring)
  • Create flow: Create Profile → enter name, pick avatar → Continue → Pay Contacts → Profile page
  • Create restore: If profile already exists for derived keys, name is pre-populated and title shows "Restore Profile"
  • Ring flow: Import with Ring → opens Pubky Ring → approve → returns to Bitkit → Import Overview → Import All or Select → Pay Contacts → Profile page
  • Ring flow (no contacts): If no remote contacts found, skip import and go to Pay Contacts
  • Import select: Can select all, select none, or pick individual contacts
  • Profile page shows name, bio, links, tags, QR code with profile picture overlay
  • Edit profile: tap edit → modify name/bio/links/tags → save → updated
  • Home header shows profile name and avatar after authentication
  • Kill and relaunch app → session restored, profile/contacts shown without re-auth
  • Kill and relaunch with no network → session preserved, toast shown, next launch with network restores
  • Sign out from profile page → returns to unauthenticated state, contacts cleared
  • Contacts list shows "My Profile" row, contacts section, search, and add button
  • Add contact by public key → fetches profile → shows preview with privacy notice → save
  • Add contact via QR scan → opens scanner → scans pubky → navigates to add contact
  • Edit contact → modify details → save → changes reflected in detail view
  • Delete contact from edit screen → confirmation → removed
  • Inline tag add/remove on contact detail → persists immediately
  • Scan pubkyauth:// QR code → shows approval sheet with permissions → biometric → authorize → success
  • Receive pubkyauth:// deep link → same approval flow
  • Suggestion sheets for links and tags show lightbulb icon and predefined suggestions

@claude

This comment has been minimized.

claude[bot]

This comment was marked as outdated.

@jvsena42

This comment was marked as outdated.

@ovitrif ovitrif added this to the 2.2.0 milestone Mar 9, 2026
@ovitrif
Copy link
Copy Markdown
Collaborator

ovitrif commented Apr 9, 2026

All this is the implementation we agreed upon (at least as I understood it) after all discussions we had. @ovitrif can you confirm this is correct?

I share the exact same understanding, aka. I confirm!

Copy link
Copy Markdown
Collaborator

@ovitrif ovitrif left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Pass 2: Test Profile Create & Edit + Contacts

nit: Toast should not show here
scene: right after profile is created

Wipe Wallet doesn't fully clear profile
Profile still shows after wipe + create new wallet, and I can still go to profile page.
Should be wiped like all other data.


Unexpected red icon in bottom-right of profile placeholder


Edit Profile Sheets Unpolished

Recommending to use as template one of the simplest sheets we already polished in the app: ActivityAddTagSheet.

Image

Link labels centered - should be left-aligned


Import Profile flow - new observations

1. Save new avatar picture error

Changing avatar pic never work for imported profile, it works for new profile every time I change it.

Error log excerpt:

22:04:02.304  E  2026-04-07 20:04:02.303 ERROR   [EditProfileViewModel.kt:160]        Failed to upload avatar [IllegalArgumentException='No secret key available'] - EditProfileViewModel
 java.lang.IllegalArgumentException: No secret key available
	at to.bitkit.repositories.PubkyRepo$uploadAvatar$4$1.invokeSuspend(PubkyRepo.kt:346)

2. Contact import warn logs

Got spammed with these logs, most likely occurring for every contact after I already imported in the past testing sessions

2026-04-07 21:05:21.866 WARN    [PubkyRepo.kt:571]                   Failed to import contact 'pubky6n3stw8ucfubm4gja1nr7t7ujk9xcukexkmwhhpy4trye5j5x9ty' [AppError='reason=Request failed: HTTP transport error: error sending request for url (https://_pubky.wzggsym1558jc1d5k6nd5o33rpj5wefypemb1na1niwytbnrm9qy/session)'] - PubkyRepo 
to.bitkit.utils.AppError: reason=Request failed: HTTP transport error: error sending request for url (https://_pubky.wzggsym1558jc1d5k6nd5o33rpj5wefypemb1na1niwytbnrm9qy/session)

The deeper stack trace seem to be pointing to FFI boilerplate code for PubkyException.AuthFailed.

It might be a non-issue for users if everything else works errorless.
Since it's a side effect or disconnect + & re-import via Pubky Ring.

Also worth mentioning contacts list is still populated after re-import.


3. Critical: Unexpected random disconnect (only observed 2 times yesterday)

After a long time waiting in the app, I get disconnected from my Pubky account in Bitkit (IIRC I traced a http relay gateway timeout error), and afterwards I can’t go to the profile page anymore, any navigation link points me to the onboarding screen in the flow.


Comment on lines +1 to +2
@file:Suppress("ImportOrdering")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed, detekt succeeds without this.

Comment on lines +241 to +255
internal fun homegateUrlFor(
network: Network,
isLocalE2eBackend: Boolean,
e2eHomegateUrl: String,
): String {
if (isLocalE2eBackend) {
return e2eHomegateUrl
}

return when (network) {
Network.BITCOIN -> "https://homegate.pubky.app"
else -> "https://homegate.staging.pubky.app"
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: logic can be inlined; it's only called once, val homegateUrl is already computed property, no real need for this internal fun homegateUrlFor

}
_contacts.update { loadedContacts }
}.onFailure {
Logger.error("Failed to load contacts", it, context = TAG)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loadContacts() also fails when creating a profile because that account has no contacts yet. AFAIU it should not be logged as an error, maybe a warning at most.

Could perhaps use 404 + something else as discriminator for the logic -- to make sure it's this case and not 404 because of other reasons

log:

2026-04-09 14:37:31.301 ERROR   [PubkyRepo.kt:471]                   Failed to load contacts [AppError='reason=Request failed: Server responded with an error: 404 Not Found - Directory Not Found'] - PubkyRepo (Fix with AI)
 to.bitkit.utils.AppError: reason=Request failed: Server responded with an error: 404 Not Found - Directory Not Found
 	at to.bitkit.async.ServiceQueue$background$2.invokeSuspend(ServiceQueue.kt:42)
 	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
 	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:98)
 	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1154)
 	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:652)
 	at java.lang.Thread.run(Thread.java:1564)
 Caused by: com.synonym.bitkitcore.PubkyException$FetchFailed: reason=Request failed: Server responded with an error: 404 Not Found - Directory Not Found

@ovitrif
Copy link
Copy Markdown
Collaborator

ovitrif commented Apr 9, 2026

Review Pass 3: Remarks from testing vs. iOS

1. Profile Input on edit screen should use heavy font


2. Profile/ Contacts Links should be editable

Currently they look like like a disabled input field, should behave like editable inputs.

Requested in iOS review as well.

@piotr-iohk
Copy link
Copy Markdown
Collaborator

Create Profile flow testing

  • Create Profile
  • Edit / Delete Profile
  • Add / Edit / Delete Contacts

1. Create profile - OK.


2. Edit / Delete profile

  • "Disconnect" - should not be there (by Figma)
Screenshot 2026-04-10 at 10 48 27
  • "Delete profile" - says profile will be deleted permanently - but when you create profile again - it is the same pubky (not sure if that is expected).
Screen.Recording.2026-04-10.at.10.54.33.mov
  • After adding link we get back to edit profile and the keyboard becomes active - seems unnecesary.
Screen.Recording.2026-04-10.at.11.05.22.mov
  • Keyboard can cover the edit field.
Screen.Recording.2026-04-10.at.11.07.26.mov

3. Add contact

  • Empty contact list should show profile and "Add contact" button (Figma)
  • you can add yourself as contact - should not be possible. (both iOS and Android)
Screen.Recording.2026-04-10.at.11.34.19.mov
  • Adding new contact:
    • note: Input length looks unbounded; worth defining and enforcing a sensible maximum.
    • whoops! contact added on fake pubky:
Screen.Recording.2026-04-10.at.12.00.12.mov

4. Edit / Delete contact

  • edit contact shows "Your Pubky" - should show "Pubky" (both iOS and Android)
Screenshot 2026-04-10 at 11 41 55
  • "Please note..." sentence needs to be updated.
  • Cannot edit / update avatar - probably should be possible.

@ben-kaufman
Copy link
Copy Markdown
Contributor Author

Same pubky after delete is expected since we use deterministic derivation based on the seed. So that's the expected behavior there.

@piotr-iohk
Copy link
Copy Markdown
Collaborator

ame pubky after delete is expected since we use deterministic derivation based on the seed. So that's the expected behavior there.

I suppose this needs to be updated then:

Screenshot 2026-04-10 at 12 23 50

@ben-kaufman
Copy link
Copy Markdown
Contributor Author

No I think the warning makes sense, it deletes the profile, but not the key to it, maybe can remove the word "permenantly"

@piotr-iohk
Copy link
Copy Markdown
Collaborator

No I think the warning makes sense, it deletes the profile, but not the key to it, maybe can remove the word "permenantly"

Hm, it's also not like the profile is entirely deleted. From what I see only profile details (bio, links, tags) are deleted but the contacts are preserved.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ben-kaufman and others added 2 commits April 10, 2026 15:12
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ben-kaufman
Copy link
Copy Markdown
Contributor Author

@piotr-iohk ah yes I missed that, fixed

@piotr-iohk
Copy link
Copy Markdown
Collaborator

Additional observation (on 1e38929):

1.

  • create wallet -> create profile -> make backup -> reset wallet -> restore wallet.

Expected:

  • profile should be there after restoration

Actual:

  • profile is "disconnected"

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. 🙏

@ben-kaufman
Copy link
Copy Markdown
Contributor Author

About 1, agree it makes sense, but backup and restore should ideally be done in a separate PR since this is already too large and they are a separate area of work.

Not sure I understood what you refer to in 2, can you please explain?

@piotr-iohk
Copy link
Copy Markdown
Collaborator

Sorry - I've added video on iOS for 2nd point: synonymdev/bitkit-ios#476 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants