@gasfree-kit/pay
v0.3.0
Published
Drop-in React payment button component for chain-abstracted USDT checkouts — powered by gasfree-kit
Downloads
229
Maintainers
Readme
@gasfree-kit/pay
Drop-in React payment button for chain-abstracted USDT checkouts.
One component, one CSS import, one prop set. The modal handles preview, confirmation, cross-chain routing, and receipt automatically. Built on top of @gasfree-kit/react, @gasfree-kit/intent, and @gasfree-kit/bridge.
How It Works
┌──────────────────────────────────────────────────────────┐
│ Your app │
│ │
│ <GasfreePay to="0x…" amount="50.00" onSuccess={…} /> │
└────────────────────────────┬─────────────────────────────┘
│
v
┌──────────────────────────────────────────────────────────┐
│ @gasfree-kit/pay │
│ │
│ ┌────────┐ ┌──────┐ ┌──────┐ ┌─────────┐ │
│ │ button │ → │ open │ → │ plan │ → │ preview │ ┐ │
│ └────────┘ └──────┘ └──────┘ └─────────┘ │ │
│ v │
│ ┌──────────────────────────┐ │
│ │ confirm → execute → done │ │
│ └──────────────────────────┘ │
│ │
│ Flow state machine · idempotency · timeout · onEvent │
└────────┬──────────────────┬──────────────────┬───────────┘
│ │ │
v v v
┌────────────────┐ ┌────────────────┐ ┌────────────────────┐
│ @gasfree-kit/ │ │ @gasfree-kit/ │ │ React context │
│ react │ │ intent │ │ (signer, address) │
│ │ │ │ │ │
│ useGasfree │ │ planIntent │ │ from │
│ useExecute │ │ executeIntent │ │ <GasfreeProvider> │
│ Intent │ │ ↓ │ │ │
│ │ │ @gasfree-kit/ │ │ │
│ │ │ bridge (auto) │ │ │
└────────────────┘ └────────────────┘ └────────────────────┘The modal walks the user through five phases — preview → confirm → executing → success / error — driven by an internal state machine. Cross-chain routes auto-bridge via @gasfree-kit/intent's planner; signing comes from whatever signer (passkey by default, seedPhrase as alternate) the surrounding <GasfreeProvider> is configured with.
What you get
- Drop-in
<GasfreePay />component — button + modal flow, no other UI work required - Cross-chain auto-routing — if the recipient is on a different chain than the user's USDT balance, pay bridges automatically (Base → Arbitrum, etc.)
- Themed defaults + CSS variable overrides — looks good out of the box, customizable via
--gasfree-*variables - Accessibility built in —
role="dialog", focus trap, ESC dismissal,aria-livestep updates, keyboard-navigable - Idempotent by design — auto-generates an idempotency key per session; double-clicks and remounts can't double-charge
- Signer-agnostic — uses whatever signer the surrounding
<GasfreeProvider>is configured with (typically passkey)
Install
pnpm add @gasfree-kit/pay @gasfree-kit/react @gasfree-kit/coreYour package manager will prompt you to install the required peer dependencies (react, react-dom, @tanstack/react-query).
Quick start
import { GasfreeProvider } from '@gasfree-kit/react';
import { GasfreePay } from '@gasfree-kit/pay';
import '@gasfree-kit/pay/styles.css';
function Checkout() {
return (
<GasfreeProvider
config={{
evm: {
chains: ['base', 'arbitrum'],
signer: { type: 'passkey', credential },
bundlerUrl: process.env.BUNDLER_URL!,
paymasterUrl: process.env.PAYMASTER_URL!,
isSponsored: true,
sponsorshipPolicyId: process.env.SPONSORSHIP_POLICY_ID!,
},
}}
address={userAddress}
>
<GasfreePay
to="0xMerchant..."
amount="50.00"
destinationChain="arbitrum"
metadata={{ orderId: 'inv-42' }}
onSuccess={(receipt) => console.log('paid', receipt)}
/>
</GasfreeProvider>
);
}That's it. One component renders the button; clicking it opens the modal and walks the user through preview → confirm → execute → receipt.
Props
| Prop | Type | Required | Description |
| ------------------ | ---------------------------------- | -------- | ---------------------------------------------------------------------- |
| to | string | yes | Recipient EVM address (0x-prefixed, 40 hex chars). |
| amount | string | yes | Human-readable amount, e.g. "50.00". |
| token | "USDT" | no | Token to transfer. Defaults to "USDT". |
| destinationChain | EvmChain | no | Preferred destination chain. Auto-routed when omitted. |
| metadata | Record<string, unknown> | no | App-specific metadata stored alongside the payment (invoice id, etc.). |
| buttonLabel | string | no | Custom button text. Defaults to "Pay {amount} {token}". |
| theme | "light" \| "dark" \| "auto" | no | Theme override. Defaults to "auto" (follows prefers-color-scheme). |
| className | string | no | Extra class on the root button + modal. |
| onSuccess | (receipt: IntentReceipt) => void | no | Fired on terminal success. |
| onError | (error: Error) => void | no | Fired on terminal failure. |
| onCancel | () => void | no | Fired when the user dismisses the modal before completion. |
Theming
The component scopes its styles under a [data-gasfree-pay] root with data-theme set to light, dark, or auto-detected. Override any of the CSS variables on an ancestor or on the root itself:
[data-gasfree-pay] {
--gasfree-primary: #6c5ce7; /* button & accent color */
--gasfree-primary-foreground: #fff;
--gasfree-radius: 8px; /* corner rounding */
--gasfree-bg: #fafafa; /* modal background */
--gasfree-fg: #111; /* modal foreground */
--gasfree-muted: #888; /* secondary text */
--gasfree-border: #ddd;
--gasfree-success: #16a34a;
--gasfree-error: #dc2626;
--gasfree-font: 'Inter', sans-serif;
--gasfree-spacing: 8px;
}Full token reference:
| Variable | Default (light) | Purpose |
| ------------------------------ | --------------- | -------------------------------- |
| --gasfree-primary | #0a84ff | Button background, accents |
| --gasfree-primary-foreground | #ffffff | Button text |
| --gasfree-radius | 12px | Border radius for button + modal |
| --gasfree-font | system stack | Font family |
| --gasfree-bg | #ffffff | Modal background |
| --gasfree-fg | #0f0f10 | Modal foreground text |
| --gasfree-muted | #6b6b70 | Secondary text |
| --gasfree-border | #e5e5e8 | Modal + step indicator borders |
| --gasfree-success | #16a34a | Success state accent |
| --gasfree-error | #dc2626 | Error state accent |
| --gasfree-spacing | 8px | Base spacing unit |
For dark mode, override the same variables under [data-gasfree-pay][data-theme='dark'].
Cross-chain routing
If the destination chain has insufficient USDT, pay auto-bridges from another chain using @gasfree-kit/intent's planner. The preview shows the bridge route explicitly:
To 0xMerc…0123
Amount 50.00 USDT
Route Base → Arbitrum
Estimated fee ~$0.40
Estimated time ~120sBridge progress surfaces as a separate step in the executing state. No additional configuration required — just declare your chains in the provider config.
Accessibility
- Modal renders with
role="dialog",aria-modal="true", andaria-labelledbypointing at the title - Focus trap (Tab / Shift+Tab cycle inside the dialog)
- ESC closes the modal (suppressed during signing/submission)
- Initial focus on the Confirm button when the preview opens
- Step indicator uses
aria-live="polite"so screen readers announce transitions - All interactive elements are keyboard-reachable
- Color contrast meets WCAG AA on the default token set
Idempotency
Pay auto-generates an idempotency key per session. The underlying bridge layer uses this key to deduplicate retries — a double-click on Confirm, a network blip during submission, or a remount in the middle of the flow can't cause a double-submit.
If you want server-side persistence, supply your own idempotencyStore via the bridge layer (see @gasfree-kit/bridge README).
Advanced
Lower-level hooks are exported for integrators who want to roll their own UI but reuse pay's flow logic:
usePayFlow()— the state machineusePayPreview()— wrapsintent.planIntentand returns aPayPreview+ plan
These are not required for the standard <GasfreePay /> usage.
License
MIT
