Skip to content

feat: support pubky contact deep links - #768

Merged
jvsena42 merged 5 commits into
masterfrom
codex/pubky-contact-deeplink
Sep 23, 2026
Merged

jvsena42 merged 5 commits into
masterfrom
codex/pubky-contact-deeplink

Conversation

@ben-kaufman

@ben-kaufman ben-kaufman commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

This PR adds bitkit://contact?pubky=<public-key> links that open the existing Pubky scanner contact flow.

Android counterpart: synonymdev/bitkit-android#1320

Description

  • Opens Add Contact with an unknown key prefilled, Contact Detail for a saved contact, or Profile for the wallet's own key, using existing scanner routing and receiver refresh.
  • Retains contact links until wallet unlock, Pubky initialization and contact loading finish, without waiting for Lightning to start.
  • Validates the URL and key before routing, rejecting missing, duplicate, overlong and non-key payloads.
  • Documents the link format and adds the matching cross-platform contact-link journey.

Example:

bitkit://contact?pubky=pubky3rsduhcxpw74snwyct86m38c63j3pq8x4ycqikxg64roik8yw5xg

The value may be a raw 52-character key or include the pubky prefix. URL-encode the value. Existing Paykit feature gating is unchanged.

Out of Scope

  • Contact management: no automatic save and no new screens.
  • Payments and authorization: opening the link neither pays nor approves an auth request; no SDK changes.
  • Generic screen/sheet deep links and universal links.

Design

N/A — no UI changes. Reuses existing Add Contact, Contact Detail and Profile screens.

Preview

N/A — existing scanner screens are unchanged.

QA Notes

Manual Tests

  • 1. Paykit-enabled wallet → open the link with an unsaved key: Add Contact opens with the key prefilled; nothing is saved automatically.
  • 2. Open with a saved contact's key → Contact Detail: existing contact is shown rather than Add Contact or Send.
  • 3. Open with the wallet's own key → Profile: no self-contact is created.
  • 4. Terminate the app with PIN enabled → open a saved-contact link → unlock: Contact Detail opens once after identity and contacts load.
  • 5. Open missing, duplicate or invalid pubky parameters: no contact, payment or auth flow opens.
  • 6. regression: scan a Pubky key or payment QR: existing scanner behavior is unchanged.

Automated Checks

  • PubkyContactLinkTests.swift: accepted key forms, malformed links and non-key payloads, retained unlock/contact-readiness gate, no Lightning dependency and scanner routing.
  • Existing ContactsManagerTests.swift and PubkyAuthURLSchemeTests.swift cover known/unknown/own-key routing, feature gating and auth/payment deep-link regressions.
  • Simulator build and CI-style unit selection passed: 1,305 tests, zero failures. Uses iPhone 17 Pro / iOS 26.1, ad-hoc signing and the repository's six live-integration exclusions.
  • Scoped SwiftFormat, whitespace and journey XML checks passed. The manual journey is documented but was not driven on-device.

@greptile-apps

greptile-apps Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

The PR should not merge until valid contact links are preserved or deliberately recoverable when Pubky initialization or contact loading fails.

Findings

  1. P1 Errors consume valid links ▶

Summary

This PR introduces validated bitkit://contact?pubky=… deep links and routes them through the existing Pubky scanner flow without waiting for Lightning.

  • Adds strict contact-link parsing and Pubky-key normalization.
  • Retains contact links through wallet unlock and normal Pubky/contact loading.
  • Reuses existing routing for Profile, Contact Detail, and Add Contact.
  • Adds unit coverage, documentation, changelog content, and a cross-platform journey.
  • The readiness error paths currently consume valid links before recovery can complete.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Receive bitkit contact URL] --> B[Retain pending URL]
    B --> C{Wallet unlocked?}
    C -- No --> B
    C -- Yes --> D{Pubky and contacts ready?}
    D -- Loading --> B
    D -- Ready --> E[Clear pending URL]
    E --> F[Validate and normalize key]
    F --> G[Existing scanner routing]
    G --> H{Key ownership}
    H -- Own key --> I[Profile]
    H -- Saved contact --> J[Contact Detail]
    H -- Unknown key --> K[Add Contact]
    D -- Initialization or load error --> E
    E --> L[Handler rejects state]
    L --> M[Valid link is lost]
Loading

Reviews (1) · Last reviewed commit: "feat: support pubky contact deep links"

Comment thread Bitkit/MainNavView.swift Outdated

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ✅ Approve


Review: diff 8 files.
Counterpart synonymdev/bitkit-android#1320: differs only in bounding the readiness wait with a timeout; this one waits indefinitely.

Findings:
N/A

Audit:
Skipped - nothing a reviewer would report across 8 files (threshold 0.4; strongest Bitkit/MainNavView.swift at 0.35).

Coverage:
QA: journeys and manual tests await all reviewers to approve, author can run it now via comment: @ovi-reviewer test


Reviewed by claude-opus-5-xhigh via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest · audit (author or owner)

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No HIGH or MEDIUM findings. Two LOWs inline, no verifier pass; read them as observations.

Checked and clean:

  • No state change from the link. Routing only navigates (profile, contact detail, add contact). Save and pay stay button-driven, and refreshContactReceiverPaths only acts on saved contacts.
  • Parser. The charset is an allowlist with a fixed 52-char key, and count <= 57 is checked before normalization. Path, fragment, userinfo, port and extra or duplicate params are rejected. The negative vectors match Android's.
  • Precedence. A contact link never waits on LDK. A malformed one is rejected without waiting on Pubky readiness.
  • Locked app. MainNavView isn't mounted before isPinVerified, so the link waits and routes exactly once after unlock.
  • Logging and sign-out. sanitizedDeeplinkDescription strips the query before logging. Sign-out mid-wait recomputes readiness, and nothing is attributed to another identity.
  • Journey. The file name, journey name and all 15 actions are byte-identical to Android's; only the platform launch command in <description> differs.

Comment thread Bitkit/MainNavView.swift Outdated
Comment thread Bitkit/MainNavView.swift Outdated

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ✅ Approve


Reaudit: diff 2 files.
Counterpart synonymdev/bitkit-android#1320 still bounds the readiness wait with a timeout; this delta does not change that.

Findings:
N/A

Audit:
Already done in comment.

Coverage:
QA: journeys and manual tests await all reviewers to approve, author can run it now via comment: @ovi-reviewer test


Reviewed by claude-opus-5-xhigh via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest · audit (author or owner)

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Delta since 92935754 (2e58180a): no findings. Both LOWs are fixed. isContactDeepLinkReady moved out of the .task id into its own .onChange, which spawns an unstructured Task, so contacts finishing their load no longer cancel an in-flight handler. With Paykit off, pubkyContactPublicKeyForRouting returns nil before parsing, so the link is dropped silently. With Paykit on, a malformed link still throws.

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ⛔️ Request Changes


Tests for the review: 3 of 6 manual tests passed.

QA:
Tested on iOS 26.5 simulator (iPhone 17 Pro)

Test 1 ✅ passed

evidence
1.mp4

Test 2 ⛔️ failed: Saved contact QA724PIN, relaunched, and reopened its bitkit://contact link. It showed Add Contact with "RETRIEVING CONTACT INFO" instead of Contact Detail.

evidence
2.mp4

Test 3 ✅ passed

evidence
3.mp4

Test 4 ⛔️ failed: Enabled PIN 1111, terminated the app, and opened the QA724PIN contact link while locked. After unlock it again showed Add Contact instead of Contact Detail.

evidence
4.mp4

Test 5 ✅ passed

evidence
5.mp4

Test 6 ⛔️ failed: Generated a QR for a valid Pubky key, added it to Photos, opened Scan, and picked it from the photo picker; the picker dismissed without opening Add Contact. A second attempt with a higher-error-correction QR image had the same result.

evidence
6.mp4

Tip

Worth a journey

Test 1
  • Enable Paykit UI in Settings → Advanced → Dev Settings
  • Return to Wallet
  • Open a contact deep link for an unsaved Pubky key
  • Verify Add Contact shows the key and Save action
  • Verify nothing is saved before tapping Save
Test 3
  • Create and activate a Pubky profile
  • Copy the wallet's own Pubky key
  • Return to Wallet
  • Open a contact deep link for the own key
  • Verify Profile opens with the same Pubky key
  • Verify Add Contact does not open
Test 5
  • Return to Wallet
  • Open a contact link without a Pubky parameter
  • Verify the error clears back to Wallet without another flow
  • Open a contact link with duplicate Pubky parameters
  • Verify the error clears back to Wallet without another flow
  • Open a contact link with an invalid Pubky parameter
  • Verify the error clears back to Wallet without another flow

Reviewed by gpt-5.6-sol-high via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest · audit (author or owner)

@ben-kaufman

Copy link
Copy Markdown
Contributor Author

Saved contact links now wait for local contact records to load even when the wallet has no active Pubky identity, so the relaunch and PIN-unlock cases route to Contact Detail. The photo-picker result stopped in Vision with No QR Code Found before Pubky routing, and this PR does not change image decoding.

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ⛔️ Request Changes


Reaudit: diff 2 files.
New findings: 1 inline (1 blocking); the rest is in the review.
Retest suggested: Tests 2, 4 (this change affects saved-contact routing and cold-start replay).
Counterpart synonymdev/bitkit-android#1320: diverges, see the parity finding.

Coverage:
Unit tests: 55% - PubkyContactLinkTests covers the readiness gate but not preload failure and recovery.
QA: journeys and manual tests await green CI checks


Reviewed by gpt-5.6-sol-high via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest · audit (author or owner)

@ben-kaufman

Copy link
Copy Markdown
Contributor Author

Cold-start contact links now wait for the initial contact load and retry after a failure before routing. Contact preparation no longer depends on Lightning node state, and the link stays queued if recovery still fails.

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Advice: ✅ Approve


Reaudit: diff 3 files.
No new findings; the rest is in the review.
Retest suggested: Tests 1-4 (it now preloads contacts for every Paykit wallet before routing a contact link, including the cold-start case).
Counterpart synonymdev/bitkit-android#1320: equivalent.


Reviewed by claude-opus-5-5-xhigh via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest · audit (author or owner)

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Delta since 2e58180a (9bf0252be, a0a768425): no findings.

  • The pending link now preloads contacts before routing, and canRoutePubkyContactLink requires hasLoadedContacts whenever Paykit UI is on, so a saved contact is never routed from an empty list.
  • isPreparingPendingContactDeepLink keeps one preload in flight. A second call falls through to routing, which is not ready yet and leaves the URL pending.
  • loadContactsIfNeeded now checks cancellation inside the isLoading wait, so a cancelled task cannot spin on the publisher.
  • The node-running trigger is split into its own .task, which skips contact links, so LDK coming up no longer cancels an in-flight contact preload.
  • Nit, not a finding: loadContactsForPendingDeepLinkIfNeeded falls back to the link's own key as contactsOwnerPublicKey. loadContacts(for:) only logs that value and reads records from the SDK, so the effect is a log line naming the counterparty as the owner.

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Passed on the 6 manual tests

@jvsena42
jvsena42 enabled auto-merge September 23, 2026 11:25
@jvsena42
jvsena42 merged commit aae7ccc into master Sep 23, 2026
13 checks passed
@jvsena42
jvsena42 deleted the codex/pubky-contact-deeplink branch September 23, 2026 11:25
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.

2 participants