Gift Card
A gift card or store credit to check and show at the register, or a new gift card to send, handed off to the host's payment flow.
Experience · Shares patterns with Account Balances, QR Code
≈ 1,649 tokens per use (tool 1,272 · guidance 317 · typical payload 60)How token costs are estimated
Example: Gift Card
What's left on my bookstore gift card?
Paper Lantern Books
Gift card, card ending in 4821
Balance $37.50
Expires Dec 31, 2027
When to use
- The user asks what's left on a gift card or store credit the agent found in their account or email.
- They want to show a card at a register to pay with it.
- They want to send someone a gift card from a store: amount, design, recipient and message, then payment.
When not to use
- The agent doesn't have the card from the issuer. Never invent balances, codes or card digits.
- The user wants to pay for something in chat. Send mode only hands off to the host's payment flow; card details never go through the conversation.
- It's a bank card or account balance. Use Account Balances.
How the agent should use it
- Pass only the last 4 digits of the card. The schema rejects anything but exactly 4 digits. Never pass the full number or the PIN; set
pin: { masked: true }when the card has one, and the card says "PIN on the back". - Pass
code.valueexactly as the issuer gave it. Never build a code yourself. - In send mode, prefill
amount,recipientandmessageonly from what the user said. Onsend, start the host's payment flow with the returned values; nothing is bought until payment succeeds there. - On
refresh, fetch the balance again and show a new card. Onadd-to-wallet, open the host's wallet pass. - Lifecycle: a balance is live data, so in balance mode set
fetchedAtto when the balance was read andstaleAfterMs(e.g. an hour) on the call; the Renderer then marks an old balance as stale and offers a refresh. Supersede the card with the new one afterrefresh. SetexpiresAtto the card's expiry only if an expired card can't be used at all. Send mode needs none of these.
Balance mode
The store's name, then one quiet line with the kind and the masked number ("Gift card · •••• 4821"). The balance is the hero, in large type with dimmed cents, with the expiry ("Expires Dec 31, 2027", "Expires today" or "Expired Mar 1, 2026") and the PIN note beneath.
Show to cashier (the primary action when there's a code) expands the issuer's code as a QR code, always black on a white tile (in dark mode too, so scanners can read it). It isn't expanded by default, so the code isn't on screen until the user wants it. Turning the screen brightness up helps scanners; say so in your reply if useful. The code is encoded locally with qrcode-generator (UTF-8, level M); nothing is sent anywhere.
Actions: Add to wallet and Check balance again (in a "More actions" menu when there's a code), each reported once.
Send mode
- Card preview on top: the chosen design (its artwork, or its pattern), the amount in large type (on artwork the type is white with a soft shadow over a light top and bottom scrim, so it needs no background box) and the recipient, updating as the user edits.
- Amount: preset chips (radio buttons), plus an "Other" input when
customis set, validated against itsminandmaxin integer minor units ("The minimum is $10.", "Use at most 2 decimal places."). - Design: radio thumbnails, named for screen readers. A design with an
imageshows the store's artwork (cropped to fit, here a Warsaw poster, white roses and poppies); one without shows a neutral pattern (stripes, dots, grid) drawn as thin lines in the border color. A broken image link drops to the plain card. - Recipient: name and email, prefilled and editable. Email is checked for a sane shape ("Enter a valid email, like dana@example.com.").
- Message: a textarea with a live counter ("42/250"), limited to
messageMaxcharacters (emoji count once). Screen readers hear the remaining count near the limit. - Delivery: send now, or schedule with a date from today on.
Errors show after a field loses focus or on Continue to payment, which then focuses the first problem. Continue to payment returns send and the footer confirms "Sent to payment"; Cancel returns dismiss. Nothing is charged in the card; payment happens in the host's next step. After either, the form locks.
Installation
npx shadcn@latest add https://your-site.com/r/gift-card.json
Usage
import { tool } from "ai"
import { giftCardTool } from "@/components/genui/gift-card.schema"
const tools = {
// No execute: the user's choice is the tool result.
[giftCardTool.name]: tool({
description: giftCardTool.description,
inputSchema: giftCardTool.inputSchema,
}),
}Render the tool call and hand the result to your own flow:
import { GiftCard } from "@/components/genui/gift-card"
function ToolPart({ part, addToolResult }) {
if (part.type !== "tool-show_gift_card") return null
return (
<GiftCard
{...part.input}
status={part.state === "input-streaming" ? "streaming" : undefined}
result={part.state === "output-available" ? part.output : undefined}
onResult={(result) => {
if (result.action === "send") openGiftCardCheckout(result)
if (part.state !== "output-available") {
addToolResult({ tool: "show_gift_card", toolCallId: part.toolCallId, output: result })
} else {
sendMessage({ text: `Gift card action: ${JSON.stringify(result)}` })
}
}}
/>
)
}The agent receives one of:
// Balance mode, once per action
{ action: "add-to-wallet" }
{ action: "refresh" }
// Send mode, once
{
action: "send",
amount: 50,
currency: "USD",
designId: "warsaw",
recipient: { name: "Dana Levi", email: "dana.levi@mail.example" },
message: "Happy birthday, Dana!",
deliverAt: "now", // or "2026-10-23"
}
{ action: "dismiss" }With LangGraph
Push it from a graph node with ui.push({ name: "show_gift_card", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.
Restoring from history
When a conversation is reloaded, pass the stored result back as result (an array in balance mode when both actions were taken). Balance actions show as done; a send restores the chosen amount, design, recipient, message and delivery, read-only. Neither calls onResult again.
<GiftCard
{...part.input}
result={part.state === "output-available" ? part.output : undefined}
/>Schema
| Prop | Type | Notes |
|---|---|---|
mode | "balance" | "send" | |
brand | { name, initials? } | Initials derived from the name when absent. Data-bound |
card | Card? | Required in balance mode. Data-bound |
card.kind | "gift-card" | "store-credit" | Default gift-card |
card.last4 | string | Exactly 4 digits, shown as "•••• 4821" |
card.balance, card.currency | number, string | Major units; ISO 4217 code |
card.expiresAt | string? | ISO 8601 date |
card.pin | { masked: true }? | Never the PIN itself |
card.code | { format: "qr", value }? | The issuer's scannable payload, up to 512 characters |
send | Send? | Required in send mode |
send.amounts | number[] | 1–6 presets, e.g. [25, 50, 100]. Data-bound |
send.custom | { min, max }? | Allows a typed amount in this range |
send.currency | string | ISO 4217 code |
send.designs | { id, label, pattern?, image? }[]? | Up to 6. image is data-bound artwork (http(s) or a same-origin path). pattern: stripes, dots, grid, plain; the fallback, and it cycles when absent |
send.designId | string? | Preselected design; default the first |
send.amount | number? | The amount the user asked for; selects a preset or fills the custom amount |
send.deliverAt | "now" | string? | now (default) or a date |
send.recipient | { name?, email? }? | Prefilled, editable |
send.message | string? | A draft message, editable |
send.messageMax | number? | 20–1000. Default 250 |
Host props: status (streaming, ready, submitting, done, sent, dismissed), now, onResult, result and className.