Skip to Content
Welcome to RitoSwap's documentation!

Connect Modal

      • ConnectModal.tsx
      • WalletModalHost.tsx
      • connectModalBridge.ts
        • ModalWrapper.tsx
        • WalletButton.tsx
        • useWalletConnection.ts
        • useFocusTrap.ts
        • useAutoCloseOnRoute.ts
        • useSwipeToClose.ts
        • DefaultView.tsx
        • QrView.tsx
        • ConnectingView.tsx
        • ErrorView.tsx
        • CanceledView.tsx
        • GetWalletView.tsx

Overview

The Connect Modal is the entry-point experience for any wallet connection inside RitoSwap. It centralizes connector discovery, WalletConnect QR flows, mobile deep-linking, and error recovery behind a single surface that can be summoned from anywhere—navigation, widgets, automation, or programmatic workflows. The 2024 refactor broke the legacy monolith into composable layers so that trigger components, modal chrome, state management, and visual states can evolve independently without re-implementing wallet logic.

Connect Modal default view showing wallet list and get-wallet link

What this system provides

  • Automatic Wagmi/EIP-6963 connector ordering with brand metadata and default fallbacks.
  • A dedicated state machine with typed states (default, walletconnect-qr, connecting, error, canceled, get-wallet) that drives every view.
  • A singleton opener bridge so only one modal instance exists, yet any feature can request it.
  • Mobile-first gestures (swipe-to-close, WalletConnect deep link handoff) and desktop niceties (focus trap, Escape, portal rendering).
  • Built-in a11y affordances: role="dialog", labelled regions per view, polite live regions for status updates, and route-change cleanup.

Architecture at a glance

Trigger & hosting surfaces

The system intentionally separates who asks for the modal from how the modal behaves:

  • ConnectState.tsx (documented on the ConnectButton page) renders the CTA, owns isModalOpen, and registers itself with the bridge.
  • connectModalBridge.ts exports openWalletConnectModal() plus useRegisterWalletConnectOpener(). Only one opener exists at a time, so the first registered component (usually ConnectButton) owns the modal until it unmounts, at which point the bridge clears itself.
  • WalletModalHost.tsx mounts near the app root and registers an opener exactly once during bootstrap. That opener stays active only until another component (e.g., ConnectState) registers its own handler; when that component unmounts, no fallback is automatically restored, so at least one trigger must remain mounted to keep openWalletConnectModal() functional.
Connect button triggering the modal on a mobile layout

This split keeps modal orchestration reusable without duplicating Wagmi awareness in every surface.

ConnectModal.tsx focuses on viewport management and a11y:

  • Creates a portal attached to document.body, adds a semantic backdrop, and wraps children with ModalWrapper (role="dialog", aria-modal, and optional aria-labelledby).
  • Installs effect-driven helpers:
    • useAutoCloseOnRoute closes the modal if Next.js navigation changes while the modal is open.
    • useFocusTrap traps Tab/Shift+Tab, captures the previously focused element, and restores focus after close. It also listens for Escape.
    • useSwipeToClose wires left-swipe gestures (50px threshold) for touch users.
  • Observes Wagmi’s useAccount state so an in-flight connecting state automatically closes the dialog once the user is connected.

Modal chrome classes (styles/ModalWrapper.module.css) expose two size presets (modal vs modalLoading) to visually differentiate list views from loading/error surfaces.

Connection state machine

useWalletConnection.ts is the heart of the system. It encapsulates everything the modal needs to render meaningful UI without exposing Wagmi primitives to the component tree.

State & data

  • ui.state: "default" | "walletconnect-qr" | "connecting" | "error" | "canceled" | "get-wallet".
  • ui.qrUri: WalletConnect URI, propagated to both the QR view and mobile deep-link openers.
  • ui.copied: toggles the “Copy to clipboard” CTA label for two seconds.
  • ui.connectingWallet: { name, icon, isWalletConnect }, so every view (connecting/error/canceled) can show accurate branding even if connectors lack icons.
  • Internal guards: pendingConnector prevents double-click races, while resetOnCloseIfNotConnecting ensures we leave WalletConnect sessions intact until the user cancels.

Transitions

  1. Connector click (actions.handleConnectorClick):
    • Injected wallets set connectingWallet, move to connecting, and await connectAsync. If called again while pending, the state just re-enters connecting for clarity.
    • WalletConnect connectors attach a display_uri listener. Desktop users see the QR view; mobile users get redirected immediately via window.location.href = uri while the UI shows the connecting view.
  2. Success resets pending state and returns to default. ConnectModal then closes when useAccount().isConnected flips true.
  3. Errors inspect the message to distinguish “User rejected” (canceled) from everything else (error). Both states automatically return to default after 1.5s.
  4. Actions exposed to views:
    • backToDefault() clears QR state and terms view.
    • cancelConnecting() stops WalletConnect attempts, clears UI state, and calls Wagmi’s reset() when the pending session was launched via WalletConnect.
    • copyQr() writes to the clipboard and manages the success toast label.
    • openWallet() reuses the stored URI for manual mobile relaunches.

Because the hook owns connector ordering (injected first, WalletConnect last), DefaultView can stay dumb and simply render data.allConnectors.

View surfaces

Each view lives in views/ with its own CSS module so we can iterate on layout or animation independently:

DefaultView

Shows the brand logo, wallet list (WalletButton entries with accessible labels), and the “I don’t have a wallet” CTA leading to Get Wallet view. Terms copy is outside the scroll region so it stays visible on small heights.

Default Connect Modal view with wallet list and terms text

QrView

Renders a placeholder until qrUri arrives, then swaps to react-qr-code. The QR canvas uses custom colors, embeds the Rito logo, and offers a copy-to-clipboard CTA with aria-label feedback.

QR view with fallback placeholder and copy-to-clipboard control

ConnectingView

Loops animated dots, shows wallet brand art, surfaces a Cancel button, and conditionally exposes “Open Wallet” when a WalletConnect URI exists on mobile. Status text uses role="status" + aria-live="polite".

Connecting state showing spinner dots and wallet brand art

ErrorView & CanceledView

Share the loading layout but change the copy so analytics can distinguish failures from user-initiated cancellations. role="alert" communicates urgency to assistive tech.

Error state with message 'Connection Unsuccessful'Canceled state with message 'Connection canceled by user'

GetWalletView

Educational panel with checklist items, each treated as role="listitem", plus an external link to Ethereum’s wallet finder that opens in a new tab.

Get Wallet educational view with checklist of wallet benefits

Drop-in styles in styles/ConnectingStates.module.css, styles/QrView.module.css, etc., keep animation and layout logic isolated from React code.

Accessibility & interaction contract

Even though the modal is manually assembled, it ships with a robust ARIA story:

  • ModalWrapper sets role="dialog", aria-modal="true", and takes an optional aria-labelledby so headings describe the surface when applicable.
  • useFocusTrap captures the previously focused element, loops focus inside the modal, and responds to Escape.
  • Every view defines its own labelled region (role="region" + heading IDs) or status role to ensure screen reader announcements are meaningful.
  • Live feedback is provided through polite regions (connecting text) or alerts (errors). The QR placeholder announces “Generating QR Code…” until data is ready.
  • Touch gestures are optional: swipe left to close, backdrop click to close, Cancel button, Escape key, or programmatic onClose.
  • useAutoCloseOnRoute guarantees we never strand the dialog when client-side routing occurs (e.g., nav clicks, app-level redirects).

Call out these guarantees when coordinating with a11y reviewers so regressions can be caught during PR review rather than exploratory testing.

Usage patterns

Basic embedding

import ConnectModal from '@/components/wallet/connectModal/ConnectModal'; function Page() { const [open, setOpen] = useState(false); return ( <> <button onClick={() => setOpen(true)}>Connect</button> <ConnectModal isOpen={open} onClose={() => setOpen(false)} /> </> ); }

Use this pattern sparingly—most product surfaces should rely on ConnectState (from ConnectButton) so the CTA and modal feel identical everywhere. Still, having a direct embed API helps testing, storybook usage, or bespoke admin screens.

Programmatic opening

import { openWalletConnectModal } from '@/components/wallet/connectModal/connectModalBridge'; export function RequireWalletGate({ children }) { const { isConnected } = useAccount(); if (!isConnected) { return ( <button onClick={() => openWalletConnectModal()}> Connect to use this feature </button> ); } return children; }

Any module can import openWalletConnectModal() without worrying about which trigger is currently mounted. If no trigger has registered yet, the bridge retries up to five zero-delay timeouts before giving up silently.

Always-available host

Add WalletModalHost near the app root (before layout children) so an opener exists during initial load. If some other trigger later registers and then unmounts, you must re-register a handler (e.g., by keeping ConnectState mounted in a hidden container or by toggling a keyed WalletModalHost) because the host does not automatically reclaim ownership.

// dapp/app/layout.tsx export default function RootLayout({ children }) { return ( <html lang="en"> <body> <WalletModalHost /> {children} </body> </html> ); }

This host integrates with the bridge automatically so QA scripts, LI.FI widgets, or form gates have an opener during boot. If those flows run after another trigger unregisters, make sure you re-register an opener (mount ConnectState, remount the host, or call useRegisterWalletConnectOpener from the relevant surface) before invoking openWalletConnectModal().

  • The ConnectButton documentation covers the disconnected-state widget, variant contract, and how ConnectState registers with the modal bridge.
  • The Disconnect widget describes the connected-state CTA that hands users a way out after the modal succeeds. Reference it whenever you mention wallet lifecycle handoffs.
  • Use this Connect Modal page for everything that happens inside the modal shell—state machine behavior, view details, bridge APIs, and accessibility rules. Avoid duplicating Wagmi primer content; instead link to the Wagmi or WalletConnect resources if engineers need protocol-level depth.

Need to extend the modal? Add new state-machine transitions or views next to the existing ones, document the motivation here, and circulate the update to product and a11y reviewers before merging so we keep the UX consistent across every wallet entry point.