GaslessCard
The gasless FXRP payment composer — you sign an authorization off-chain, an operated relayer submits it and pays the gas, and the payment reads as paid only from the on-chain transfer.
import { GaslessCard } from '@flarekit-dev/react-ui'
GaslessCard composes an FXRP payment you never pay native gas for. You sign an
EIP-712 payment request off-chain; an operated relayer submits it on-chain and
covers the gas. Gasless is not free, and the card says so: the first payment
needs a one-time approval that is an ordinary, gas-costing transaction, and it is
rendered loudly rather than buried. The relayer accepting the job is submitted.
Paid comes only from reading the transfer on-chain.
Live#
The preview renders the gallery's own cases — records and plans built from the fixtures OBSERVED in the recorded live Coston2 run, never re-authored here. The state switcher walks the cases the surface was verified against, so nothing here shows a state the card never actually reaches.
Gasless FXRP payment
import type { GaslessOperation, GaslessPlanResult } from '@flarekit-dev/core'
import { useGasless } from '@flarekit-dev/react'
import { GaslessCard } from '@flarekit-dev/react-ui'
import '@flarekit-dev/react-ui/styles.css'
const FXRP = { symbol: 'FTestXRP', address: '0x0b6A…3dc7', decimals: 6 } as const
export function Pay({ op, plan, reconcile }: {
op: GaslessOperation
plan: GaslessPlanResult
reconcile: (op: GaslessOperation) => Promise<GaslessOperation>
}) {
const { operation } = useGasless({ operation: op, reconcile })
if (!operation) return null
return (
<GaslessCard
operation={operation}
sendToken={FXRP}
planResult={plan}
amountText="1"
recipientText="0xA4b0…1Bd9"
relayerUrl="http://localhost:8788"
networkLabel="Coston2"
onSubmit={() => console.log('sign the payment request')}
/>
)
}Usage#
The card is prop-driven: the host builds the plan with buildGaslessPlan, signs
through its own wallet, and hands the record back. useGasless owns only the
reconcile poll that re-reads the chain while the payment is in flight.
import type { GaslessOperation, GaslessPlanResult } from '@flarekit-dev/core'
import { useGasless } from '@flarekit-dev/react'
import { GaslessCard } from '@flarekit-dev/react-ui'
import '@flarekit-dev/react-ui/styles.css'
const FXRP = { symbol: 'FTestXRP', address: '0x0b6A…3dc7', decimals: 6 } as const
export function Pay({ op, plan, reconcile }: {
op: GaslessOperation
plan: GaslessPlanResult
reconcile: (op: GaslessOperation) => Promise<GaslessOperation>
}) {
const { operation } = useGasless({ operation: op, reconcile })
if (!operation) return null
return (
<GaslessCard
operation={operation}
sendToken={FXRP}
planResult={plan}
amountText="1"
recipientText="0xA4b0…1Bd9"
relayerUrl="http://localhost:8788"
networkLabel="Coston2"
onSubmit={() => console.log('sign the payment request')}
/>
)
}Props#
| Prop | Type | Default | Description |
|---|---|---|---|
| operationrequired | OperationRecord | — | The record the whole card is projected from — the state chip, the CTA, the spine and the relay timeline all read off it. Fields stop being editable once the operation is in flight or concluded. |
| sendTokenrequired | DexToken | — | The token being sent, for the pay leg and the asset row. FXRP on the networks the forwarder was verified on. |
| planResult | GaslessPlanResult | — | The unsigned plan, or the reason there isn't one. A plan shows the authorization review; an error becomes the note and disables the CTA. Absent, the composer stays in its pre-plan state. |
| unavailable | string | — | A live read failed. Renders its own note rather than a confident no-balance — a forwarder the host could not read is not the same fact as a balance that is too low. |
| amountText | string | — | The amount in the pay leg, as text the host owns. |
| recipientText | string | — | The recipient address in the recipient field. |
| fromBalance | Amount | — | The payer's FXRP balance, shown on the pay leg when the host knows it. |
| relayerUrl | string | — | The relayer endpoint, shown as a proper host label. Omit it and the relayer row is not rendered at all, because there is no relayer identity to name. |
| relayerReachable | boolean | true | Whether the last reach of that endpoint succeeded. Only an explicit false reads as unreachable. |
| nonce | bigint | — | The signed request's nonce, shown in the authorization review once a plan exists. |
| deadline | bigint | — | The unix deadline the signature is valid until, shown beside the nonce. |
| mockLabel | string | — | The mock-mode chip. Explicit and host-supplied — the card never infers mock mode from a failure. |
| networkLabel | string | — | The network name in the header, e.g. Coston2. |
| onAmountChange | (text: string) => void | — | Called as the amount is typed. Omit it and the amount is display-only. |
| onRecipientChange | (text: string) => void | — | Called as the recipient is typed. |
| onSubmit | () => void | — | Called when the reader takes the CTA — approve, or sign the payment request. The card holds no key and broadcasts nothing; the host owns signing. |
| theme | 'light' | 'dark' | — | Overrides the inherited theme. Normally left unset — the widget follows data-theme. |
| className | string | — | Extra class on the outer element, so a host layout can place the card. |
What it renders#
One panel: the pay leg with the FXRP amount and balance, an asset row, the
recipient field, the relayer identity, and a CTA that names the exact next act
(Approve FXRP (one-time), Review payment, Relaying…, Paid).
The asset row carries a declared gap rather than a silent omission. USD₮0 sits
beside FXRP marked unavailable on testnet, because the real USD₮0 does not
implement EIP-3009 there, so a gasless USD₮0 payment has no substrate to run on.
It is shown and explained instead of quietly dropped from the list.
Once a plan exists, the authorization review shows the nonce and the unix
deadline you are actually signing over, with the replay rule stated in the same
place: each payment uses the next nonce, so an old signature cannot be replayed.
From the moment the payment starts moving through to its final state, the card
renders both the operation spine and the relay timeline — relayer accepted,
relayer submitting on-chain, FXRP transfer confirmed on-chain. The third leg is
the load-bearing one. It completes only when
the operation reaches succeeded from an on-chain transfer read, never from the
relayer's HTTP response.
States#
Every state in the switcher above is imported from
packages/react-ui/gallery/, one source of truth for both the gallery and these
docs:
- compose — the editable composer, with the relayer-covers-gas fact and the unavailable-on-testnet asset both on screen before anything is signed.
- needs-approval — the loud one-time approval. It costs gas, once; every
payment after it is gasless. This is the state that keeps
gaslessfrom being read asfree. - ready — already approved, so there is nothing to approve and only the payment request left to sign.
- approving — the one-time approval in flight.
- signing — the payment request signature, off-chain and gasless.
- submitted — the relayer accepted the job. The confirmed leg is still pending, and the card does not say paid. An acceptance over HTTP is not a transfer on-chain.
- awaiting-relay — the relayer is submitting on-chain; the awaited actor is named as the relayer, not folded into Flare.
- succeeded —
Paid, entered only from the on-chain transfer read. - insufficient-balance — the FXRP balance is below the amount.
- expired — the signing window closed; start again for a fresh deadline.
- relayer-unreachable — the relayer did not accept the request. Your signature is off-chain and nothing moved, so the outcome is not confirmed yet rather than failed.
- not-verified — a declared gap. No live run has confirmed a gasless FXRP
transfer against this forwarder in this build, so the kit refuses to sign an
approval against it and the CTA reads
Not available. - unavailable — a read failed. Kept distinct from a no-balance answer, because the host does not know the balance either.
Mock to live#
The card takes props; it never reaches for a network itself. What changes between
mock and live is the adapter behind buildGaslessPlan — the forwarder deployment
and the relayer endpoint come from @flarekit-dev/contracts, so network is
configuration and the screen does not change.
// From this…
const adapter = createMockGaslessAdapter()
// …to this. The component does not change.
const adapter = makeGaslessAdapter(publicClient, gaslessFor('coston2')!)What it will not do#
It will not call a relay acceptance a payment. submitted renders as submitted,
the confirmed leg stays pending, and Paid is reachable only from the on-chain
transfer read. It will not present gasless as free — the one-time approval is
shown as the gas-costing transaction it is, before you sign anything. It will not
turn an unreachable relayer into a failure, because a signature that never left
your machine moved nothing. It will not offer a signable payment against a
forwarder no live run has confirmed, and it will not read a failed RPC call as an
empty balance. It signs nothing and broadcasts nothing itself; onSubmit hands
the decision back to you.