Skip to content

feat: add deep links to every screen - #1119

Merged
ovitrif merged 17 commits into
synonymdev:masterfrom
guzino:feat/deep-link-screens
Aug 4, 2026
Merged

feat: add deep links to every screen#1119
ovitrif merged 17 commits into
synonymdev:masterfrom
guzino:feat/deep-link-screens

Conversation

@guzino

@guzino guzino commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Fixes #659
Refs #1118
Refs #1126

This PR adds a bitkit://screen/... URI for every screen in the app, bottom sheets included, gated on dev mode.

Description

A destination declares whether it may be entered directly, in the model that defines it. 88 root routes are Routes.DeepLinkable and 15 are Routes.InternalOnly, and deepLinkableComposable accepts only the former, so a new screen has to make the choice rather than inherit one. ScreenDeepLinks adapts the URI and nothing else: it derives the id by kebab-casing the class name, so Routes.RgsServer is reachable at bitkit://screen/rgs-server. Arguments without a default become path segments, arguments with a default become query parameters.

Bottom sheets needed a second mechanism. They are not in the root graph: they are Sheet values rendered by SheetHost, each carrying the start route of its own nested NavHost, so NavController.handleDeepLink cannot reach them. Each sealed route family declares DeepLinkStart or InternalOnly on its states and owns a fromDeepLink lookup, and SheetDeepLinks picks the family and wraps the result. Ids derive from class names throughout, so bitkit://screen/widgets/price-edit comes from Sheet.Widgets plus WidgetsRoute.PriceEdit.

  • Registers 38 sheet paths across send, receive, backup, widgets, hardware and the three sheets with no nested graph.
  • Gates screen links on isDevModeEnabled in AppViewModel.handleDeeplinkIntent, and holds one until the wallet is loaded; ContentView replays it.
  • Detaches the screen URI from the Activity intent in MainActivity once the gated pipeline has read it, so graph creation cannot navigate outside the dev-mode gate.
  • Classifies the recovery phrase, auth, transfer and LNURL destinations as internal, so they yield no link at all. Links navigate and prefill only, and never send, broadcast or change a setting without the usual confirmation.
  • Leaves payment URIs (bitcoin:, lightning:, lnurl*) on the scanner decode path, untouched.
  • Dismisses an open sheet only once a root-screen URI has matched, so a rejected link leaves the visible screen alone.
  • Removes docs/deeplinks.md. The route markers, deepLinkableComposable, the sheet families' fromDeepLink and the unit tests are the source of truth, and a parallel doc drifted three times during review.
  • Keeps the 25 flow-only sheet states internal. None is tap-reachable, so none is a user-facing defect. Direct support for the late transfer screens is tracked in feat: deep link the late transfer screens #1126.

Not every nested state is a valid start destination. I fired all 51 against an emulator, then re-ran each rejected one in isolation against logcat. The states left internal fall into three groups:

  • SendRoute.FeeRate and FeeCustom sit inside navigationWithDefaultTransitions<SendRoute.FeeNav>. A nested graph's child cannot be a NavHost start destination, so both throw IllegalStateException: Cannot find startDestination ... from NavGraph. send/fee-nav resolves but renders only the screen title.
  • send/quick-pay throws on requireNotNull(quickPayData) (SendSheet.kt:309). send/confirm and the four receive confirm/liquidity routes do not crash but render empty or zero-amount screens. send/confirm offers "Swipe To Pay" over a payment that was never built.
  • backup/success reports a backup that never ran, and its OK button persists backupVerified = true (BackupNavSheetViewModel.onSuccessContinue). backup/warning is one tap upstream of the same write. hardware/searching waits forever because discovery starts from the intro's continue action, and hardware/paired claims a paired device over default state.

Tests pin the classification, and a final sweep confirmed the rejected paths no-op with the wallet overview intact and zero fatal exceptions.

Preview

N/A

QA Notes

Dev mode is on by default on debug builds (Settings ▸ Advanced ▸ Dev Settings). The app must be past onboarding.

Manual Tests

  • 1. adb shell am start -a android.intent.action.VIEW -d "bitkit://screen/settings" to.bitkit.dev → Settings opens. A mistyped id still resolves MainActivity, because the manifest accepts every bitkit: URI by scheme, so check logcat for Unhandled screen deeplink rather than relying on -W.
  • 2. Deep-linked screen → Back: returns to the wallet overview, not the launcher.
  • 3. bitkit://screen/send → Send sheet on the recipient picker. bitkit://screen/widgets/price-edit → Bitcoin Price editor.
  • 4. bitkit://screen/recovery-mnemonic and bitkit://screen/backup/show-mnemonic → screen unchanged, recovery phrase never shown, logcat carries Unhandled screen deeplink.
  • 5. bitkit://screen/send/fee-rate and bitkit://screen/backup/success → screen unchanged, no crash, and backupVerified is untouched.
  • 6. PIN screen up → fire any screen link: navigates behind the PIN, which is still required before anything is visible.
  • 7. Dev mode off → any bitkit://screen/... link is ignored, on warm start and on cold start. Settings ▸ Support, tap Version five times to toggle.
  • 8. regression: scan a bitcoin: / lightning: / lnurl URI → still decodes through the scanner path.

Automated Checks

  • Unit tests in ScreenDeepLinksTest.kt: id derivation, path vs query argument placement, and that every route declares its eligibility.
  • Unit tests in SheetDeepLinksTest.kt: bare-id defaults, sub-route selection, case-insensitive lookup, ids derived from the Sheet class names, and that every state marked as a start is registered in its family lookup.
  • Instrumented ScreenDeepLinkDetachmentTest: graph creation stays on Home after detachment, the captured URI reaches Settings only through the replay, and a denied route is not matched.
  • Two journeys under journeys/deeplinks/, including cold-start cases for dev mode off and on.
  • Fired all 38 sheet paths and the screen links against an API 33 emulator and confirmed each renders. The rejected paths no-op without crashing.
  • Local: just compile, just test, just lint all pass, no new detekt findings.

greptile-apps[bot]

This comment was marked as resolved.

@greptile-apps

This comment was marked as outdated.

@ovitrif ovitrif left a comment

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.

Thanks for addressing the existing backup and hardware findings; I confirmed both fixes on 511250c.

I found three blocking issues: cold-start links bypass the dev-mode gate, bitkit://screen/recovery-mode is consumed by the legacy parser before the gate, and external-confirm can reach invalid fresh state and crash. I also left two non-blocking corrections for Home registration and the adb -W guidance.

Comment thread app/src/main/java/to/bitkit/ui/MainActivity.kt Outdated
Comment thread app/src/main/java/to/bitkit/ui/utils/ScreenDeepLinks.kt Outdated
Comment thread app/src/main/java/to/bitkit/ui/utils/Transitions.kt
Comment thread docs/deeplinks.md Outdated
Comment thread app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated
@ovitrif ovitrif self-assigned this Jul 30, 2026

@ovitrif ovitrif left a comment

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.

Thanks for addressing the earlier findings. Three blocking issues remain:

  • Cold-start coverage is missing at the Activity/NavHost boundary.
  • Late transfer destinations allow entry without the flow state they require.
  • Root-screen links route behind an active sheet.

Comment thread app/src/main/java/to/bitkit/ui/utils/ScreenDeepLinks.kt Outdated
Comment thread app/src/main/java/to/bitkit/ui/ContentView.kt

@ovitrif ovitrif left a comment

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.

One blocking navigation regression remains: rejected screen links dismiss an active sheet before route handling determines that the URI is invalid.

Comment thread app/src/main/java/to/bitkit/ui/ContentView.kt Outdated

@ovitrif ovitrif left a comment

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.

The deep-link implementation currently splits navigation policy across the route models and parallel registries, so I came up with 4 required corrections to keep direct entry safe and make the route hierarchy the single source of truth, and fix the changelog:

  • A denied root-screen link dismisses an active sheet before rejection, so rejected input changes the visible flow.
  • Root-screen eligibility has a second owner in ScreenDeepLinks.DENIED, so new routes become reachable without an explicit decision and every route change must synchronize a parallel policy.
  • SheetDeepLinks.SHEETS correctly fails closed while duplicating which nested states may start a flow outside the sealed route families that define them.
  • The changelog is incorrect because it describes an internal QA workflow rather than the public developer capability available to people running their own builds.

Comment thread app/src/main/java/to/bitkit/ui/utils/ScreenDeepLinks.kt Outdated
Comment thread app/src/main/java/to/bitkit/ui/utils/SheetDeepLinks.kt Outdated
Comment thread changelog.d/next/1119.added.md Outdated
@guzino

guzino commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

The red e2e-status is not from this branch. e2e-tests-staging - pubky_paykit fails the same four contact specs on ContactViewName here and on #1123, which touches only channel-details time formatting. My #1111 passed that job on 29 Jul and ContactDetailScreen has not changed since May, so the break is on master or in the staging env rather than in either PR.

The rest is green: build, build-local, build-staging, detekt, and both local e2e specs.

@ovitrif ovitrif left a comment

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.

Thanks for addressing the earlier findings. One blocking issue and one docs cleanup remain:

  • ExternalConnection is still Routes.DeepLinkable but registered with composableWithDefaultTransitions, so bitkit://screen/external-connection lost its Nav deep link after that helper stopped auto-registering links.
  • docs/deeplinks.md no longer matches the fail-closed route-owned model and should be removed rather than kept as a second source of truth for this developer-oriented feature.

PS. We can ignore the red e2e failures, it's caused by a premature merge of a PR in the e2e repo targeting paykit-related PRs that are not yet into master. Classic PR stacks issues 🙃 .

Comment thread app/src/main/java/to/bitkit/ui/ContentView.kt
Comment thread docs/deeplinks.md Outdated

@ovitrif ovitrif left a comment

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.

The ExternalConnection deep link is restored and docs/deeplinks.md is removed; this looks good to merge.

@ovitrif
ovitrif merged commit 7fa641e into synonymdev:master Aug 4, 2026
18 checks passed
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.

Add deep link linking to every screen in the app

2 participants