Skip to content

feat: introduce AppRouter for centralised navigation - #213

Merged
raulriera merged 8 commits into
mainfrom
feat/app-router
Apr 28, 2026
Merged

feat: introduce AppRouter for centralised navigation#213
raulriera merged 8 commits into
mainfrom
feat/app-router

Conversation

@raulriera

@raulriera raulriera commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces ad-hoc per-screen navigation flags with a single observable router that owns per-stack paths and the top-level sheet presentation. Cross-stack jumps from deeplinks or push notifications now reliably swap sheets and replace paths, fixing the bug where a deeplink to a different token did nothing while the token info screen was already open.

Test plan

  • Open Wallet, tap a token, then deeplink to a different token; the screen swaps cleanly
  • Cold-launch from a token deeplink; app opens directly to that token's screen
  • From a deep Settings path, deeplink to a token; settings dismisses and wallet opens with the token; reopening settings restores the previous depth
  • Every Settings child screen pushes; no mix of push and sheet
  • Wallet > Discover Currencies pushes
  • Wallet > Discover Currencies > Create Currency > Get Started pushes into the wizard
  • Withdraw flow works: pick currency, enter amount, enter address, confirm
  • Navigation logs emit one structured line per interaction

Single @observable @mainactor router that owns per-stack navigation paths
and the top-level sheet presentation. Replaces ad-hoc per-screen @State
flags and bindings with a generic API: push, pop, popToRoot, popLast,
pushAny, setPath, present, dismissSheet, navigate.

Per-stack paths are NavigationPath (type-erased) so a single stack can
carry mixed Hashable types — Destination cases at the top level and
sub-flow Hashable cases (e.g. WithdrawNavigationPath) below — without
nested NavigationStacks. Cross-stack navigate(to:) presents the
destination's owning sheet and replaces the path; CurrencyInfoScreen
uses .id(mint) so leaf swaps rebuild with fresh @State, fixing the
deeplink-replace bug. Every mutator logs at INFO via flipcash.router.

- BalanceScreen, SettingsScreen, ScanScreen drive their stacks/sheets
  from the router; remove local navigation @State.
- DeepLinkController routes currency links through router.navigate;
  Session.pendingCurrencyInfoMint is gone.
- WithdrawScreen drops its inner NavigationStack and registers
  WithdrawNavigationPath destinations on the parent (Settings) stack;
  WithdrawViewModel pushes substeps through router callbacks.
- CurrencyDiscoveryScreen drops its inner NavigationStack and pushes
  through the router. CurrencyInfoScreen drops the metadata: init.
- Settings children all push: My Account, App Settings, Advanced
  Features, Beta, Account Selection, Application Logs, Access Key,
  Deposit, Withdraw — no more sheet/push mix.
- Replace 14+9+3 hand-rolled #expects in owningStack/sheet mapping
  tests with parameterized @test(arguments:) — each pair now runs as
  an independent test case in Xcode.
- Inline SettingsToggle into SettingsAppSettingsScreen — its single
  call site doesn't justify a standalone view.
- Add Navigation: AppRouter section under Architecture & Patterns
  describing the canonical navigation system, NavigationPath storage,
  no-nested-NavigationStack rule, and local-vs-routed sheets.
- Add two pitfalls: nested NavigationStack crashes with
  comparisonTypeMismatch; same-case leaf swaps need .id(value).
- Add navigation key files to Quick Reference.
- Update architecture diagram: Session is @observable, not
  ObservableObject (migrated previously).
- Drop Session from the legacy ObservableObject example list.
- Fix test naming convention: drop the XCTest-style test prefix
  since the project uses Swift Testing.
- Fix typo (indetify → identify).
Route the launch-cover dismiss through AppRouter so the whole balance
sheet collapses instead of popping the wizard back to the summary. Show
the bill after the sheet animation completes so it enters fresh on
ScanScreen instead of being revealed underneath the closing sheet.
Dragging a sheet down to dismiss left its NavigationPath populated, so
re-presenting the sheet (e.g. tapping Wallet again) restored the stale
leaf instead of starting at root. dismissSheet now clears the dismissed
stack's path. Sheet swaps still go through present(_:), which preserves
both stacks' paths for return trips.
Replaces the last local @State-driven navigations with router pushes so
the trace covers every nav action and deeplinks can reach these screens.
Adds .currencyInfoForDeposit, .give, and .deposit destinations and
surfaces the mint as its own metadata key in router logs.

Reworks dismissSheet to mark-and-clear-on-reopen instead of deferring
the path clear via Task — no more "push back, then dismiss" animation,
no timing dependency, no async tests. Drops the bidirectional sync
between giveViewModel.isPresented and router.presentedSheet that
desync'd on swipe-down and stalled the next tap of the give button.
Router navigation logs now cover the user-action trail; the per-event
Analytics debug line was duplicating that signal.
The local-vs-router rule needed a clearer tiebreaker for new screens, and
the dismiss/present clear-on-reopen contract wasn't documented anywhere
— easy for future contributors to add manual popToRoot calls around
their own dismissals and fight the router.
@raulriera
raulriera merged commit 11e6049 into main Apr 28, 2026
@raulriera
raulriera deleted the feat/app-router branch April 28, 2026 18:31
bmc08gt added a commit that referenced this pull request Aug 28, 2026
…694)

The launch screen is a fullScreenCover over the creation wizard, and its
`dismissParentContainer` was wired to `router.dismissSheet()`. The creation
flow is pushed onto the Wallet tab's stack, so there is no sheet to pop and
that call returns early — both exits from CurrencyLaunchProcessingScreen, the
finished handoff and the failure dismissal, did nothing. The screen hides the
back button and disables interactive dismiss, so a user whose launch bill was
claimed by someone else was left in the wizard with only its back chevron and
force-quit the app.

Pop the wizard's owning stack to root instead, matching the convert flow. The
stack is named rather than resolved through `popToRoot()`'s topmost lookup,
since HomeTabView clears `activeTabStack` while the cover is up.

The wiring was correct when it landed in #213, where `.balance` was sheet-hosted
and dismissing the sheet tore down the whole flow; #619 made `.balance`
tab-hosted and this call site was missed.
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.

1 participant