Skip to content

refactor(swap): model confirm modal state as discriminated union#859

Open
notJoon wants to merge 1 commit intodevelopfrom
refactor/swap-confirm-modal-state-union
Open

refactor(swap): model confirm modal state as discriminated union#859
notJoon wants to merge 1 commit intodevelopfrom
refactor/swap-confirm-modal-state-union

Conversation

@notJoon
Copy link
Copy Markdown
Member

@notJoon notJoon commented Apr 17, 2026

Description

  • Convert SwapConfirmModalState from a nullable object shape to a discriminated union with idle and ready states
  • Narrow ConfirmSwapModal at the component boundary so ready-only data is handled as non-null inside the rendered content
  • Add regression coverage for idle and ready modal rendering

I did not change the state transition to force { status: "idle" } when modal inputs become unavailable. That behavior was potentially ambiguous, so the existing semantics were preserved.

Summary by CodeRabbit

  • Tests

    • Added comprehensive test suite for swap confirmation modal with state validation and token/price rendering assertions.
  • Refactor

    • Improved swap confirmation modal state management with explicit idle and ready states, ensuring modal only renders when data is available. Enhanced internal component structure and simplified null checks for better reliability.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gnoswap-interface Error Error Apr 17, 2026 5:05am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

Walkthrough

The changes refactor swap confirmation modal state management from a nullable-field interface to a discriminated union type with explicit "idle" and "ready" statuses. A wrapper component guards rendering, preventing content display until state is ready. The modal content component receives non-optional state via props. Hook synchronization and test suite are updated accordingly.

Changes

Cohort / File(s) Summary
State Type Refactoring
packages/web/src/states/swap.ts
Changed SwapConfirmModalState from an interface with nullable swapTokenInfo and swapSummaryInfo fields to a discriminated union with "idle" status (minimal) and "ready" status (all fields, non-nullable). Atom initialization updated to use status: "idle".
Modal Component Restructuring
packages/web/src/components/swap/confirm-swap-modal/ConfirmSwapModal.tsx
Introduced wrapper component that reads state atom and returns null unless status is "ready". Extracted content into new ConfirmSwapModalContent receiving non-optional state via props. Removed optional chaining and null guards throughout, replaced with direct field access. Updated useMemo and useCallback dependencies to match direct accesses.
Modal Test Suite
packages/web/src/components/swap/confirm-swap-modal/ConfirmSwapModal.spec.tsx
Added new test file with mocked react-i18next, SDK message/transaction builders, and fixture data. Tests verify modal renders nothing when state is idle and renders "Confirm swap" title with expected token/price values when state is ready.
Hook State Synchronization
packages/web/src/hooks/swap/data/use-swap-handler.tsx
Updated useEffect setting swapConfirmModalState to use direct object assignment ({ status: "ready", ... }) instead of functional state update, explicitly setting status: "ready" when data becomes available.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main refactoring work: converting SwapConfirmModalState to a discriminated union pattern, which is the primary change across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/swap-confirm-modal-state-union

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/web/src/components/swap/confirm-swap-modal/ConfirmSwapModal.spec.tsx (1)

4-4: Consider extracting SwapRateAction out of the hook module.

Importing SwapRateAction from @hooks/swap/data/use-swap-handler pulls the entire useSwapHandler module graph (including ConfirmSwapModal, query hooks, broadcast handlers, etc.) into the test file just to get an enum value. This bloats the test compilation/eval graph and creates a minor circular import risk (hook imports ConfirmSwapModal; this spec tests ConfirmSwapModal and imports from the hook).

Consider moving SwapRateAction (and PriceImpactStatus) to a pure types module like @models/swap/swap-rate-action.ts and re-exporting from the hook. The production component and this test can then import the enum without dragging the whole hook along.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b05bb414-2892-48f6-8111-105ff5713c62

📥 Commits

Reviewing files that changed from the base of the PR and between 9c10333 and 1a63afa.

📒 Files selected for processing (4)
  • packages/web/src/components/swap/confirm-swap-modal/ConfirmSwapModal.spec.tsx
  • packages/web/src/components/swap/confirm-swap-modal/ConfirmSwapModal.tsx
  • packages/web/src/hooks/swap/data/use-swap-handler.tsx
  • packages/web/src/states/swap.ts

Comment on lines +139 to +144
it("does not render when confirm modal state is idle", () => {
const { container } = renderModal({ status: "idle" });

expect(container.firstChild).toBeNull();
expect(screen.queryByText("Confirm swap")).not.toBeInTheDocument();
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f 'GnoswapThemeProvider' --exec cat {}

Repository: gnoswap-labs/gnoswap-interface

Length of output: 1871


Update test assertion — container.firstChild will not be null with GnoswapThemeProvider.

The idle-case assertion expect(container.firstChild).toBeNull() will fail because GnoswapThemeProvider renders a <Global> component that produces DOM nodes, even when ConfirmSwapModal correctly returns null. Since wrapping components in GnoswapThemeProvider is required for tests, replace this assertion with expect(screen.queryByText("Confirm swap")).not.toBeInTheDocument(), which already provides the correct functional coverage and aligns with the provider's DOM output.

Comment on lines 630 to 641
useEffect(() => {
if (!swapTokenInfo || !swapSummaryInfo) return;

setSwapConfirmModalState(prev => ({
...prev,
setSwapConfirmModalState({
status: "ready",
swapTokenInfo,
swapSummaryInfo,
isRefetching,
estimatedAmount,
tokenAmountLimit,
}));
});
}, [swapTokenInfo, swapSummaryInfo, isRefetching, estimatedAmount, tokenAmountLimit]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

State transition is one-way: "idle""ready" only.

Once set to "ready", the atom is never reset back to "idle" from this hook, even when tokenA/tokenB are cleared or swapSummaryInfo becomes null again. The PR description calls this out as intentional ("Preserve existing state-transition semantics"), so behavior matches the previous nullable model.

One caveat to be aware of: because swapConfirmModalState is a module-level atom (not scoped to the modal lifetime), stale swapTokenInfo/swapSummaryInfo from a prior session can remain in the atom until the next render that has fresh values. If the modal is reopened synchronously with different tokens, the wrapper in ConfirmSwapModal.tsx will render the old "ready" snapshot for one frame before the effect runs. If that flicker is acceptable (and it seemingly was before this refactor), no action needed — otherwise, consider resetting to { status: "idle" } in the else branch or on closeModal.

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