Skip to content

feat: add deep links to every screen - #1119

Open
guzino wants to merge 10 commits into
synonymdev:masterfrom
guzino:feat/deep-link-screens
Open

feat: add deep links to every screen#1119
guzino wants to merge 10 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

Screens in the root graph register themselves. ScreenDeepLinks derives the id from the route type by kebab-casing its class name and composableWithDefaultTransitions attaches the link, so Routes.RgsServer is reachable at bitkit://screen/rgs-server and a new screen works as soon as it joins the graph. 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. SheetDeepLinks maps a path to a Sheet and ContentView hands it to showSheet before falling through to the nav graph. Paths are still derived from class names via the shared kebabId helper, so bitkit://screen/widgets/price-edit comes from Sheet.Widgets plus WidgetsRoute.PriceEdit. A bare sheet id opens its first registered route.

  • 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.
  • Denies RecoveryMnemonic, AuthCheck, LegacyRnRecovery, LnurlChannel, CriticalUpdate, RecoveryMode, the three External* routes and the six late transfer destinations, the backup/show-mnemonic and show-passphrase sheet routes, the pin / change-pin / disable-pin sheets, and force-transfer. 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 before a root-screen URI reaches NavController, while a sheet URI still replaces the open sheet.
  • Documents the URI shape, the reachable sheet routes and every exclusion in docs/deeplinks.md.
  • Keeps the thirteen unreachable sheet routes out of scope. None is tap-reachable, so none is a user-facing defect. Starting inside the FeeNav sub-graph, and letting the confirm and liquidity screens tolerate an empty flow, are follow-ups.

Sheet routes are registered by hand because not every route is a valid start destination. I fired all 51 against an emulator, then re-ran each rejected one in isolation against logcat. Thirteen are excluded, for three reasons:

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

A test pins all thirteen, and a final sweep confirmed they 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 denied route yields no link.
  • Unit tests in SheetDeepLinksTest.kt: bare-id defaults, sub-route selection, case-insensitive lookup, and that sensitive and mid-flow paths resolve to null rather than falling back to the sheet default.
  • 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

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown

Greptile Summary

Adds development-mode deep links for root navigation destinations and manually selected sheet routes.

  • Derives root-screen URI patterns from serializable route class names and arguments.
  • Queues incoming screen links until Compose navigation is available, then routes them to the root graph or sheet host.
  • Documents supported and excluded links and adds unit tests and emulator journeys.

Confidence Score: 4/5

The backup-success and hardware mid-flow links should be removed or made to establish their required state before this PR is merged.

Direct sheet entry can report a backup that never occurred or render hardware searching and paired states without starting discovery or establishing a paired device.

Files Needing Attention: app/src/main/java/to/bitkit/ui/utils/SheetDeepLinks.kt

Important Files Changed

Filename Overview
app/src/main/java/to/bitkit/ui/utils/SheetDeepLinks.kt Registers direct sheet entry points, including backup and hardware flow states that require work or initialization performed by earlier steps.
app/src/main/java/to/bitkit/ui/utils/ScreenDeepLinks.kt Derives root-route deep links and excludes an explicit set of sensitive routes.
app/src/main/java/to/bitkit/ui/ContentView.kt Replays queued links through either the sheet host or root NavController.
app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Gates screen links on development mode and stores one pending URI for UI consumption.
app/src/main/java/to/bitkit/ui/utils/Transitions.kt Makes generated screen links the default for root composable and nested-graph registrations.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A["bitkit://screen/... intent"] --> B["AppViewModel checks dev mode"]
  B -->|disabled| C["Ignore link"]
  B -->|enabled| D["pendingScreenDeepLink"]
  D --> E{"SheetDeepLinks match?"}
  E -->|yes| F["showSheet with nested start route"]
  E -->|no| G["NavController.handleDeepLink"]
Loading

Reviews (1): Last reviewed commit: "feat: add deep links to bottom sheet scr..." | Re-trigger Greptile

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

return@LaunchedEffect
}

if (shouldDismissSheetForScreenLink(uri, appViewModel.currentSheet.value)) {

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.

backup/show-mnemonic is denied by SheetDeepLinks, but it reaches this branch and hides the active backup intro before handleDeepLink reports the URI as unhandled. The new sheet journey therefore returns to the wallet overview instead of leaving the visible screen unchanged; any rejected screen URI can similarly dismiss the current sheet. Could we confirm that the URI matches a root destination before hiding the sheet and add regression coverage for a denied URI while a sheet is open?

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