feat: upgrade paykit to rc51 - #697
Conversation
Greptile SummaryThe PR upgrades Paykit to rc50 and adopts app-scoped Pubky grants with environment-stable Bitkit client IDs.
Confidence Score: 4/5The PR should not merge until failed grant revocation can leave the retained authenticated account's payment-sharing state intact for a safe retry. Normal sign-out deletes and persists endpoint state before attempting the operation allowed to fail, so the advertised failure recovery retains the identity but not its prior payment configuration. Files Needing Attention: Bitkit/Managers/PubkyProfileManager.swift
|
| Filename | Overview |
|---|---|
| Bitkit/Managers/PubkyProfileManager.swift | Coordinates the new revoke/forget lifecycle, but normal sign-out mutates payment state before revocation succeeds and can leave a retained account partially dismantled. |
| Bitkit/Services/PubkyService.swift | Adopts rc50 client-scoped bootstrap/session access and exposes explicit revoke and local-forget operations. |
| BitkitTests/PubkyProfileManagerTests.swift | Updates cancellation and backup-replacement tests, but does not cover normal sign-out when endpoint cleanup succeeds and revocation fails. |
| BitkitTests/PaykitSdkClientConfigTests.swift | Verifies the network-dependent stable Bitkit client ID and existing Pubky client configuration. |
| Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved | Resolves Paykit 0.1.0-rc50 at the updated revision. |
Sequence Diagram
sequenceDiagram
participant U as User
participant M as PubkyProfileManager
participant P as Paykit endpoint state
participant G as Pubky grant
U->>M: Sign out
M->>P: Remove private/public endpoints
P-->>M: Cleanup persisted
M->>G: Revoke Bitkit grant
G-->>M: Revocation error
M-->>U: Show error and remain authenticated
Note over U,P: Account remains active with payment sharing already dismantled
Reviews (1): Last reviewed commit: "feat: upgrade paykit auth to rc50" | Re-trigger Greptile
147778a to
efcf886
Compare
|
The failed unit job was caused by stale test URLs from before rc50: those fixtures still generated legacy |
piotr-iohk
left a comment
There was a problem hiding this comment.
e2e ACK.
Latest (3a0b0e9) with matching e2e branch codex/paykit-rc50-auth (#212). Recreated the two staging Paykit fixture pubkys. e2e-tests-staging - pubky_paykit green. Full CI green.
Manual on iPhone 17 sim: online Delete/Disconnect clears paykit_session. Offline Delete: transport_error, profile kept. Offline Disconnect from that dialog: endpoint cleanup WARN, no revoke-failure log, session still in keychain; next launch restored pubkyyc14…4rso. Offline Disconnect looked hung rather than a clean error + retry. Not a blocker.
Did not retest other-app grant stays authorized, or backup replace.
|
starting review |
jvsena42
left a comment
There was a problem hiding this comment.
Requesting changes: two high-severity issues below can leave a live Pubky grant or a published endpoint set behind, which is the opposite of what this PR sets out to guarantee.
Scope was origin/master...HEAD (17 files). The hardware-send / SwipeButton changes visible in a master...HEAD diff are already merged via #708 and out of scope.
Blocking
PrivatePaykitService+Contacts.swift:273— the.restoreSavedContactsretry retires itself without republishing whenever the contact list has not loaded yet, which is exactly the offline case it exists for.PubkyProfileManager.swift:528— a session persisted insidecompleteAuth()is never revoked when the call throws afterwards, and gets signed back in on the next launch.
Non-blocking, but worth resolving in this PR
PubkyProfileManager.swift:367—deleteProfile()can queue a republish for a profile that no longer exists.PubkyProfileManager.swift:965— a throwingforgetSessionAccess()aborts the whole backup restore (QA item 4).PubkyService.swift:1220— delete order plus the newtrycan strand a restorable session secret.PublicPaykitService.swift:104—.publishReceiverMarkeris unreachable; both new tests only assert that unreachable pair.
Verification. Cleared the stale .pcm cache (the documented paykitFFI.h modified-since-module symptom of an rc bump), then ran the paykit suites in the simulator: 150 tests, 0 failures. The only failures in a full run were AddressTypeIntegrationTests hitting blocktank regtest → 404, which needs the Docker stack and is unrelated.
Each finding was reproduced as a failing test locally, and each suggested fix + regression test was applied locally and confirmed to flip it green with the 150 still passing. Nothing is committed — the diffs are suggestions, not a branch. Exceptions: finding 5 is confirmed by source only (PaykitSdkSessionProvider is private, so its test needs the seam in that comment), and finding 6's test is a forward guard that already passes.
Findings 2 and 6 are windows the Android counterpart (synonymdev/bitkit-android#1200) closed in the same PR that iOS has not mirrored.
Checked and correct: the rc50 stale-session context string matches paykit-ffi/src/session.rs:372; AppError(message: "pubky_auth__invalid_request") resolves through errorDescription's t(message), so no raw key reaches the user (the Android analogue leaked English); the requester label is in a ScrollView with lineLimit(1) + .tail, so a 253-char client ID cannot push the trust warning out of view; approvalBootstrap passing the requester's client ID is required by approve_auth's validate_auth_url_client_id; forgetSessionAccess does clear activeAuthRequest.
🤖 Generated with Claude Code
|
One final self-review follow-up is in |
0cdee24 to
2777d79
Compare
There was a problem hiding this comment.
The grant-binding, revoke-on-sign-out and reconciliation work is sound, and every thread I opened earlier is fixed at head. Approval pinning is clean — AppViewModel.swift:805-806 puts the parsed request and the authUrl into one immutable PubkyAuthApprovalConfig, the sheet passes both together, and approvalBootstrap re-parses that same URL and fails closed unless approvedClientID == requestClientID. No TOCTOU.
One migration gap the review rounds haven't covered: rc48+ only accepts grant-backed sessions, pubky 0.11 still successfully restores the cookie-format secrets rc31/rc46 wrote, and the resulting error isn't the one the deferral predicate matches. Non-blocking given the dev-flag gating, but it leaves affected devices with a permanently broken Pubky tab until reinstall. Details inline.
Also replied on PubkyProfileManager.swift:553 — the fix I asked for there over-corrected and can now revoke a pre-existing identity on a cancelled sign-in.
One amendment to my earlier note: I wrote that the rc51 context string matches session.rs:372. That's right, but it's the check that misses the second context the SDK emits on the same path.
jvsena42
left a comment
There was a problem hiding this comment.
Re-reviewed the delta at 70c51fa8. One LOW inline — not blocking, dev/QA-facing.
Context for why I looked hard at this: I noted on #1200 that Paykit has never shipped, so upgrade concerns are dev/QA by construction. If that's what prompted dropping the migration handling, the reasoning holds for the grant-backed clause — but the client-ID clause isn't a migration condition, and on iOS it's still live. Android is genuinely safe here, because its flavors use applicationIdSuffix and get separate keychains; iOS ships one bundle id for both configurations.
The narrowed tests don't lose live coverage — testMissingSessionOrUnrelatedIdentityFailureIsNotDeferred still asserts the hasStoredSession: false case for the surviving context, so the dropped loop assertion was redundant for that one and moot for the removed two.
There was a problem hiding this comment.
QA Notes
Tested on an iPhone 17 simulator (iOS 26.5), built and installed from this head. Verified the running binary carried this branch's session-cleanup log strings before driving anything.
Ran the Paykit profile lifecycle end to end: created a wallet, enabled the Paykit UI, created a Pubky profile with Bitkit (a real identity against homegate.staging.pubky.app), then deleted it. The app returned to the disconnected profile choice screen with no error toast, no stuck spinner, and no leftover public or private publishing flags — the state the receiver-marker fix protects. Tapping Import with Pubky Ring without the Ring app installed surfaced the "Pubky Ring Not Installed" dialog and left both buttons enabled afterwards, so the reworked cancellation path on that screen recovers cleanly.
Manual Tests 1-5 were not run. Each needs either the Pubky Ring app, which has no iOS Simulator build, or two privately linked wallets on a regtest backend, so they need a device pair rather than this machine.
Approve.
The private-only receiver marker issue from the last round is fixed. syncLocalReceiverMarker() now takes no arguments and reads both stored publishing flags, so isDiscoverable stays true for a user with public sharing off and private sharing on, and the new (false, true) reconciliation case pins it.
The session cleanup rework holds up. completeAuth returning the activated session secret means the discard path only ever fires for a secret that was actually persisted, and comparing it against the stored keychain session before revoking is what stops a newer session being torn down by a late cancellation.
On the open thread about dropping the Pubky grant client ID matcher: I do not think the flavour-switch case it describes is reachable. Env.keychainGroup appends the network name to the access group and both Keychain.load and Keychain.save pin kSecAttrAccessGroup, so a Debug regtest build reads KYH47R284B.to.bitkit.regtest and cannot load a mainnet TestFlight session. clientID is derived from the same Env.network, so the stored client ID and the expected one always move together. Separately, both removed matchers were introduced and removed inside this PR, so no build outside it ever persisted a session that depended on them.
—
Reviewed by Claude Code (claude-opus-5 high) via gh-pr-review-loop skill
70c51fa to
9d10c44
Compare
There was a problem hiding this comment.
The rebase onto master looks clean except for one line, and that line changes behaviour master had deliberately settled.
PrivatePaykitService+Contacts.swift:60 brings back the error return on the deferral branch that 06901046 removed. That commit removed it and flipped ContactPaymentsService.enable to pass requireImmediatePublication: true as a single change, so with the return restored and the caller unchanged, enabling Pay Contacts now throws whenever private publication is merely deferred — including the everyday case of the Lightning node still starting up. Public endpoint publication is skipped and the user sees an error toast where master defers quietly. Details inline.
The rest of the PR holds up. I re-checked the two points raised earlier in this discussion and both are sound: Env.keychainGroup appends the network name and Keychain.load/save pin kSecAttrAccessGroup, so a Debug build cannot load a mainnet session, and clientID derives from the same Env.network, so the stored and expected client IDs always move together. The rc51 pin matches the upstream tag.
Notes
- The
### Screenshot / Videosection saysN/A — no visual changes, butPubkyAuthApprovalSheet.swiftadds a visibleRequester ID: {clientId}row above the permissions list and retimes the spacing around it, movingdescriptionTextfrom.padding(.bottom, 32)to.padding(.bottom, 8). That note tells reviewers and QA to skip the approval sheet, so the new row and its truncation behaviour with a long client ID go unchecked. Could we swap the N/A for a screenshot of the sheet showing the new row?
Coverage
Total: 37%
- Journeys: 40% - Sign-out and cancellation flows are covered only as extracted helpers in unit tests; the enable-contact-payments path has no end-to-end coverage.
- Unit tests: 70% - The Pubky and Paykit suites pin reconciliation, client-ID binding and session teardown, but no test exercises
enableagainst a deferral. - QA: 0% - Manual Tests not run.
—
Reviewed by Claude Code (claude-opus-5 high) via gh-pr-review-loop skill
| Logger.info("Deferring private Paykit endpoint publication during prepare: \(reason)", context: "PrivatePaykitService") | ||
| await prepareLinks(publicKeys) | ||
| return nil | ||
| return requireImmediatePublication && !publicKeys.isEmpty ? PrivatePaykitError.privateUnavailable : nil |
There was a problem hiding this comment.
This line restores requireImmediatePublication && !publicKeys.isEmpty ? PrivatePaykitError.privateUnavailable : nil on the deferral branch, but master removed exactly that return in 06901046, and in the same commit flipped ContactPaymentsService.enable to call preparePrivateEndpoints(contactPublicKeys, true). Those two edits were one change: the caller was allowed to pass true precisely because the deferral branch had stopped turning that flag into an error.
With the return restored and the caller still passing true (ContactPaymentsService.swift:126-133), enable now throws whenever privateEndpointPublicationUnavailabilityReason is non-nil. That includes the ordinary case of the Lightning node still starting (PrivatePaykitService+Invoices.swift:96), so turning on Pay Contacts shortly after launch skips syncPublicEndpoints(true) entirely, restore rolls the preference back, and the user gets an error toast instead of the quiet deferral master ships.
testPreparingSavedContactsReportsUnavailableImmediatePublication pins the restored behaviour rather than catching it, so the suite now encodes the reverted semantics; ContactPaymentsServiceTests' OperationsSpy always returns nil, so no test exercises enable against a deferral.
Could we leave this branch returning nil and give retryPendingEndpointReconciliation its own privateEndpointPublicationUnavailabilityReason check before it clears the pending flag, so the retry keeps the signal it needs without changing what enable sees?
There was a problem hiding this comment.
Restored quiet deferral when enabling Pay Contacts. Sign-out reconciliation now checks publication availability and uses the strict refresh path, so it keeps recovery pending until the endpoints are restored.


Fixes #723
This PR:
0.1.0-rc46to0.1.0-rc51.Description
Bitkit now identifies itself as
bitkit.toon mainnet andstaging.bitkit.toelsewhere. Normal sign-out remotely revokes only Bitkit's current grant. If revocation cannot be confirmed, the profile and private Paykit state remain available so the user can retry.Completed Ring authentication that is later canceled, and identity creation that fails after activating a session, also attempt secure revocation. Explicit app reset and backup replacement forget the local session.
Paykit has not launched, so migration from earlier development builds is intentionally unsupported. There is no upgrade handling for cookie-backed sessions or grants using old client IDs. Normal recovery of current-format grant sessions remains supported.
Paykit rc51 also fixes replay ordering when incoming and outgoing Payment Request lifecycles interleave. Each request's events are derived in the correct order, preserving its payer/payee role so the existing history mapping retains the row. The fix is in paykit-rs #151 and is available in the published release before that PR merges.
Linked Issues/Tasks
Screenshot / Video
N/A — no visual changes.
QA Notes
Manual Tests
regression:restore a wallet backup with different Pubky state: the previous local session is forgotten and the backup identity is installed.regression:two privately linked wallets → each creates a 1,000-sat Lightning request and a 25,000-sat on-chain request → the peer pays all four → Payment Requests: both wallets retain all four rows after refresh and app restart.Automated Checks
PubkyProfileManagerTests.swift: covers canceled completed authentication revocation and backup session replacement.PaykitSdkClientConfigTests.swift: covers the stable Bitkit client ID and Pubky client configuration.git diff --checkpassed.