feat: support Pubky signup - #724
Conversation
Greptile SummaryThis PR adds Pubky Ring signup URL parsing, wallet-derived identity registration, authorization approval, session activation, and resumable profile setup through the existing scanner and approval UI.
Confidence Score: 3/5The PR should not merge until Ring signup can recover from intermediate failures and users can leave pending profile setup without being immediately redirected back. The new flow can strand a remotely registered identity when approval or sign-in fails, and its navigation observer creates a repeatable Create Profile trap while setup remains pending. Files Needing Attention: Bitkit/Managers/PubkyProfileManager.swift, Bitkit/MainNavView.swift
|
| Filename | Overview |
|---|---|
| Bitkit/Managers/PubkyProfileManager.swift | Adds Ring signup and pending-profile state, but the signup sequence is not recoverable after an intermediate failure. |
| Bitkit/MainNavView.swift | Adds automatic profile-setup resumption, but route-driven reevaluation prevents users from leaving the setup screen. |
| Bitkit/Models/PubkyAuthRequest.swift | Adds strict parsing and validation for Ring signup parameters and reconstructs the corresponding authorization URL. |
| Bitkit/ViewModels/AppViewModel.swift | Routes Ring signup requests through the existing scanner while preserving restricted payment-flow state. |
| Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift | Extends the approval sheet to execute signup and transition into profile setup, with partial-failure recovery delegated to the manager. |
| Bitkit/Services/PubkyService.swift | Adds low-level registration and Ring authorization operations used by the new signup sequence. |
Sequence Diagram
sequenceDiagram
participant R as Pubky Ring request
participant B as Bitkit scanner
participant H as Homeserver
participant A as Auth relay
participant P as Profile setup
R->>B: pubkyring://signup
B->>B: Parse and show approval
B->>H: Register wallet-derived identity
H-->>B: Registration complete
B->>A: Approve authorization
A-->>B: Authorization complete
B->>H: Sign in
H-->>B: Active session
B->>P: Navigate to Create Profile
Reviews (1): Last reviewed commit: "feat: support Pubky Ring signup" | Re-trigger Greptile
|
Regtest device QA, home Scan, QR from staging.pubky.app. Staging e2e doesn’t finish — is that expected? No spinner after scan. Scanner stays up after already-signed-in / invalid auth. bitkit_logs_2026-09-03_09-45-08-ios.zip ScreenRecording_09-03-2026_11-38-24_ios-compressed.mp4 |
|
@piotr-iohk Thanks for the device QA and logs. You found a real signup interop bug: Bitkit treated every The ordinary sign-in QR rejection is separate. These PRs use Paykit rc50’s app-scoped grant auth model, while staging Pubky App is still generating the older auth request format. Pubky App needs to update its sign-in flow to the new grant model for ordinary sign-in to work with Bitkit. Could you please recheck the staging signup path on this head? |
91555ae to
436ee06
Compare
|
Retested the rebased head on a physical iPhone 13 using the regtest build and a signup QR from staging.pubky.app. Signup now completes end to end: Bitkit shows progress while processing the request, creates the Pubky identity, opens profile setup, and the staging website continues successfully. Ordinary sign-in still fails because staging currently generates the older non-grant authorization request. As clarified, that is outside the scope of this signup PR. The original signup interoperability and missing-progress issues are resolved for me. bitkit_logs_2026-09-03_14-36-23-ios.zip ScreenRecording_09-03-2026.16-31-36_1-ios.MP4 |
436ee06 to
4eedebb
Compare
692eccc to
0ae29a6
Compare
0cdee24 to
2777d79
Compare
5c008e2 to
6f836b7
Compare
|
Restacked onto the updated #697 head ( |
6f836b7 to
9981476
Compare
There was a problem hiding this comment.
Reviewed after confirming the same hole on the Android twin (synonymdev/bitkit-android#1224). iOS has the same shape with a smaller blast radius — details inline. Non-blocking, since Paykit UI is behind PaykitFeatureFlags.isUIEnabled (AppViewModel.swift:507), which needs the paykitUiEnabled opt-in that only Dev Settings writes, and showDevSettings defaults to Env.isDebug.
Two things that further reduce the risk versus Android:
- No deeplink vector.
Info.plistregisters onlybitkit, bitcoin, lightning, lnurl*underCFBundleURLTypes;pubkyauthappears only underLSApplicationQueriesSchemes, which is outbound-only, and there are no associated domains inBitkit.entitlements. So iOS handspubkyauth://to Pubky Ring and a malicious web link cannot reach Bitkit. On Android the equivalent link did reach the handler, before the wallet-exists guard. - The reachable vectors are in-app QR scan (
ScannerManager.swift:82) and clipboard-redirect (MainNavView.swift:291-293→ alert at:412-424→processClipboardUri→:707), both requiring the app foregrounded and unlocked.
Still worth closing before the flag flips on: a consent-and-local-auth gate exists on one signup path and not the other, and the fix is a deletion rather than an addition.
Not re-raising: the register→approve→activate orphan / no-signIn-fallback question is already greptile's thread at PubkyProfileManager.swift:384 and ben answered it; the full-screen signup loader is the one piotr-iohk asked for and re-verified, and it disappears anyway if the branch below goes.
Key material is clean: the secret key hex is derived on a detached task, passed only to registerIdentity/approveRingAuth/activateBootstrapResult, and never logged (Logger.info emits only the public key). Signup requests force clientID = "", reject bitkitClaim, and reject duplicate query params.
Non-blocking: Paykit UI is dev-flag gated, so this is dev/QA-facing. Finding stands in the inline comment.
9981476 to
6c502f1
Compare
jvsena42
left a comment
There was a problem hiding this comment.
Re-reviewed at 6c502f1f. One MEDIUM inline — not blocking, because paykitUiEnabled defaults false and .profile is ComingSoonScreen otherwise. But it is worth closing before the flag flips, and it lands on the Android twin (#1224) identically.
Short version: this PR is the first way a seed-derived key can be registered on a homeserver other than Homegate, and that quietly removes the invariant that made createIdentity's existing Homegate signUp safe. The fallthrough code itself is unchanged from the base branch — I checked, so this is not a regression you introduced at those lines; it is pre-existing code whose precondition this feature removes.
Your fixes for the earlier threads are correct. The authorizationUrl == nil bypass is gone and the signup branch always routes through showSheet(.pubkyAuthApproval); performAuthorization is only reachable via onAuthorize() → resolvePubkyApprovalLocalAuthMode, so PIN/biometrics gate all three URL formats, and testSignupScannerRoutesRequireApproval pins that with no identity created. :262's let publicKey guard is right as far as it goes — my note below is about where the else-path goes, not about that condition.
Key material clean. Keys are seed-derived (Paykit.pubkySecretKeyFromBip39Mnemonic, index 0), held in memory during the flow, persisted only to Keychain under kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly — the same class as the mnemonic. Never logged (the info line emits the public key only), never in a URL or an error string, and no recovery blob is produced. approveSignupAuth double-guards publicKey == nil && !hasStoredIdentity() both before and after deriveKeys, so it cannot overwrite an existing identity, and there is no suspension point between activation success and the state writes.
Trust boundary clean. hs is z32-validated before display, the reconstructed relay URL is re-validated by BitkitCore.parsePubkyAuthUrl, clientID is forced empty, bitkitClaim is rejected, and duplicate query params are rejected. Contact-pushed Paykit requests carrying a pubky URL are now rejected before Scanner decode, which is an improvement over base. Only a new UserDefaults bool is added, so there is no migration exposure, and no sat-moving path is touched.
9e0707a to
951c102
Compare
jvsena42
left a comment
There was a problem hiding this comment.
Re-reviewed the delta at 951c10267. The homeserver re-homing is fixed — trace on the resolved thread. No HIGH/MEDIUM.
Scoping note so it isn't misread: the shouldDeferStaleSession narrowing that appears in this range is not part of this fix — it comes from the restacked parent (70c51fa8 refactor: remove paykit migration handling). I'm reviewing that separately on #697 and will report there.
One coverage caveat worth having on record, not a defect: the new tests exercise the extracted completeIdentityCreation with injected closures, so the production defaults — that loadStoredSecretKey reads .pubkySecretKey and not hasStoredIdentity() — are correct by reading rather than under test. That's precisely the distinction the fix turns on, so if that wiring is ever refactored, the suite won't catch a swap back.
jvsena42
left a comment
There was a problem hiding this comment.
Reviewed the delta at 2365306e. No HIGH/MEDIUM — clean refactor. I treated it as a behavioural review rather than a cosmetic one, since identity-setup wiring is where a "refactor" tends to hide a change.
Both properties I verified last pass still hold, and neither is actually touched — PubkyProfileManager.swift, PubkyAuthApprovalSheet.swift, PubkyAuthRequest.swift and PubkyService.swift are all untouched in this commit. completeIdentityCreation still guards on a non-empty stored secret via the production loader reading .pubkySecretKey (not hasStoredIdentity()), so the signIn path returns before the Homegate signUp is reachable; and a genuine first run with no key still falls through to signUp(). The session-only Ring login is unaffected because the guard never consults .paykitSession.
The consent and local-auth gate is intact. Every request.isSignup still routes through showSheet(.pubkyAuthApproval) with no authorizationUrl == nil bypass, and performAuthorization() has exactly three callers — all downstream of resolvePubkyApprovalLocalAuthMode, with the Authorize button calling onAuthorize() rather than reaching it directly. approveSignupAuth is only called from performAuthorization.
The resume logic is a move, not a rewrite. shouldResume(didResume:) reproduces the old inline guards exactly, the resolver is a verbatim copy of the previous computed property, didResumePendingPubkyProfileSetup is still @State, and initial: true is retained — so the once-per-lifetime-re-armed-only-when-pending-clears semantics survive. A stale UserDefaults flag with no session still resolves .waiting because .ready requires isAuthenticated, so it can't navigate for a wrong or absent identity.
The AppViewModel change removes a genuinely dead dependency — pubkyProfile had zero reads at the previous head, and handlePubkyAuthApproval already used the static hasStoredIdentity(). The ShopPaymentRequestTests edit is compile-follow for that removed ctor argument, not an assertion change, which is the thing I check for when a test moves during a refactor.
One scoping note on the new PendingProfileSetupResumeTests: its three cases are real and would catch a regression of the resume latch or the isAuthenticated/isPaykitUIActive gates, including the navigation-trap case. It would not catch a regression of the homeserver-preservation property — that lives in completeIdentityCreation and is covered by the two unchanged PubkyProfileManagerTests. Worth knowing which suite guards which if either is ever moved.
2365306 to
6646af9
Compare
Description
pubkyring://signupand auth-bearingpubkyauth://signuprequests, plus directpubkyauth://direct_signupand parameter-only legacypubkyauth://signup, through the normal scanner and deep-link flow.This PR is stacked on #697 and uses its Paykit rc51 authorization model. Ordinary Pubky App sign-in must use that grant-auth model; compatibility with the older sign-in request is intentionally outside this signup PR.
Linked Issues/Tasks
Depends on #697.
Screenshot / Video
Not included; this reuses the existing scanner, authorization approval sheet, loading treatment, and profile setup UI.
QA Notes
Validation:
PubkyProfileManagerTests.swiftcovers stored-key sign-in, credential-read/sign-in/profile failures and retries, cancellation-error propagation, and unchanged no-key signup/cleanup. Existing consent, activation and scanner regressions also pass.