Skip to content

feat(widget): add per-tab navigation config#781

Open
effie-ms wants to merge 7 commits into
mainfrom
feat/private-tab-mode
Open

feat(widget): add per-tab navigation config#781
effie-ms wants to merge 7 commits into
mainfrom
feat/private-tab-mode

Conversation

@effie-ms

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

Copy link
Copy Markdown
Contributor

Which Linear task is linked to this PR?

https://linear.app/lifi-linear/issue/EMB-465/per-tab-config-for-header-navigation-tabs

Why was it implemented this way?

Generalises a header navigation tab's overrides from a fixed, widget-side preset to integrator-supplied config.

Per-tab config on _navigationTabs. Changes the entry shape from NavigationTabKey[] to { tabKey, config: Partial<WidgetConfig> }[]. The widget-side preset table (navigationTabsByKey, which baked variant / mode / modeOptions per key) is removed — each tab's config is now supplied by the integrator and layered onto the widget config ({ ...widgetConfig, ...activeConfig }) while the tab is active, with omitted fields falling back to the base config. A tab can therefore override any config field, not just the three preset ones. useSplitMode derives the effective split side from the active tab's resolved config (resolveSplitMode); existing mode: 'split' and mode: 'default' behaviour is unchanged.

_navigationTabs: [
  { tabKey: 'default', config: { variant: 'wide', mode: 'default' } },
  { tabKey: 'private', config: { variant: 'compact', mode: 'split', modeOptions: { split: 'swap' } } },
  { tabKey: 'refuel', config: { variant: 'wide', mode: 'refuel' } },
]

Scope / trade-offs. NavigationTabsStoreProvider sits inside StoreProvider — below SDKClientProvider, ThemeProvider and I18nProvider — so tab overrides apply to the widget body only. sdkConfig, theme / appearance and languages are read above this provider and are not tab-overridable.

Visual showcase (Screenshots or Videos)

N/A

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 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8df9a50

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 changed the base branch from main to feat/amount-input-cards June 12, 2026 15:14
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

E2E Examples — all passed

All examples passed in the latest run.

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

E2E Playground results

passed  158 passed

Details

stats  158 tests across 10 suites
duration  2 minutes, 10 seconds
commit  8df9a50

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

@effie-ms effie-ms force-pushed the feat/amount-input-cards branch from 53fa01e to 9f97fb1 Compare June 17, 2026 11:22
@effie-ms effie-ms force-pushed the feat/private-tab-mode branch 2 times, most recently from 572513c to a80d82f Compare June 18, 2026 08:45
@effie-ms effie-ms self-assigned this Jun 18, 2026
@effie-ms effie-ms force-pushed the feat/amount-input-cards branch 2 times, most recently from 9c39a0a to 6ae7bef Compare June 22, 2026 08:27
@effie-ms effie-ms force-pushed the feat/private-tab-mode branch from a80d82f to 65f2d16 Compare June 22, 2026 11:20
@effie-ms effie-ms changed the base branch from feat/amount-input-cards to feat/header-tabs-store June 22, 2026 11:21
Base automatically changed from feat/header-tabs-store to main June 22, 2026 13:18
@effie-ms effie-ms changed the title feat(widget): add private mode requiring send-to-wallet address feat(widget): per-tab navigation config and per-tab sdkConfig routing Jun 22, 2026
@effie-ms effie-ms force-pushed the feat/private-tab-mode branch from 5b24746 to dc8bbac Compare June 22, 2026 15:31
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

✅ E2E Dev Smoke — passing

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

4 passed · 0 failed · 0 skipped · 23s

View run

@effie-ms effie-ms changed the title feat(widget): per-tab navigation config and per-tab sdkConfig routing feat(widget): add per-tab navigation config Jun 23, 2026
@effie-ms effie-ms added the Agent Review Request triggers QA Agent Zeus label Jun 23, 2026
@effie-ms effie-ms marked this pull request as ready for review June 23, 2026 16:01
@github-actions github-actions Bot added QA AI Reviewing and removed Agent Review Request triggers QA Agent Zeus labels Jun 23, 2026
@lifi-qa-agent

lifi-qa-agent Bot commented Jun 23, 2026

Copy link
Copy Markdown

🔍 QA Review — EMB-465

PR: #781 feat(widget): add per-tab navigation config
Review type: 🆕 First review
Verdict: Needs Work


What this PR does

Replaces the widget-internal navigationTabsByKey preset table (which hardcoded variant/mode/modeOptions per tab key) with an integrator-supplied { tabKey, config: Partial<WidgetConfig> }[] shape on _navigationTabs. The active tab's config is spread onto the base widget config in NavigationTabsStoreProvider, so every useWidgetConfig() consumer below that provider transparently sees tab-merged values. The useSplitMode hook is updated to derive split direction from the active tab's resolved config via a new resolveSplitMode utility.


Acceptance criteria check

# Criterion Status Evidence
1 _navigationTabs accepts { tabKey, config: Partial<WidgetConfig> }[]; widget-side navigationTabsByKey preset table removed ✅ Met NavigationTabConfig interface added in types/widget.ts; navigationTabsByKey, getTabMode, getTabVariant, getTabModeOptions all deleted from utils.ts; _navigationTabs type changed from NavigationTabKey[] to NavigationTabConfig[]
2 Active tab's config merged onto widget config; omitted fields fall back to base config; any config field overridable ✅ Met useNavigationTabsStore.tsx: return { ...widgetConfig, ...activeConfig } in tabConfig useMemo; when activeConfig is undefined the base widgetConfig is returned unchanged
3 Switching tabs re-applies active tab config and clears fromAmount/fromToken/toToken ⚠️ Partial Re-applying the active tab's config is implemented. Form field clearing is conditional and undocumented: FormStoreProvider only includes a field in reactiveFormValues when Object.hasOwn(tabConfig, '<field>') is true. For a typical tab config like { mode: 'refuel', variant: 'wide' }, none of fromAmount/fromToken/toToken are own properties of the merged tabConfig, so they are never cleared. See Issue 1 for full trace.
4 No _navigationTabs + mode: 'split': Swap/Bridge tabs render and switching constrains routing as before ✅ Met getNavigationTabs falls through to the module-level splitTabs constant when _navigationTabs is absent and mode === 'split'; resolveSplitMode tested for this path
5 mode: 'default' (or unset): no tabs render; split mode resolves to undefined ✅ Met getNavigationTabs returns [] when neither condition applies; resolveSplitMode test case 4 confirms undefined for mode: 'default'
6 _navigationTabs remains _-prefixed / internal, not public API ✅ Met @internal JSDoc on NavigationTabConfig and _navigationTabs property; changeset explicitly states "internal"; minor bump (not major)
7 sdkConfig, theme/appearance, languages not tab-overridable ✅ Met (architectural) ThemeProvider, SDKClientProvider, I18nProvider sit above StoreProviderNavigationTabsStoreProvider in AppProvider; they read from the outer WidgetContext set by WidgetProvider, not from the tab-merged context
8 Unit tests cover tab resolution and resolveSplitMode; type-check and existing tests pass ✅ Met utils.test.ts has comprehensive tests for getNavigationTabs, getInitialActiveTab, and four resolveSplitMode scenarios; 158 E2E tests pass per CI

Issues

1. [Medium] AC 3 — Form fields not cleared unconditionally on tab switch

File: packages/widget/src/stores/navigationTabs/useNavigationTabsStore.tsx + packages/widget/src/stores/form/FormStore.tsx

The AC states: "Switching tabs re-applies the active tab's config and clears the form fields (fromAmount / fromToken / toToken)."

The implementation relies on Object.hasOwn checks in FormStoreProvider to determine which fields appear in reactiveFormValues. A field is only reset by FormUpdatersetUserAndDefaultValues when it is an own property of the merged tabConfig. For a typical tab config of { mode: 'default', variant: 'wide' }:

tabConfig = { ...baseWidgetConfig, mode: 'default', variant: 'wide' }
Object.hasOwn(tabConfig, 'fromToken')  // false → not in reactiveFormValues
// FormUpdater never resets fromToken → user's selection persists across tab switch

To clear fields under the current implementation, an integrator must explicitly include { fromToken: undefined, fromAmount: undefined, toToken: undefined } in every tab config. This is unintuitive, undocumented, and contradicts the AC's framing that clearing is unconditional.

Suggested fix: In NavigationTabsStoreProvider, add a useEffect that fires when activeTab changes and explicitly resets the form fields (e.g. via a formUpdateKey increment or by calling form store reset actions directly). Alternatively, update the AC to clarify the intended design and document the integrator opt-in pattern.

2. [Low] getNavigationTabs returns a new [] reference on every render when no tabs are configured

File: packages/widget/src/stores/navigationTabs/useNavigationTabsStore.tsx

When no navigation tabs are configured and mode !== 'split', getNavigationTabs returns a new [] literal on every call. Since navigationTabs is used as a useMemo dependency, the memo re-executes on every render in this scenario. Correctness is unaffected (the returned value is always widgetConfig — stable, so no downstream re-renders are triggered), but the unnecessary memo execution is worth fixing.

Suggested fix: Return a module-level empty-array constant from getNavigationTabs (analogous to the existing splitTabs constant), or wrap the call in useMemo: const navigationTabs = useMemo(() => getNavigationTabs(config), [config]).

3. [Advisory] No type-level restriction on non-effective fields in NavigationTabConfig.config

File: packages/widget/src/types/widget.ts

NavigationTabConfig.config is typed as Partial<WidgetConfig>, which includes sdkConfig, theme, appearance, and languages. These fields are non-effective for tab overrides (providers consuming them sit above NavigationTabsStoreProvider), but no JSDoc warns about this. A note listing non-effective keys on the config field would prevent future confusion for consumers reading the type.


Test coverage

Added/updated in utils.test.ts:

  • getNavigationTabs: configured tabs (stable reference), implicit split tabs, pin-to-one-side early-exit.
  • getInitialActiveTab: first configured tab, split-mode seed, mode: 'default'undefined.
  • resolveSplitMode (4 cases): split tabs configured, non-split active tab with split base config, implicit split tabs, tab-less fallback.

Gaps:

  • No component/integration test for the tabConfig merge in NavigationTabsStoreProvider.
  • No test for the form field clearing path (AC 3) — consistent with the implementation gap.

Downstream impact

Split-tab regression risk: Low. resolveSplitMode correctly handles all four paths and all cases have unit test coverage; 158 E2E tests pass.

Breaking change for _navigationTabs string consumers: The type changes from NavigationTabKey[] to NavigationTabConfig[]. Any integrator using _navigationTabs: ['default', 'private'] will get a TypeScript error and a runtime no-op. Changeset is correctly minor (internal API). Playground updated.


QA review by lifi-qa-agent · EMB-465 · PR #781

@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 2 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 🟠 Medium Code AC 3 not met: form fields (fromAmount/fromToken/toToken) not cleared unconditionally on tab switch
2 🟢 Low Code packages/widget/src/stores/navigationTabs/useNavigationTabsStore.tsxnavigationTabs not memoized, unstable reference in useMemo deps

1. [Medium] AC 3 — Form fields not cleared unconditionally on tab switch

The AC states: "Switching tabs re-applies the active tab's config and clears the form fields (fromAmount / fromToken / toToken)."

The current implementation does not do this unconditionally. FormStoreProvider only includes form fields in reactiveFormValues (and therefore in setUserAndDefaultValues) when Object.hasOwn(tabConfig, '<field>') is true. For a typical tab config such as { mode: 'default', variant: 'wide' }, none of fromAmount, fromToken, or toToken are own properties of the merged tabConfig, so FormUpdater never resets them. The user's field selections persist unchanged after switching tabs.

For clearing to work under the current implementation, an integrator must explicitly include { fromToken: undefined, fromAmount: undefined, toToken: undefined } in every tab config — this is unintuitive, undocumented, and directly contradicts the AC.

Suggested fix: Add a useEffect in NavigationTabsStoreProvider (or HeaderNavigationTabs) that fires when activeTab changes and unconditionally resets those three fields. Alternatively, if the intended design is integrator opt-in, update the AC/documentation to clarify this and add a JSDoc example to NavigationTabConfig.

2. [Low] Unstable navigationTabs reference in tabConfig useMemo deps

In useNavigationTabsStore.tsx, navigationTabs is computed inline on every render via getNavigationTabs(config). When this returns [] (no tabs configured, non-split mode), each call produces a new array reference, which causes the tabConfig useMemo to re-execute on every render even when nothing has changed. The correctness impact is zero (the memo always returns the same stable widgetConfig reference in this path), but it is unnecessary work on every render of NavigationTabsStoreProvider.

Fix: Return a module-level empty-array constant from getNavigationTabs when returning [] (analogous to the existing splitTabs constant), or wrap the call: const navigationTabs = useMemo(() => getNavigationTabs(config), [config]).


💡 Once you've addressed the items above, re-apply the "Agent Review Request" label to trigger an automated re-review.

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.

1 participant