Skip to content

feat(widget): add support for navigation tabs (internal)#775

Merged
chybisov merged 14 commits into
mainfrom
feat/header-tabs-store
Jun 22, 2026
Merged

feat(widget): add support for navigation tabs (internal)#775
chybisov merged 14 commits into
mainfrom
feat/header-tabs-store

Conversation

@effie-ms

@effie-ms effie-ms commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

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 _navigationTabs option (not part of the public API). Each tab key maps to a variant / mode / modeOptions, and the active tab drives the displayed flow — those values are derived from the active tab, never duplicated into a store.

Implementation:

  • NavigationTabsStore replaces the old SplitModeStore and holds only tabs + activeTab. Per-tab variant/mode/modeOptions live in a static lookup and are resolved by util (getTabMode / getTabVariant / getTabModeOptions / getTabSplitMode); each field falls back to config when a tab leaves it unset.
  • NavigationTabsStoreProvider re-provides an overridden WidgetContext below the store, so every existing useWidgetConfig() consumer (container width, mode, modeOptions, …) reflects the active tab with no consumer changes.
  • HeaderTabs is a shared, key-based presentational tabs bar; HeaderNavigationTabs wires it to the store and useNavigationTabLabel resolves a tab key to its i18n label.
  • The split Swap / Bridge tabs are served by this same unified store (no separate split-mode store), preserving existing mode: 'split' behaviour. useSplitMode is derived from the active tab.
  • utils/variant.ts was renamed to utils/mode.ts.

Tab presets: default → Swap & Bridge (wide), private (compact, swap split), refuel (wide), swap-advanced / bridge-advanced (wide split), limit (compact), plus the implicit swap / bridge split tabs.

Note

The _navigationTabs option, the limit mode 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

  • I have performed a self-review and testing of my code.
  • This pull request is focused and addresses a single problem.
  • If this PR modifies the Widget API or adds new features that require documentation, I have updated the documentation in the public-docs repository.

@changeset-bot

changeset-bot Bot commented Jun 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1b581b5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@lifi/widget Minor
nft-checkout Patch
tanstack-router-example Patch

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

@effie-ms effie-ms self-assigned this Jun 11, 2026
@effie-ms effie-ms marked this pull request as draft June 11, 2026 07:25
@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown

E2E Examples — all passed

All examples passed in the latest run.

@effie-ms effie-ms temporarily deployed to widget-test-pr-775 June 12, 2026 13:14 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown

E2E Playground results

passed  158 passed

Details

stats  158 tests across 10 suites
duration  2 minutes, 34 seconds
commit  1b581b5

📥 Download full HTML report (open the run → Artifacts → playwright-report)

@effie-ms effie-ms changed the title feat: replace SplitModeStore with unified HeaderTabsStore and add jumper modes feat: add simple and advance modes switching Jun 15, 2026
@effie-ms effie-ms changed the title feat: add simple and advance modes switching feat (widget): add simple and advance modes switching Jun 15, 2026
@effie-ms effie-ms changed the title feat (widget): add simple and advance modes switching feat(widget): add simple and advance modes switching Jun 15, 2026
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown

✅ E2E Dev Smoke — passing

Check Result
Dev server start (pnpm dev) ✅ started
Smoke tests ✅ passed

4 passed · 0 failed · 0 skipped · 23s

View run

Comment thread packages/widget/src/components/AppContainer.tsx Outdated
Comment thread packages/widget/src/AppLayout.tsx Outdated
@effie-ms effie-ms changed the title feat(widget): add simple and advance modes switching feat(widget): add simple and advanced tiers switch Jun 17, 2026
@effie-ms effie-ms changed the title feat(widget): add simple and advanced tiers switch feat(widget): add support for navigation tabs (internal) Jun 18, 2026
],
variant: 'wide',
// mode: 'split',
// _navigationTabs: ['default', 'private', 'refuel'], // ['swap-advanced', 'bridge-advanced', 'limit']

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

For testing: Uncomment to get a view with tabs

@effie-ms effie-ms marked this pull request as ready for review June 19, 2026 11:13
@effie-ms effie-ms added the Agent Review Request triggers QA Agent Zeus label Jun 19, 2026
@github-actions github-actions Bot added QA AI Reviewing and removed Agent Review Request triggers QA Agent Zeus labels Jun 19, 2026
@effie-ms effie-ms requested a review from chybisov June 19, 2026 11:22
@lifi-qa-agent

lifi-qa-agent Bot commented Jun 19, 2026

Copy link
Copy Markdown

🔍 QA Review — EMB-451

🔗 Linear Ticket · Pull Request #775

ℹ️ 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 main (React 19 migration).

🧠 What this ticket does

This PR replaces the old SplitModeStore with a unified NavigationTabsStore that makes the widget header tabs fully config-driven via a new internal _navigationTabs option. Each tab key maps to a preset of variant, mode, and modeOptions; the active tab re-provides the WidgetContext with those overrides so all existing useWidgetConfig() consumers see the active tab's settings transparently. Existing mode: 'split' behavior is preserved by routing Swap/Bridge through the same unified store.

Verdict: ✅ Pass — all prior change requests are resolved or explicitly accepted with technical justification. One new Low-severity observation noted on an @internal API.


📋 Ticket Summary

Add 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: NavigationTabsStore, tab presets, label resolution, HeaderNavigationTabs, and AppContainer width support.

Acceptance Criteria:

  1. Widget accepts navigation tabs (internal config) — ✅ Met
    • Evidence: _navigationTabs?: NavigationTabKey[] in widget.ts:378; NavigationHeader.tsx guards on navigationTabsCount > 0
  2. Existing (old) modes/tabs behave identically to before — no regression — ✅ Met
    • Evidence: getNavigationTabKeys falls through to splitTabKeys for mode: 'split' without _navigationTabs; useSplitMode() replaces useSplitModeStore at all call sites; CI green (158 E2E tests passing)

🏷️ PR Naming — ❌ Fail

feat(widget): add support for navigation tabs (internal)

The project's expected format is [TICKET-XXX] type: Description. The ticket ID EMB-451 is referenced only in the PR body, not the title. Discoverability is maintained via the body link, but the title does not match the team convention.

🔎 Ticket Discoverability — ✅ Pass

EMB-451 found in: PR description body (first line: https://linear.app/lifi-linear/issue/EMB-451/...)


✅ Ticket Coverage — High

The 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 (utils.test.ts), the feature is complete against both AC items, and CI is green.


🔁 Re-review resolution status

# Original severity Item Status Notes
1 🟠 High utils.test.ts — store utility unit tests ✅ Resolved 65-line test file added; covers all core branches
2 🟠 High useNavigationTabsStore.test.tsx — store/provider tests ⚠️ Deferred (accepted) No RTL/jsdom infra in widget package; deferral explicitly accepted with justification; follow-up advised
3 🟠 High useNavigationTabLabel.test.ts — label hook tests ⚠️ Deferred (accepted) Same RTL infra gap; label-switch tests could be done with vi.mock in node-env but accepted for this PR
4 🟡 Medium useNavigationTabLabel switch missing default case ✅ Resolved default: return t('header.swapAndBridge') added; safe fallback prevents undefined label
5 🟡 Medium Small-screen tab overflow / AppLayout TODO ✅ Resolved TODO removed; narrow-screen layout delegated to integrator via theming
6 🟢 Low InternalNavigationTabKey / InternalWidgetMode publicly exported ✅ Accepted No change; @internal JSDoc + _ prefix convention confirmed and documented in changeset

⚠️ Issues Found (1)

# 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, tabConfig override, split-mode derivation. Requires RTL/jsdom setup. Developer response: accepted with technical justification. ⚠️ Follow-up ticket recommended.
  • [High → deferred] useNavigationTabLabel switch-case exhaustiveness. Could be done with vi.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 _navigationTabs config 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

@lifi-qa-agent lifi-qa-agent 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.

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 (_navigationTabs set/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 setActiveTab call
  • Missing: useSplitMode returns correct value for split tabs (swap/bridge/swap-advanced/bridge-advanced) and undefined for non-split tabs
  • Missing: tabConfig override — useWidgetConfig() consumers inside the provider read the tab-overridden mode/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 — swap and swap-advanced resolve to the same label; bridge and bridge-advanced resolve 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.tsInternalNavigationTabKey, 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.

effie-ms added 2 commits June 22, 2026 10:06
# Conflicts:
#	packages/widget/src/providers/WidgetProvider/WidgetProvider.tsx
#	packages/widget/src/stores/settings/useSplitModeStore.tsx
@effie-ms

Copy link
Copy Markdown
Contributor Author

Addressed the review items + rebased on latest main (React 19 migration):

  • LF: testing deployment #4 — label switch default case ✅ Added a default arm to useNavigationTabLabel so it always returns a label (never undefined) if the key union grows.
  • feat: add main swap view layout #1utils.ts unit tests ✅ Added stores/navigationTabs/utils.test.ts covering the core branches of getNavigationTabKeys, getInitialActiveTab, getTabSplitMode, and the per-tab variant/mode/modeOptions derivation (preset vs. config fallback).
  • LF: wallet support #2 / feat: get and display routes #3 — store & label hook tests ⏭️ Out of scope here. The widget package has no jsdom/RTL test infra — vitest runs node-env only (see existing createSettingsStore.test.ts, which tests the store factory directly, not the provider). The provider's recreation-on-signature-change and WidgetContext override genuinely need React rendering, so these belong in a separate change that introduces the rendering test setup.
  • LF-552: wallet bugfix #5 — small-screen tab overflow ✅ Resolved. The TODO is gone; narrow-screen tab layout is intended to be configured by the integrator (Jumper) via theming rather than hardcoded in the widget.
  • put wallet management into own package #6 — internal types exported ✅ Accepted as a known limitation, already documented in the changeset. The _ prefix + @internal JSDoc are the agreed convention for now.

@lifi-qa-agent lifi-qa-agent 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.

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

@chybisov chybisov merged commit 0ad86fe into main Jun 22, 2026
46 checks passed
@chybisov chybisov deleted the feat/header-tabs-store branch June 22, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants