feat(widget): add support for navigation tabs (internal)#775
Conversation
🦋 Changeset detectedLatest commit: 1b581b5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
E2E Examples — all passedAll examples passed in the latest run. |
E2E Playground resultsDetails
📥 Download full HTML report (open the run → Artifacts → |
✅ E2E Dev Smoke — passing
4 passed · 0 failed · 0 skipped · 23s |
| ], | ||
| variant: 'wide', | ||
| // mode: 'split', | ||
| // _navigationTabs: ['default', 'private', 'refuel'], // ['swap-advanced', 'bridge-advanced', 'limit'] |
There was a problem hiding this comment.
For testing: Uncomment to get a view with tabs
🔍 QA Review — EMB-451ℹ️ Re-review — this is a follow-up to the 2026-06-19 review (6 items flagged). The developer addressed all items on 2026-06-22 and rebased on 🧠 What this ticket doesThis PR replaces the old
📋 Ticket SummaryAdd support for tabs passed from Simple and Advanced Jumper modes The ticket describes adding config-driven header tabs for "Simple" and "Advanced" Jumper tiers. The PR delivers the foundational infrastructure: Acceptance Criteria:
🏷️ PR Naming — ❌ Fail
The project's expected format is 🔎 Ticket Discoverability — ✅ Pass
✅ Ticket Coverage — HighThe foundational infrastructure for config-driven navigation tabs is fully delivered and self-consistent: store, presets, label resolution, component wiring, type definitions, changeset, and i18n keys. The tab-switching flow and split-mode backwards compatibility are verified. Coverage is High: the core utility logic now has direct unit test coverage ( 🔁 Re-review resolution status
|
| # | Severity | Type | Issue |
|---|---|---|---|
| 1 | 🟢 Low | UX edge case | HeaderNavigationTabs renders a single non-switchable tab when _navigationTabs has exactly one entry |
🟢 [Low] Single-tab _navigationTabs renders an un-switchable tab bar
packages/widget/src/components/Header/HeaderNavigationTabs.tsx
When config._navigationTabs is ['default'] (one entry), tabs.length is 1 and activeTab is defined, so HeaderNavigationTabs renders a single tab. The user sees a tab bar with no switchable destination. The guard is if (!tabs.length || !activeTab) return null — a tabs.length < 2 check would suppress the tab bar for degenerate single-tab configs.
Since _navigationTabs is @internal and controlled entirely by the Jumper integration layer, this is unlikely to occur in production. Noting for completeness.
Suggestion: Add if (tabs.length < 2 || !activeTab) return null or document that single-entry _navigationTabs configs are unsupported.
🔎 Fix code analysis (2B checks)
utils.test.ts (new):
Tests are pure-function, node-env compatible, and follow the createSettingsStore.test.ts pattern. Assertions verify the correct return values for each conditional branch. No tautological assertions (toBeDefined(), toBeGreaterThan(0)). One minor gap: getNavigationTabKeys(config({ _navigationTabs: [] })) (empty array) is not tested — the function relies on .length truthy check and would return [], which is the same behavior as a non-split default mode. Not blocking.
useNavigationTabLabel.ts default case (modified):
The default: return t('header.swapAndBridge') fix is functionally correct. It prevents undefined labels at runtime for any future NavigationTabKey extension. A TypeScript exhaustiveness guard (key satisfies never) would be stronger and provide a compile-time catch, but the current fallback is an acceptable and safer approach for a rapidly evolving @internal feature. No regression introduced.
AppLayout.tsx (TODO removed):
No functional code change — the TODO comment was removed. The design decision (integrator configures narrow-screen layout via theming) is documented in the PR description. The NavigationTabs.tsx flex: 'none' / whiteSpace: 'nowrap' / minWidth: 'auto' changes ensure individual tabs don't collapse. ✅
React 19 rebase:
use(NavigationTabsStoreContext) in useNavigationTabsStore.tsx and use(WidgetContext) in WidgetProvider.tsx are consistent with the repo-wide React 19 migration. WidgetContext is now explicitly exported with a Context<WidgetContextProps> type annotation (required for isolatedDeclarations compliance and for useNavigationTabsStore.tsx to re-provide it). This export is not visible via @lifi/widget's public index.ts. ✅
🧪 Test Coverage
| Layer | Score | Files reviewed |
|---|---|---|
| Unit (Vitest) | Partial | packages/widget/src/stores/navigationTabs/utils.test.ts — added ✅; useNavigationTabsStore.test.tsx and useNavigationTabLabel.test.ts absent (deferred to follow-up) |
| E2e (Playwright) | Partial | 158 tests passing in CI (split-mode regression covered); no new spec for _navigationTabs-driven tab switching |
ℹ️ e2e/ folder present — Playwright coverage will apply once the suite is active. The existing playground specs adequately cover mode: 'split' regression via the Swap/Bridge tab strip tests.
Unit gaps (deferred, tracked below):
[High → deferred]useNavigationTabsStore/useSplitMode— provider recreation,tabConfigoverride, split-mode derivation. Requires RTL/jsdom setup. Developer response: accepted with technical justification.⚠️ Follow-up ticket recommended.[High → deferred]useNavigationTabLabelswitch-case exhaustiveness. Could be done withvi.mock('react-i18next')in node-env without RTL — lower-hanging fruit than item above. Developer response: accepted with technical justification.⚠️ Recommended for earliest follow-up.
E2e gaps:
[Medium]No Playwright spec exercises the_navigationTabsconfig path: render 3-tab bar, switch tabs, verify mode change, verify form fields clear on switch. Noted for future automation scope.
🔗 Downstream Impact
Related to: EMB-452 (Add Send/Receive amount input cards for Jumper modes)
EMB-452 depends on the active tab correctly setting mode and modeOptions via tabConfig — when EMB-452 renders amount input cards conditionally via useWidgetConfig().mode, it will receive the correct per-tab value. This contract is implemented correctly in NavigationTabsStoreProvider. The small-screen gap from the original review is resolved (design decision made); EMB-452 can proceed on this foundation.
QA Agent — 2026-06-22
There was a problem hiding this comment.
Requesting changes on 6 items — each requires either a code fix or an explicit acceptance comment with justification before this review is considered complete.
| # | Severity | Type | Issue / File |
|---|---|---|---|
| 1 | 🟠 High | Test gap | packages/widget/src/stores/navigationTabs/utils.test.ts (new) |
| 2 | 🟠 High | Test gap | packages/widget/src/stores/navigationTabs/useNavigationTabsStore.test.tsx (new) |
| 3 | 🟠 High | Test gap | packages/widget/src/stores/navigationTabs/useNavigationTabLabel.test.ts (new) |
| 4 | 🟡 Medium | Code | useNavigationTabLabel switch missing default case — silent undefined at runtime |
| 5 | 🟡 Medium | AC gap | Small-screen overflow for navigation tab bar unresolved (open TODO in AppLayout.tsx) |
| 6 | 🟢 Low | API surface | InternalNavigationTabKey and InternalWidgetMode publicly exported via index.ts |
1. [High] Test gap — utils.test.ts (new)
- Missing:
getNavigationTabKeys— all branches (_navigationTabsset/unset,mode: 'split'with string/object/undefined modeOptions, empty array) - Missing:
getInitialActiveTab— with_navigationTabs, with split mode, with non-split modes - Missing:
getTabSplitMode— split-mode tab, non-split tab, undefined key - Missing:
getTabVariant/getTabMode/getTabModeOptions— tab with preset, tab that inherits from config
2. [High] Test gap — useNavigationTabsStore.test.tsx (new)
- Missing: store created on first render, recreated on signature change, NOT recreated on
setActiveTabcall - Missing:
useSplitModereturns correct value for split tabs (swap/bridge/swap-advanced/bridge-advanced) andundefinedfor non-split tabs - Missing:
tabConfigoverride —useWidgetConfig()consumers inside the provider read the tab-overriddenmode/variant/modeOptions
3. [High] Test gap — useNavigationTabLabel.test.ts (new)
- Missing: each of the six case branches maps to the expected
t()key - Missing: shared-case verification —
swapandswap-advancedresolve to the same label;bridgeandbridge-advancedresolve to the same label
4. [Medium] useNavigationTabLabel switch missing default case
packages/widget/src/stores/navigationTabs/useNavigationTabLabel.ts — the inner function is typed as (key: NavigationTabKey) => string but has no default arm. When NavigationTabKey gains a new member, TypeScript will not warn and the function will silently return undefined at runtime, removing the tab label from the UI. Add an exhaustiveness guard:
default: {
const _exhaustive: never = key
return _exhaustive
}Or at minimum: default: return key so the raw key renders rather than nothing.
5. [Medium] Small-screen overflow unresolved
packages/widget/src/AppLayout.tsx has an open TODO: what to show on smaller screens? inline comment. The tab scroller uses overflow: 'visible !important' — tabs that exceed the container width will spill outside the widget on narrow viewports. Before merging, decide on a behaviour (scroll, collapse to menu, hide tabs) or link a follow-up ticket in the TODO so this is tracked.
6. [Low] Internal types publicly exported
packages/widget/src/types/widget.ts — InternalNavigationTabKey, InternalWidgetMode, and _navigationTabs are all re-exported from index.ts via export * from './types/widget.js'. They are guarded only by JSDoc @internal and underscore convention. Consider moving internal-only types to a file not re-exported from the package root, or document the stability contract explicitly in the JSDoc.
💡 Once you've addressed the items above, re-apply the "Agent Review Request" label to trigger an automated re-review.
# Conflicts: # packages/widget/src/providers/WidgetProvider/WidgetProvider.tsx # packages/widget/src/stores/settings/useSplitModeStore.tsx
|
Addressed the review items + rebased on latest
|
There was a problem hiding this comment.
✅ QA Pass (re-review) — all prior change requests from 2026-06-19 are resolved or explicitly accepted with technical justification. Utils unit tests added, default label fallback fixed, small-screen TODO resolved. Deferred store/hook tests tracked for follow-up RTL infra work.
Which Linear task is linked to this PR?
https://linear.app/lifi-linear/issue/EMB-451/enable-simple-and-advanced-jumper-modes-on-widget
Why was it implemented this way?
For testing: uncomment this line from the code https://github.com/lifinance/widget/pull/775/changes#diff-7fb6dbe81ac16ec6a71b3f658593e2711e2a94393e9e9a57014e4546eb3c97acR63
Header tabs are now config-driven through a new internal
_navigationTabsoption (not part of the public API). Each tab key maps to avariant/mode/modeOptions, and the active tab drives the displayed flow — those values are derived from the active tab, never duplicated into a store.Implementation:
NavigationTabsStorereplaces the oldSplitModeStoreand holds onlytabs+activeTab. Per-tabvariant/mode/modeOptionslive in a static lookup and are resolved by util (getTabMode/getTabVariant/getTabModeOptions/getTabSplitMode); each field falls back toconfigwhen a tab leaves it unset.NavigationTabsStoreProviderre-provides an overriddenWidgetContextbelow the store, so every existinguseWidgetConfig()consumer (container width, mode, modeOptions, …) reflects the active tab with no consumer changes.HeaderTabsis a shared, key-based presentational tabs bar;HeaderNavigationTabswires it to the store anduseNavigationTabLabelresolves a tab key to its i18n label.mode: 'split'behaviour.useSplitModeis derived from the active tab.utils/variant.tswas renamed toutils/mode.ts.Tab presets:
default→ Swap & Bridge (wide),private(compact, swap split),refuel(wide),swap-advanced/bridge-advanced(wide split),limit(compact), plus the implicitswap/bridgesplit tabs.Note
The
_navigationTabsoption, thelimitmode and the advanced tab keys are all@internal— not part of the public widget API.Visual showcase (Screenshots or Videos)
Tabs can be enabled from config:
https://github.com/user-attachments/assets/82547e6a-7e15-4848-85db-d8afe34ab680
https://github.com/user-attachments/assets/956457ce-2d7b-46fb-95d6-6aa5ec188f90
Checklist before requesting a review