-
Notifications
You must be signed in to change notification settings - Fork 3
feat: use new payment widget #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Warning Rate limit exceeded@bassgeta has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 4 minutes and 31 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (1)
WalkthroughAdds a local, API-driven Payment Widget (components, context, hooks, utils, receipts, Wagmi/Viem integration), replaces the monolithic Playground with a tabbed Playground (Customize/Seller/Buyer) and live preview, updates validation and currency constants, adjusts PaymentStep to the new widget API, and updates dependencies and config. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant PW as PaymentWidget
participant Conn as ConnectionHandler
participant WCM as WalletConnectModal
participant PM as PaymentModal
participant Steps as Steps (Currency/Buyer/Confirm)
participant Hook as usePayment
participant API as Request API
participant Chain as Blockchain
User->>PW: Click "Pay with crypto"
PW->>Conn: open modal (validate rnApiClientId & supportedCurrencies)
alt Wallet provided
PW->>PM: Render PaymentModal (skip connect)
else Not connected
Conn->>WCM: show connectors
User->>WCM: connect wallet
WCM-->>Conn: connected
Conn->>PM: Render PaymentModal
end
PM->>Steps: CurrencySelect fetches conversion routes
Steps->>API: GET /v2/currencies/USD/conversion-routes
API-->>Steps: conversionRoutes
User->>Steps: Select currency
Steps->>PM: selectedCurrency
PM->>Hook: executePayment(rnApiClientId, params)
Hook->>API: POST /v2/payouts
API-->>Hook: { requestId, transactions }
Hook->>Chain: send transactions...
Chain-->>Hook: receipts
Hook-->>PM: { requestId, receipts }
PM-->>PW: onSuccess(requestId, receipts)
PM->>Steps: Show PaymentSuccess
sequenceDiagram
autonumber
participant PS as PaymentSuccess
participant Rcv as Receipt Utils
participant PDF as html2pdf.js
actor User
PS->>Rcv: createReceipt(params)
Rcv-->>PS: ReceiptData
alt User clicks "Download Receipt"
PS->>PDF: dynamic import + generate PDF
PDF-->>PS: PDF blob
PS-->>User: Save receipt.pdf
end
opt Show Request Scan URL
PS-->>User: Provide Request Scan link
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches✅ Passed checks (5 passed)
Comment |
…p forms int oseparate components
fe5761c to
84eaf20
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 14
🧹 Nitpick comments (71)
components.json (1)
19-21: Fix double slash and confirm intended registry environment.The registry URL has a double slash before "r". Also, this points to "ui.stage" — confirm you don't want "ui.request.network" in production.
Apply this diff:
- "registries": { - "@requestnetwork": "https://ui.stage.request.network//r/{name}.json" - } + "registries": { + "@requestnetwork": "https://ui.stage.request.network/r/{name}.json" + }Optionally, parameterize for prod:
- "@requestnetwork": "https://ui.stage.request.network/r/{name}.json" + "@requestnetwork": "https://${process.env.NEXT_PUBLIC_UI_HOST ?? "ui.request.network"}/r/{name}.json"package.json (1)
32-33: Guard html2pdf.js for client‑only usage to avoid SSR crashes.html2pdf.js touches window; ensure all imports are dynamic and client‑only.
If not already, use:
// in client component handler const html2pdf = (await import("html2pdf.js")).default;src/components/payment-widget/components/disconnect-wallet.tsx (3)
16-18: Avoid rendering “undefined...undefined” when not connected.Guard the address display so it only renders when an address exists.
Apply this diff:
- <span className="text-sm font-mono text-slate-500 dark:text-slate-400"> - {`${address?.slice(0, 6)}...${address?.slice(-4)}`} - </span> + {address && ( + <span className="text-sm font-mono text-slate-500 dark:text-slate-400"> + {`${address.slice(0, 6)}...${address.slice(-4)}`} + </span> + )}
9-11: Disable button while disconnecting and when no address; add a11y label.Prevents no‑op clicks and improves UX.
Apply this diff:
- const { disconnect } = useDisconnect(); + const { disconnect, isPending } = useDisconnect(); const handleDisconnect = () => { - disconnect(); + if (!address || isPending) return; + disconnect(); }; @@ - <Button + <Button variant="outline" onClick={handleDisconnect} - className="text-sm ml-auto" + className="text-sm ml-auto" + aria-label="Disconnect wallet" + disabled={!address || isPending} > Disconnect </Button>Also applies to: 19-25
5-8: Optional: prefer ENS name when available.Consider showing ENS (fallback to truncated address) for better UX.
src/lib/constants.ts (1)
2-43: Export a CurrencyId union type; keep runtime immutable.Exposing a typed union helps prop validation; optional freeze avoids accidental mutation at runtime.
Apply this diff:
-export const CURRENCY_ID = { +export const CURRENCY_ID = Object.freeze({ @@ - "ETH-BASE_BASE": "ETH-base-base", -} as const; + "ETH-BASE_BASE": "ETH-base-base", +} as const); + +export type CurrencyId = (typeof CURRENCY_ID)[keyof typeof CURRENCY_ID];src/components/payment-widget/constants.ts (1)
3-15: Large base64 icons bloat the initial bundle; move to static assets.Inlining >100KB of images inflates JS and hurts TTI. Prefer static files or lazy imports.
Apply this diff (example for metamask; replicate for others):
-export const ICONS = { - metamask: - "data:image/svg+xml;base64,....", - walletConnect: - "data:image/webp;base64,....", - coinbase: - "data:image/webp;base64,....", - safe: "data:image/webp;base64,....", - requestNetwork: - "data:image/svg+xml;base64,....", - defaultWallet: - "data:image/svg+xml;base64,....", -}; +export const ICONS = { + metamask: "/icons/wallets/metamask.svg", + walletConnect: "/icons/wallets/walletconnect.webp", + coinbase: "/icons/wallets/coinbase.webp", + safe: "/icons/wallets/safe.webp", + requestNetwork: "/icons/request.svg", + defaultWallet: "/icons/wallets/default.svg", +} as const;If you must inline, lazy‑load the ICONS map:
export const getIcons = async () => (await import("./icons.inline")).ICONS;src/components/payment-widget/utils/wagmi.ts (3)
18-27: Type connectors and drop the any cast.Use Connector[] to avoid the cast and catch type drift.
Apply this diff:
+import type { Connector } from "wagmi"; @@ -export const getWagmiConfig = (walletConnectProjectId?: string) => { - const connectors = [ +export const getWagmiConfig = (walletConnectProjectId?: string) => { + const connectors: Connector[] = [ injected(), coinbaseWallet({ appName: "Request Network Payment", }), metaMask(), safe(), ]; @@ - const connector = walletConnect({ + const connector = walletConnect({ projectId: walletConnectProjectId, metadata: { name: "Request Network Payment", description: "Pay with cryptocurrency using Request Network", url: "https://request.network", icons: ["https://request.network/favicon.ico"], }, showQrModal: true, }); - connectors.push(connector as any); + connectors.push(connector);Also applies to: 28-45
19-26: Optional: avoid duplicate listing of MetaMask with injected + metaMask.Having both may show two MetaMask entries in some UIs. If your UI lists connectors directly, consider dropping one.
47-58: Make RPC endpoints configurable per-chain and enable batching.File: src/components/payment-widget/utils/wagmi.ts (lines 47–58)
http() falls back to the chain's public RPC and can be rate-limited — pass per-chain authenticated RPC URLs and enable Batch JSON‑RPC via http(url, { batch: true }) to reduce requests. (wagmi.sh)
Apply this diff:
- const config = createConfig({ + const config = createConfig({ chains: [mainnet, sepolia, arbitrum, optimism, polygon, base], connectors, transports: { - [mainnet.id]: http(), - [sepolia.id]: http(), - [arbitrum.id]: http(), - [optimism.id]: http(), - [polygon.id]: http(), - [base.id]: http(), + [mainnet.id]: http(process.env.NEXT_PUBLIC_RPC_MAINNET, { batch: true }), + [sepolia.id]: http(process.env.NEXT_PUBLIC_RPC_SEPOLIA, { batch: true }), + [arbitrum.id]: http(process.env.NEXT_PUBLIC_RPC_ARBITRUM, { batch: true }), + [optimism.id]: http(process.env.NEXT_PUBLIC_RPC_OPTIMISM, { batch: true }), + [polygon.id]: http(process.env.NEXT_PUBLIC_RPC_POLYGON, { batch: true }), + [base.id]: http(process.env.NEXT_PUBLIC_RPC_BASE, { batch: true }), }, });WalletConnect option name confirmed: "showQrModal" (boolean, default true). (0.2.x.wagmi.sh)
src/components/payment-widget/types/html2pdf.d.ts (2)
23-35: Tighten option literal types to catch typos.html2canvas/jsPDF options can use string unions; consider narrowing to known values.
41-47: Refine return types; avoid any.If possible, specify concrete return types for save/outputPdf/then/catch to improve DX.
Example:
type OutputType = "blob" | "datauristring" | "arraybuffer"; outputPdf(type?: OutputType): Promise<Blob | string | ArrayBuffer>;src/components/payment-widget/types/index.ts (4)
8-11: Broaden PaymentError.error to unknown.Thrown values aren’t guaranteed to be Error; type as unknown and normalize at the boundary.
Apply this diff:
export interface PaymentError { type: "wallet" | "transaction" | "api" | "unknown"; - error: Error; + error: unknown; }
13-17: Align Transaction with viem types.Prefer viem Hex/bigint to match wagmi sendTransaction.
Apply this diff:
+import type { Hex } from "viem"; export interface Transaction { to: string; - data: string; - value: { hex: string }; + data?: Hex; + value?: bigint; }
19-28: Decide on numeric representation (number vs decimal string) and be consistent.ReceiptItem/ReceiptTotals mix number and string. Pick one (e.g., DecimalString) and enforce with zod.
Example:
type DecimalString = `${number}`;Also applies to: 60-65
6-6: Track the TODO with an issue.Convert the TODO into an issue to avoid lingering type drift.
I can open a follow‑up ticket and propose a type migration plan.
src/components/ui/combobox.tsx (2)
47-54: Align RHF value shape (array vs comma‑string) to avoid inconsistent form state.You call onChange with an array but bind the hidden input’s value as a comma‑separated string. This can create divergent RHF state vs DOM value. Prefer one shape. Minimal fix: send a string in onChange to match the hidden input.
Apply this diff:
- onChange({ target: { name, value: updatedSelection } }); + onChange({ target: { name, value: updatedSelection.join(",") } });Alternatively (cleaner): drop the hidden input and use RHF Controller/setValue to store string[] directly.
Please confirm which downstream expects: string[] or CSV string. I can provide a Controller-based version if string[] is required.
Also applies to: 98-99
24-27: Consider using the key as the display label (verify CURRENCY_ID shape).If CURRENCY_ID is a map like { USD: "usd", EUR: "eur" }, using value for the label will render lowercase codes. Using key for label improves readability.
If CURRENCY_ID already maps symbols to symbols, ignore this.
-const currencies = Object.entries(CURRENCY_ID).map(([key, value]) => ({ - value: value, - label: value, -})); +const currencies = Object.entries(CURRENCY_ID).map(([key, value]) => ({ + value, + label: key, +}));src/components/payment-widget/utils/chains.ts (1)
10-29: Prefer a lookup map with trim/lowercase and common aliases (e.g., “ethereum”).A map is easier to extend and adds minor resiliency.
Apply this diff:
-export const getChainFromNetwork = (network: string) => { - switch (network.toLowerCase()) { - case "mainnet": - return mainnet; - case "arbitrum": - return arbitrum; - case "base": - return base; - case "optimism": - return optimism; - // our currencies API uses matic for polygon for the moment - case "polygon": - case "matic": - return polygon; - case "sepolia": - return sepolia; - default: - throw new Error(`Unsupported network: ${network}`); - } -}; +export const getChainFromNetwork = (network: string) => { + const key = network.trim().toLowerCase(); + const map: Record<string, typeof mainnet> = { + mainnet, + ethereum: mainnet, + arbitrum, + base, + optimism, + polygon, + matic: polygon, // temporary alias used by currencies API + sepolia, + }; + const chain = map[key]; + if (!chain) throw new Error(`Unsupported network: ${network}`); + return chain; +};Ensure your Wagmi config includes all returned chains; otherwise chain switching can fail at runtime.
src/components/payment-widget/components/connection-handler.tsx (2)
13-30: Handle reconnecting state for a smoother UX.Surface a loading state while Wagmi is reconnecting.
Apply this diff:
- const { isConnected, isConnecting } = useAccount(); + const { isConnected, isConnecting, isReconnecting } = useAccount(); ... - <WalletConnectModal - isLoading={isConnecting} + <WalletConnectModal + isLoading={isConnecting || isReconnecting}
7-17: Optional: accept a render function to avoid constructing paymentModal when disconnected.This defers heavyweight modal construction until needed.
-interface ConnectionHandlerProps { - isOpen: boolean; - handleModalOpenChange: (open: boolean) => void; - paymentModal: ReactNode; -} +interface ConnectionHandlerProps { + isOpen: boolean; + handleModalOpenChange: (open: boolean) => void; + renderPaymentModal: () => ReactNode; +} ... -export function ConnectionHandler({ - isOpen, - handleModalOpenChange, - paymentModal, -}: ConnectionHandlerProps) { +export function ConnectionHandler({ + isOpen, + handleModalOpenChange, + renderPaymentModal, +}: ConnectionHandlerProps) { ... - return paymentModal; + return renderPaymentModal();Only apply if call sites can be updated easily.
src/components/payment-widget/context/web3-context.tsx (1)
17-25: Avoid stale WalletConnect config; simplify by dropping useMemoYou’re intentionally memoizing to prevent WC double-init, but the
useMemodepwalletConnectProjectIdis ignored onceconfigRef.currentis set, so a prop change won’t be reflected. If the prop is guaranteed stable per mount, simplify to a one-liner and make that contract explicit.Proposed change:
- const wagmiConfig = useMemo(() => { - if (!configRef.current) { - configRef.current = getWagmiConfig(walletConnectProjectId); - } - return configRef.current; - }, [walletConnectProjectId]); + const wagmiConfig = + (configRef.current ??= getWagmiConfig(walletConnectProjectId));If
walletConnectProjectIdcan change during the lifetime of this component, we should either recreate the config (and accept WC quirks) or warn/block on change. Can you confirm it’s stable?src/components/Playground/blocks/buyer-info.tsx (2)
3-3: Rename UI Error component to avoid shadowing global ErrorBiome is right: importing
Errorshadows the global. Rename the component import and usages.-import { Error } from "../../ui/error"; +import { Error as FormError } from "../../ui/error"; @@ - {errors.receiptInfo?.buyerInfo?.email?.message && ( - <Error>{errors.receiptInfo.buyerInfo.email.message}</Error> - )} + {errors.receiptInfo?.buyerInfo?.email?.message && ( + <FormError>{errors.receiptInfo.buyerInfo.email.message}</FormError> + )} @@ - <Error>{errors.receiptInfo.buyerInfo.address.street.message}</Error> + <FormError>{errors.receiptInfo.buyerInfo.address.street.message}</FormError> @@ - <Error>{errors.receiptInfo.buyerInfo.address.city.message}</Error> + <FormError>{errors.receiptInfo.buyerInfo.address.city.message}</FormError> @@ - <Error>{errors.receiptInfo.buyerInfo.address.state.message}</Error> + <FormError>{errors.receiptInfo.buyerInfo.address.state.message}</FormError> @@ - <Error>{errors.receiptInfo.buyerInfo.address.postalCode.message}</Error> + <FormError>{errors.receiptInfo.buyerInfo.address.postalCode.message}</FormError> @@ - <Error>{errors.receiptInfo.buyerInfo.address.country.message}</Error> + <FormError>{errors.receiptInfo.buyerInfo.address.country.message}</FormError>Also applies to: 69-71, 100-101, 120-122, 139-141, 161-163, 181-182
34-49: Small UX nits: mobile layout and autocomplete
- For better mobile stacking, consider
sm:w-1/2(full width on xs).- Add
autoCompleteattributes for email/name/address fields to improve autofill.Also applies to: 104-143, 145-184
src/components/payment-widget/README.md (1)
216-221: Docs/typing mismatches: onSuccess signature and totals value types
- The widget invokes
onSuccess(requestId, transactionReceipts)(see usage and PaymentModal), but the Props table lists(requestId: string) => void. Align the type.- In Basic Usage,
totalsare numbers, while your Receipt types use strings. Make examples consistent (use strings).-#### `onSuccess` (optional) -- **Type**: `(requestId: string) => void | Promise<void>` +#### `onSuccess` (optional) +- **Type**: `(requestId: string, transactionReceipts: TransactionReceipt[]) => void | Promise<void>` @@ - onSuccess={(requestId) => console.log("Payment successful:", requestId)} + onSuccess={(requestId, transactionReceipts) => + console.log("Payment successful:", requestId, transactionReceipts)} @@ - totals: { - total: 100, - totalUSD: 100.00, - totalDiscount: 0.00, - totalTax: 0.00, - }, + totals: { + total: "100.00", + totalUSD: "100.00", + totalDiscount: "0.00", + totalTax: "0.00", + },Also applies to: 77-79, 70-76
src/components/payment-widget/components/buyer-info-form.tsx (2)
51-59: Normalize nested address fields (trim/undefined) before submitCurrently only top-level fields are cleaned. Trim and convert empty address subfields to
undefinedtoo.- const cleanData: BuyerInfo = { - email: data.email, - firstName: cleanValue(data.firstName), - lastName: cleanValue(data.lastName), - businessName: cleanValue(data.businessName), - phone: cleanValue(data.phone), - address: data.address && hasAnyAddressField ? data.address : undefined, - }; + const cleanedAddress = + data.address && hasAnyAddressField + ? { + street: cleanValue(data.address.street), + city: cleanValue(data.address.city), + state: cleanValue(data.address.state), + country: cleanValue(data.address.country), + postalCode: cleanValue(data.address.postalCode), + } + : undefined; + + const cleanData: BuyerInfo = { + email: data.email.trim(), + firstName: cleanValue(data.firstName), + lastName: cleanValue(data.lastName), + businessName: cleanValue(data.businessName), + phone: cleanValue(data.phone), + address: cleanedAddress, + };
69-83: Add autocomplete hints for better UXSmall enhancement: set
autoCompleteon common fields (email, names, phone, address, locality, region, country, postal-code).- <Input + <Input id="email" type="email" placeholder="john.doe@example.com" + autoComplete="email" @@ - <Input + <Input id="firstName" placeholder="John" + autoComplete="given-name" @@ - <Input id="lastName" placeholder="Doe" {...register("lastName")} /> + <Input id="lastName" placeholder="Doe" autoComplete="family-name" {...register("lastName")} /> @@ - <Input + <Input id="phone" type="tel" placeholder="+1 (555) 123-4567" + autoComplete="tel" @@ - <Input + <Input id="address.street" placeholder="123 Main St, Apt 4B" + autoComplete="address-line1" @@ - <Input + <Input id="address.city" placeholder="San Francisco" + autoComplete="address-level2" @@ - <Input + <Input id="address.state" placeholder="CA" + autoComplete="address-level1" @@ - <Input + <Input id="address.country" placeholder="US" + autoComplete="country" @@ - <Input + <Input id="address.postalCode" placeholder="94105" + autoComplete="postal-code"Also applies to: 93-123, 128-146, 149-188, 193-238
src/components/payment-widget/components/receipt/receipt-template.tsx (2)
24-29: Fix name spacing and address formatting (missing spaces/commas)Strings like city/state/postal/country currently concatenate without spaces; name joins lack a space.
- {receipt.company.address.city},{""} - {receipt.company.address.state}{""} - {receipt.company.address.postalCode}{""} - {receipt.company.address.country} + {receipt.company.address.city}, {receipt.company.address.state} {receipt.company.address.postalCode} {receipt.company.address.country} @@ - {receipt.company.address.city},{""} - {receipt.company.address.state}{""} - {receipt.company.address.postalCode}{""} - {receipt.company.address.country} + {receipt.company.address.city}, {receipt.company.address.state} {receipt.company.address.postalCode} {receipt.company.address.country} @@ - {receipt.buyer.address.city}, {receipt.buyer.address.state}{""} - {receipt.buyer.address.postalCode}{""} - {receipt.buyer.address.country} + {receipt.buyer.address.city}, {receipt.buyer.address.state} {receipt.buyer.address.postalCode} {receipt.buyer.address.country} @@ - {[receipt.buyer.firstName, receipt.buyer.lastName] - .filter(Boolean) - .join("") || "Customer"} + {[receipt.buyer.firstName, receipt.buyer.lastName] + .filter(Boolean) + .join(" ") || "Customer"}Also applies to: 59-63, 84-87, 72-75
170-178: Verify currency used for “Subtotal”
receipt.payment.amountis rendered as crypto here, but in PaymentSuccess it is currently set fromamountInUsd(TODO comment). This prints a USD value with a crypto symbol. Either feed the paid crypto amount or render USD for subtotal until exchange data is wired.src/components/ui/radio-group.tsx (1)
28-39: Conflicting border classes
border-slate-200 border-slate-900are both set; the latter wins. Keep one and align dark mode appropriately.- className={cn( - "aspect-square h-4 w-4 rounded-full border border-slate-200 border-slate-900 text-slate-900 ring-offset-white focus:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-800 dark:border-slate-50 dark:text-slate-50 dark:ring-offset-slate-950 dark:focus-visible:ring-slate-300", - className - )} + className={cn( + "aspect-square h-4 w-4 rounded-full border border-slate-200 text-slate-900 ring-offset-white focus:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-800 dark:text-slate-50 dark:ring-offset-slate-950 dark:focus-visible:ring-slate-300", + className + )}src/components/Playground/blocks/seller-info.tsx (2)
3-3: Avoid shadowing global Error; alias the UI component.Prevents confusion and satisfies the linter.
Apply:
-import { Error } from "../../ui/error"; +import { Error as FieldError } from "../../ui/error"; @@ - {errors.receiptInfo?.companyInfo?.name?.message && ( - <Error>{errors.receiptInfo.companyInfo.name.message}</Error> - )} + {errors.receiptInfo?.companyInfo?.name?.message && ( + <FieldError>{errors.receiptInfo.companyInfo.name.message}</FieldError> + )} @@ - {errors.receiptInfo?.companyInfo?.address?.street?.message && ( - <Error>{errors.receiptInfo.companyInfo.address.street.message}</Error> - )} + {errors.receiptInfo?.companyInfo?.address?.street?.message && ( + <FieldError>{errors.receiptInfo.companyInfo.address.street.message}</FieldError> + )} @@ - {errors.receiptInfo?.companyInfo?.address?.city?.message && ( - <Error>{errors.receiptInfo.companyInfo.address.city.message}</Error> - )} + {errors.receiptInfo?.companyInfo?.address?.city?.message && ( + <FieldError>{errors.receiptInfo.companyInfo.address.city.message}</FieldError> + )} @@ - {errors.receiptInfo?.companyInfo?.address?.state?.message && ( - <Error>{errors.receiptInfo.companyInfo.address.state.message}</Error> - )} + {errors.receiptInfo?.companyInfo?.address?.state?.message && ( + <FieldError>{errors.receiptInfo.companyInfo.address.state.message}</FieldError> + )} @@ - {errors.receiptInfo?.companyInfo?.address?.postalCode?.message && ( - <Error>{errors.receiptInfo.companyInfo.address.postalCode.message}</Error> - )} + {errors.receiptInfo?.companyInfo?.address?.postalCode?.message && ( + <FieldError>{errors.receiptInfo.companyInfo.address.postalCode.message}</FieldError> + )} @@ - {errors.receiptInfo?.companyInfo?.address?.country?.message && ( - <Error>{errors.receiptInfo.companyInfo.address.country.message}</Error> - )} + {errors.receiptInfo?.companyInfo?.address?.country?.message && ( + <FieldError>{errors.receiptInfo.companyInfo.address.country.message}</FieldError> + )}Also applies to: 47-49, 101-103, 122-124, 141-143, 163-165, 182-184
34-50: Associate labels with inputs for a11y.Provide ids and htmlFor to improve accessibility and error focus.
Example for one field:
-<Label className="flex items-center"> +<Label htmlFor="company-name" className="flex items-center"> @@ -<Input +<Input + id="company-name" placeholder="ACME Corp"Apply similarly for address inputs.
Also applies to: 86-104, 106-145, 147-186
src/lib/validation.ts (2)
4-18: Tighten core validations (amount, wallet, fee, currencies).Strengthen data quality before sending to the widget/API.
Proposed changes:
export const PlaygroundValidation = z.object({ // Payment basics - amountInUsd: z.string().min(1, "Amount is required"), - recipientWallet: z.string().min(1, "Recipient wallet is required"), + amountInUsd: z + .string() + .regex(/^\d+(\.\d+)?$/, "Amount must be a valid number"), + recipientWallet: z + .string() + .min(1, "Recipient wallet is required"), + // .refine(isEthereumAddress, "Invalid Ethereum address"), // enable when validator is added @@ - feeInfo: z.object({ - feePercentage: z.string(), - feeAddress: z.string(), - }).optional(), + feeInfo: z + .object({ + feePercentage: z + .string() + .regex(/^\d+(\.\d+)?$/, "Fee percentage must be numeric"), + feeAddress: z.string(), // .refine(isEthereumAddress, "Invalid fee address"), + }) + .optional(), - supportedCurrencies: z.array(z.string()).min(1, "At least one supported currency is required"), + supportedCurrencies: z + .array(z.string()) + .min(1, "At least one supported currency is required"), })
56-73: Item/totals numeric-string normalization.Align on numeric string format to avoid mixed types across UI and PaymentStep.
Apply:
- quantity: z.number(), - unitPrice: z.string(), + quantity: z.number().int().positive(), + unitPrice: z.string().regex(/^\d+(\.\d+)?$/, "Invalid unit price"), @@ - total: z.string(), + total: z.string().regex(/^\d+(\.\d+)?$/, "Invalid total"), @@ - totals: z.object({ - totalDiscount: z.string(), - totalTax: z.string(), - total: z.string(), - totalUSD: z.string(), - }), + totals: z.object({ + totalDiscount: z.string().regex(/^\d+(\.\d+)?$/), + totalTax: z.string().regex(/^\d+(\.\d+)?$/), + total: z.string().regex(/^\d+(\.\d+)?$/), + totalUSD: z.string().regex(/^\d+(\.\d+)?$/), + }),src/components/PaymentStep.tsx (7)
21-29: Ensure invoice item fields use consistent types.unitPrice/total are numbers here; elsewhere (validation, widget types) they are often strings. Normalize to strings to avoid type drift.
- const invoiceItems = Object.values(tickets).map((ticket, index) => ({ + const invoiceItems = Object.values(tickets).map((ticket, index) => ({ id: ticket.id || (index + 1).toString(), description: ticket.name, quantity: ticket.quantity, - unitPrice: ticket.price, - total: ticket.price * ticket.quantity, + unitPrice: ticket.price.toFixed(2), + total: (ticket.price * ticket.quantity).toFixed(2), currency: "USD", }));
30-37: Remove debug log.Leftover console.log leaks noisy output.
- console.log("ma kaj mona", total, invoiceTotals)
30-35: Totals shape/type consistency.Consider stringifying totals to match item totals and potential widget expectations.
- const invoiceTotals = { - totalDiscount: 0, - totalTax: 0, - total: total, - totalUSD: total, - }; + const invoiceTotals = { + totalDiscount: "0", + totalTax: "0", + total: total.toFixed(2), + totalUSD: total.toFixed(2), + };
81-85: Externalize recipient wallet to env/config.Hard-coding a wallet address complicates environment changes and leaks configuration in code.
- recipientWallet="0xb07D2398d2004378cad234DA0EF14f1c94A530e4" + recipientWallet={process.env.NEXT_PUBLIC_RECIPIENT_WALLET ?? ""}Add NEXT_PUBLIC_RECIPIENT_WALLET to env docs.
124-125: receiptNumber vs invoiceNumber naming.Schema uses invoiceNumber; here it’s receiptNumber. Standardize to one to prevent downstream type mismatches.
78-141: No fallback UI when RN API client id missing.Currently renders nothing; show a clear message to help integrators.
- {clientId && ( + {clientId ? ( <PaymentWidget ... > ... </PaymentWidget> - )} + ) : ( + <div className="text-sm text-red-600"> + Missing NEXT_PUBLIC_RN_API_CLIENT_ID; set it to enable payments. + </div> + )}
80-81: Pass fixed-precision amount string.Ensure amountInUsd is a canonical decimal string.
- amountInUsd={total.toString()} + amountInUsd={total.toFixed(2)}src/components/payment-widget/components/wallet-connect-modal.tsx (1)
26-31: Connect errors are swallowed.Surface connect() errors to the user or log them; wagmi connect can reject.
- const handleConnect = (connector: Connector) => { - connect({ connector }); - }; + const handleConnect = async (connector: Connector) => { + try { + await connect({ connector }); + } catch (e) { + console.error("Wallet connect failed:", e); + } + };src/components/payment-widget/utils/currencies.ts (1)
25-37: Harden fetch and implement the “networks” filter.Better errors aid debugging; filtering cuts payload size.
-export const getConversionCurrencies = async ( - rnApiClientId: string, -): Promise<ConversionCurrency[]> => { - const response = await fetch( - `${RN_API_URL}/v2/currencies/${DEFAULT_CURRENCY}/conversion-routes`, - { - headers: { - "x-client-id": rnApiClientId, - "Content-Type": "application/json", - }, - }, - ); +export const getConversionCurrencies = async ( + rnApiClientId: string, + networks?: string[], // e.g. ["sepolia"] +): Promise<ConversionCurrency[]> => { + const qs = networks?.length ? `?networks=${encodeURIComponent(networks.join(","))}` : ""; + const url = `${RN_API_URL}/v2/currencies/${DEFAULT_CURRENCY}/conversion-routes${qs}`; + const response = await fetch(url, { + headers: { + "x-client-id": rnApiClientId, + "Content-Type": "application/json", + }, + }); @@ - if (!response.ok) { - throw new Error("Network response was not ok"); - } + if (!response.ok) { + const body = await response.text().catch(() => ""); + throw new Error(`Failed to fetch conversion routes (${response.status}): ${body}`); + } @@ - return data.conversionRoutes; + return data.conversionRoutes;Also applies to: 39-42
src/components/payment-widget/context/payment-widget-context.tsx (2)
6-7: ImportisAddressto validate recipient wallet early.Add a runtime guard to fail fast on invalid config instead of deferring to deeper layers.
-import type { TransactionReceipt, WalletClient } from "viem"; +import { isAddress, type TransactionReceipt, type WalletClient } from "viem";
69-76: Fail fast on missingrnApiClientIdand invalidrecipientWallet.Prevents confusing downstream API/tx errors and surfaces actionable messages.
}: PaymentWidgetProviderProps) { const { address } = useAccount(); + // Basic invariants: surface config errors early + if (!paymentConfig.rnApiClientId) { + throw new Error("paymentConfig.rnApiClientId is required"); + } + if (!isAddress(recipientWallet as `0x${string}`)) { + throw new Error("recipientWallet must be a valid EVM address"); + } + const isWalletOverride = walletAccount !== undefined;src/components/payment-widget/hooks/use-payment.ts (1)
54-67: Avoid creating a new Public Client per tx (micro‑perf).Hoist or memoize
createPublicClient({ chain: requiredChain, transport: http() })byrequiredChain.idto cut repeated instantiation.- ? async (hash: `0x${string}`): Promise<TransactionReceipt> => { - const publicClient = createPublicClient({ - chain: requiredChain, - transport: http(), - }); + ? async (hash: `0x${string}`): Promise<TransactionReceipt> => { + // TODO: memoize public client by chain id + const publicClient = createPublicClient({ chain: requiredChain, transport: http() });src/components/payment-widget/components/receipt/styles.css (1)
1-243: Optional: add print/PDF hints for html2pdf (page breaks, bleed, font fallbacks).Consider
.page-break-before,@page { margin: ... }, and embedding safe fallbacks for monospace fonts to improve PDF consistency.src/components/payment-widget/payment-widget.types.ts (1)
9-10: Mark arrays as readonly to discourage mutation.- supportedCurrencies: string[]; // an array of currency ids + supportedCurrencies: readonly string[]; // an array of currency idssrc/components/payment-widget/components/payment-confirmation.tsx (3)
168-186: Disable “Pay” when no wallet is connected; allow “Back” regardless.Prevents a no‑op click on Pay and avoids trapping users on this step.
<Button type="button" variant="outline" onClick={onBack} className="flex-1" - disabled={isExecuting || !connectedWalletAddress} + disabled={isExecuting} > Back </Button> <Button type="button" onClick={handleExecutePayment} className="flex-1" - disabled={isExecuting} + disabled={isExecuting || !connectedWalletAddress} > {isExecuting ? "Processing..." : "Pay"} </Button>
46-50: Remove unusedpreventDefault; the handler isn’t attached to a form submit.- const handleExecutePayment = async (e: React.FormEvent) => { - e.preventDefault(); + const handleExecutePayment = async () => {
154-156: Addrole="alert"andaria-livefor error block (a11y).- {localError && ( - <div className="p-4 bg-red-500/10 border border-slate-200 border-red-500/20 rounded-lg dark:bg-red-900/10 dark:border-slate-800 dark:border-red-900/20"> + {localError && ( + <div role="alert" aria-live="polite" className="p-4 bg-red-500/10 border border-slate-200 border-red-500/20 rounded-lg dark:bg-red-900/10 dark:border-slate-800 dark:border-red-900/20">src/components/payment-widget/components/payment-success.tsx (2)
74-83: Unawaited html2pdf chain may swallow errors; await the save.Without awaiting, exceptions inside the worker can bypass your try/catch.
- html2pdf() + await html2pdf() .set({ margin: 1, filename: `receipt-${receiptParams.metadata?.receiptNumber || "payment"}.pdf`, image: { type: "jpeg", quality: 0.98 }, html2canvas: { scale: 2 }, jsPDF: { unit: "in", format: "a4", orientation: "portrait" }, }) .from(element) .save();
38-63: Stabilize receipt generation across re-renders.
generateReceiptNumber()can change on re-render. Consider memoizingreceiptParamskeyed by its inputs.I can send a small
useMemopatch if you want.src/components/payment-widget/payment-widget.tsx (2)
34-40: Defensive check for supportedCurrencies.Avoid possible runtime error if
supportedCurrenciesis undefined.- if (supportedCurrencies.length === 0) { + if (!supportedCurrencies || supportedCurrencies.length === 0) { console.error( "PaymentWidget: supportedCurrencies is required in paymentConfig", ); isButtonDisabled = true; }
26-33: Reduce console spam from config validation.Errors log on every render. Log once when the disabled reason changes, or gate behind
NODE_ENV !== "production".I can provide a tiny
useEffectto log only on transitions.src/components/Playground/index.tsx (4)
120-129: Normalize supportedCurrencies to an array before codegen.
CurrencyComboboxcan yield a comma-delimited string; codegen should handle both shapes to avoid emittingsupportedCurrencies: "ETH,USDC"in the snippet.- const paymentConfig = formValues.paymentConfig; - const cleanedPaymentConfig = { - ...paymentConfig, - supportedCurrencies: paymentConfig.supportedCurrencies?.length - ? paymentConfig.supportedCurrencies - : undefined, + const paymentConfig = formValues.paymentConfig; + const normalizedSupported = + Array.isArray(paymentConfig.supportedCurrencies) + ? paymentConfig.supportedCurrencies + : typeof paymentConfig.supportedCurrencies === "string" + ? paymentConfig.supportedCurrencies.split(",").map((s) => s.trim()).filter(Boolean) + : []; + const cleanedPaymentConfig = { + ...paymentConfig, + supportedCurrencies: normalizedSupported.length ? normalizedSupported : undefined, feeInfo: (paymentConfig.feeInfo?.feeAddress || paymentConfig.feeInfo?.feePercentage !== "0") ? paymentConfig.feeInfo : undefined, };
131-136: Typo in variable name (cleanedreceiptInfo) and downstream usage.- const cleanedreceiptInfo = { + const cleanedReceiptInfo = { ...formValues.receiptInfo, buyerInfo: Object.values(formValues.receiptInfo.buyerInfo || {}).some(val => val) ? formValues.receiptInfo.buyerInfo : undefined, }; @@ - receiptInfo={${formatObject(cleanedreceiptInfo, 2)}} + receiptInfo={${formatObject(cleanedReceiptInfo, 2)}}Also applies to: 143-144
31-37: Preview should default to disabled until a real API Client ID is provided.Using "YOUR_CLIENT_ID_HERE" makes the button enabled and leads to runtime failures.
- rnApiClientId: "YOUR_CLIENT_ID_HERE", + rnApiClientId: "",
80-81: Remove unused ref.
codeRefis declared but never used meaningfully.- const codeRef = useRef<HTMLPreElement>(null); ... - <pre - ref={codeRef} - className="bg-gray-100 text-gray-800 p-4 rounded-lg overflow-x-auto" - > + <pre className="bg-gray-100 text-gray-800 p-4 rounded-lg overflow-x-auto"> <code className="language-jsx">{integrationCode}</code> </pre>Also applies to: 236-241
src/components/Playground/blocks/customize.tsx (1)
5-5: Avoid shadowing global Error; alias the component import.Biome warning is valid. Alias the UI Error component and update usages.
-import { Error } from "../../ui/error"; +import { Error as FormError } from "../../ui/error"; @@ - {errors.recipientWallet?.message && ( - <Error>{errors.recipientWallet.message}</Error> - )} + {errors.recipientWallet?.message && ( + <FormError>{errors.recipientWallet.message}</FormError> + )} @@ - {errors.paymentConfig?.rnApiClientId?.message && ( - <Error>{errors.paymentConfig.rnApiClientId.message}</Error> - )} + {errors.paymentConfig?.rnApiClientId?.message && ( + <FormError>{errors.paymentConfig.rnApiClientId.message}</FormError> + )} @@ - {errors.paymentConfig?.supportedCurrencies?.message && ( - <Error>{errors.paymentConfig.supportedCurrencies.message}</Error> - )} + {errors.paymentConfig?.supportedCurrencies?.message && ( + <FormError>{errors.paymentConfig.supportedCurrencies.message}</FormError> + )} @@ - <Input + <Input type="number" step="0.01" readOnly value={typeof item.total === 'string' ? parseFloat(item.total) || 0 : item.total} className="bg-gray-50" /> @@ - <span>Subtotal:</span> + <span>Subtotal:</span> <span>${(parseFloat(formValues.receiptInfo.totals.total) || 0).toFixed(2)}</span>Also applies to: 108-111, 126-129, 152-155, 224-226, 252-253, 260-261, 268-281
src/components/payment-widget/utils/receipt.ts (4)
17-23: Clarify walletAddress requirements and add runtime validation.
If buyers/companies can be email‑only, consider makingwalletAddressoptional or validate at runtime (e.g., with viem’sisAddress) before creating receipts to avoid bad data flowing into PDFs.
39-45: Stronger, less‑predictable receipt IDs (avoid Math.random collisions).
Use crypto‑backed randomness and a shorter base36 timestamp. This reduces collision risk and predictability.Apply this diff:
-export const generateReceiptNumber = (prefix: string = "REC"): string => { - const timestamp = Date.now(); - const random = Math.floor(Math.random() * 1000) - .toString() - .padStart(3, "0"); - return `${prefix}-${timestamp}-${random}`; -}; +export const generateReceiptNumber = (prefix: string = "REC"): string => { + const ts = Date.now().toString(36); + const rand = + (globalThis.crypto?.getRandomValues + ? Array.from(globalThis.crypto.getRandomValues(new Uint8Array(4))) + .map((b) => b.toString(16).padStart(2, "0")) + .join("") + : Math.random().toString(16).slice(2)).slice(0, 8); + return `${prefix}-${ts}-${rand}`; +};If your TS config lacks DOM types on the server, we can polyfill or guard further.
47-60: formatUSDAmount: sanitize common human inputs (commas/underscores/spaces).
Pre-clean the string so"1,234.50"or"1_234.50"formats correctly.Apply this diff:
-export const formatUSDAmount = (amount: string): string => { - const numericAmount = parseFloat(amount); +export const formatUSDAmount = (amount: string): string => { + const sanitized = amount.replace(/[,_\s]/g, ""); + const numericAmount = Number(sanitized);
62-67: formatCryptoAmount: consider standardized formatting.
Consider using a formatter with max 6–8 decimals and a narrow no‑break space: e.g.,formatUnits(amountInWei, decimals)from viem +Intl.NumberFormat, then${formatted}\u202F${currency}. Keeps outputs readable and locale‑aware.src/components/payment-widget/utils/payment.ts (7)
61-85: Fix misleading warning message in normalizeValue.
The warning says “defaulting to 0” but the code throws. Adjust message to avoid confusion.Apply this diff:
- console.warn("Unknown value format, defaulting to 0:", value); - throw new Error("Unknown value format"); + console.warn("Unknown tx.value format:", value); + throw new Error("Unknown value format");
93-107: Add runtime validation for to/data before submitting a tx.
Guard against malformed API responses early withisAddress/isHex; fail fast with a typedPaymentError.Apply this diff:
- const hash = await sendTransaction({ + // Basic runtime validation + if (!isAddress(tx.to)) { + throw { type: "transaction", error: new Error(`Invalid to address: ${tx.to}`) } as PaymentError; + } + if (!isHex(tx.data)) { + throw { type: "transaction", error: new Error("Invalid data hex") } as PaymentError; + } + const hash = await sendTransaction({ to: tx.to as `0x${string}`, data: tx.data as `0x${string}`, value: normalizeValue(tx.value), });Add this import:
import { isAddress, isHex } from "viem";
154-167: Pre‑validate payer/recipient wallet addresses before calling the API.
Surface “wallet” errors early instead of returning an API error after a round‑trip.Apply this diff:
try { + if (!isAddress(paymentParams.payerWallet) || !isAddress(paymentParams.recipientWallet)) { + throw { type: "wallet", error: new Error("Invalid payer or recipient wallet address") } as PaymentError; + } const response = await createPayout(rnApiClientId, paymentParams);(Add
isAddressimport as noted above.)
128-145: Minor: include Accept header for explicit JSON negotiation.
Harmless clarity; some proxies behave better with explicitAccept.Apply this diff:
const response = await fetch(`${RN_API_URL}/v2/payouts`, { method: "POST", headers: { "x-client-id": rnApiClientId, "Content-Type": "application/json", + "Accept": "application/json", },
149-153: Enrich response with paymentReference and metadata for UX/support.
These are already available from the API response; returning them simplifies UI flows and debugging.Apply these diffs:
export interface PaymentResponse { requestId: string; transactionReceipts: TransactionReceipt[]; + paymentReference?: string; + metadata?: PayoutAPIResponse["metadata"]; }- return { requestId: data.requestId, transactionReceipts }; + return { + requestId: data.requestId, + transactionReceipts, + paymentReference: data.paymentReference, + metadata: data.metadata, + };Also applies to: 192-193
116-147: Optional: idempotency key and request timeout.
Add an “Idempotency-Key” header and a short timeout (AbortController) to harden the call against retries/hangs. I can wire this with a small helper if you want.
61-85: Tests for normalizeValue edge cases.
Add unit tests covering: large safe integers, hex strings, decimal strings (should throw), and{ type, hex }objects.I can draft a minimal test suite if helpful.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (43)
components.json(1 hunks)package.json(1 hunks)src/app/playground/page.tsx(1 hunks)src/components/PaymentStep.tsx(3 hunks)src/components/Playground.tsx(0 hunks)src/components/Playground/blocks/buyer-info.tsx(1 hunks)src/components/Playground/blocks/customize.tsx(1 hunks)src/components/Playground/blocks/seller-info.tsx(1 hunks)src/components/Playground/index.tsx(1 hunks)src/components/payment-widget/README.md(1 hunks)src/components/payment-widget/components/buyer-info-form.tsx(1 hunks)src/components/payment-widget/components/connection-handler.tsx(1 hunks)src/components/payment-widget/components/currency-select.tsx(1 hunks)src/components/payment-widget/components/disconnect-wallet.tsx(1 hunks)src/components/payment-widget/components/payment-confirmation.tsx(1 hunks)src/components/payment-widget/components/payment-modal.tsx(1 hunks)src/components/payment-widget/components/payment-success.tsx(1 hunks)src/components/payment-widget/components/receipt/receipt-template.tsx(1 hunks)src/components/payment-widget/components/receipt/styles.css(1 hunks)src/components/payment-widget/components/wallet-connect-modal.tsx(1 hunks)src/components/payment-widget/constants.ts(1 hunks)src/components/payment-widget/context/payment-widget-context.tsx(1 hunks)src/components/payment-widget/context/web3-context.tsx(1 hunks)src/components/payment-widget/hooks/use-payment.ts(1 hunks)src/components/payment-widget/payment-widget.tsx(1 hunks)src/components/payment-widget/payment-widget.types.ts(1 hunks)src/components/payment-widget/types/html2pdf.d.ts(1 hunks)src/components/payment-widget/types/index.ts(1 hunks)src/components/payment-widget/utils/chains.ts(1 hunks)src/components/payment-widget/utils/currencies.ts(1 hunks)src/components/payment-widget/utils/payment.ts(1 hunks)src/components/payment-widget/utils/receipt.ts(1 hunks)src/components/payment-widget/utils/wagmi.ts(1 hunks)src/components/ui/button.tsx(1 hunks)src/components/ui/combobox.tsx(1 hunks)src/components/ui/dialog.tsx(1 hunks)src/components/ui/input.tsx(1 hunks)src/components/ui/radio-group.tsx(1 hunks)src/components/ui/select.tsx(1 hunks)src/lib/constants.ts(1 hunks)src/lib/currencies.ts(0 hunks)src/lib/utils.ts(1 hunks)src/lib/validation.ts(1 hunks)
💤 Files with no reviewable changes (2)
- src/lib/currencies.ts
- src/components/Playground.tsx
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-11-07T02:52:54.845Z
Learnt from: MantisClone
PR: RequestNetwork/rn-checkout#17
File: src/components/ui/tabs.tsx:85-94
Timestamp: 2024-11-07T02:52:54.845Z
Learning: In the playground component (`src/components/ui/tabs.tsx`), ARIA attributes are not necessary; focus on adding them to the payment widget instead.
Applied to files:
src/app/playground/page.tsxsrc/components/payment-widget/payment-widget.tsxsrc/components/Playground/index.tsx
📚 Learning: 2024-11-07T06:08:13.072Z
Learnt from: aimensahnoun
PR: RequestNetwork/rn-checkout#17
File: src/components/Playground.tsx:46-46
Timestamp: 2024-11-07T06:08:13.072Z
Learning: In the Playground component, the default value for `enableBuyerInfo` is now `true`, and this is reflected in the Zod validation schema.
Applied to files:
src/components/Playground/blocks/buyer-info.tsxsrc/lib/validation.ts
🧬 Code graph analysis (24)
src/components/payment-widget/components/currency-select.tsx (2)
src/components/payment-widget/utils/currencies.ts (3)
ConversionCurrency(3-11)getConversionCurrencies(22-42)getSymbolOverride(44-51)src/components/payment-widget/context/payment-widget-context.tsx (1)
usePaymentWidgetContext(121-131)
src/components/payment-widget/components/receipt/receipt-template.tsx (1)
src/components/payment-widget/utils/receipt.ts (4)
ReceiptData(25-37)formatReceiptDate(69-75)formatCryptoAmount(62-67)formatUSDAmount(47-60)
src/components/payment-widget/context/web3-context.tsx (1)
src/components/payment-widget/utils/wagmi.ts (1)
getWagmiConfig(18-61)
src/components/payment-widget/components/payment-confirmation.tsx (5)
src/components/payment-widget/utils/currencies.ts (2)
ConversionCurrency(3-11)getSymbolOverride(44-51)src/components/payment-widget/types/index.ts (2)
BuyerInfo(45-58)PaymentError(8-11)src/components/payment-widget/context/payment-widget-context.tsx (1)
usePaymentWidgetContext(121-131)src/components/payment-widget/hooks/use-payment.ts (1)
usePayment(22-116)src/components/payment-widget/utils/payment.ts (1)
executePayment(154-205)
src/components/payment-widget/hooks/use-payment.ts (3)
src/components/payment-widget/utils/chains.ts (1)
getChainFromNetwork(10-29)src/components/payment-widget/utils/payment.ts (6)
SendTransactionFunction(49-49)TxParams(43-47)WaitForTransactionFunction(51-53)PaymentParams(5-24)PaymentResponse(149-152)executePayment(154-205)src/components/payment-widget/types/index.ts (1)
PaymentError(8-11)
src/components/payment-widget/payment-widget.types.ts (1)
src/components/payment-widget/types/index.ts (3)
FeeInfo(1-4)ReceiptInfo(67-73)PaymentError(8-11)
src/components/payment-widget/context/payment-widget-context.tsx (2)
src/components/payment-widget/types/index.ts (3)
FeeInfo(1-4)ReceiptInfo(67-73)PaymentError(8-11)src/components/payment-widget/payment-widget.types.ts (1)
PaymentWidgetProps(48-68)
src/components/payment-widget/components/payment-modal.tsx (8)
src/components/payment-widget/context/payment-widget-context.tsx (1)
usePaymentWidgetContext(121-131)src/components/payment-widget/utils/currencies.ts (1)
ConversionCurrency(3-11)src/components/payment-widget/types/index.ts (1)
BuyerInfo(45-58)src/components/payment-widget/components/disconnect-wallet.tsx (1)
DisconnectWallet(5-28)src/components/payment-widget/components/currency-select.tsx (1)
CurrencySelect(19-134)src/components/payment-widget/components/buyer-info-form.tsx (1)
BuyerInfoForm(15-265)src/components/payment-widget/components/payment-confirmation.tsx (1)
PaymentConfirmation(25-189)src/components/payment-widget/components/payment-success.tsx (1)
PaymentSuccess(23-145)
src/components/Playground/blocks/buyer-info.tsx (4)
src/lib/validation.ts (1)
PlaygroundFormData(76-76)src/components/ui/section-header.tsx (1)
SectionHeader(5-14)src/lib/utils.ts (1)
cn(4-6)src/components/ui/error.tsx (1)
Error(3-5)
src/components/payment-widget/components/wallet-connect-modal.tsx (3)
src/components/payment-widget/constants.ts (1)
ICONS(3-15)src/components/ui/dialog.tsx (5)
Dialog(112-112)DialogContent(117-117)DialogHeader(118-118)DialogTitle(120-120)DialogDescription(121-121)src/components/ui/button.tsx (1)
Button(56-56)
src/components/payment-widget/payment-widget.tsx (6)
src/components/payment-widget/context/payment-widget-context.tsx (2)
usePaymentWidgetContext(121-131)PaymentWidgetProvider(59-119)src/components/payment-widget/constants.ts (1)
ICONS(3-15)src/components/payment-widget/components/payment-modal.tsx (1)
PaymentModal(26-133)src/components/payment-widget/components/connection-handler.tsx (1)
ConnectionHandler(13-30)src/components/payment-widget/payment-widget.types.ts (1)
PaymentWidgetProps(48-68)src/components/payment-widget/context/web3-context.tsx (1)
Web3Provider(10-32)
src/components/payment-widget/utils/receipt.ts (1)
src/components/payment-widget/types/index.ts (3)
BuyerInfo(45-58)CompanyInfo(30-43)ReceiptItem(19-28)
src/components/PaymentStep.tsx (1)
src/components/payment-widget/payment-widget.tsx (1)
PaymentWidget(89-116)
src/components/payment-widget/components/buyer-info-form.tsx (1)
src/components/payment-widget/types/index.ts (1)
BuyerInfo(45-58)
src/components/payment-widget/components/payment-success.tsx (5)
src/components/payment-widget/utils/currencies.ts (1)
ConversionCurrency(3-11)src/components/payment-widget/types/index.ts (1)
BuyerInfo(45-58)src/components/payment-widget/context/payment-widget-context.tsx (1)
usePaymentWidgetContext(121-131)src/components/payment-widget/utils/receipt.ts (3)
CreateReceiptParams(77-89)generateReceiptNumber(39-45)createReceipt(91-107)src/components/payment-widget/components/receipt/receipt-template.tsx (1)
ReceiptPDFTemplate(11-196)
src/components/payment-widget/utils/payment.ts (2)
src/components/payment-widget/types/index.ts (2)
FeeInfo(1-4)PaymentError(8-11)src/components/payment-widget/constants.ts (1)
RN_API_URL(1-2)
src/components/Playground/blocks/seller-info.tsx (4)
src/lib/validation.ts (1)
PlaygroundFormData(76-76)src/components/ui/section-header.tsx (1)
SectionHeader(5-14)src/lib/utils.ts (1)
cn(4-6)src/components/ui/error.tsx (1)
Error(3-5)
src/components/Playground/index.tsx (5)
src/lib/validation.ts (1)
PlaygroundValidation(4-74)src/components/Playground/blocks/customize.tsx (1)
CustomizeForm(15-286)src/components/Playground/blocks/seller-info.tsx (1)
SellerForm(11-189)src/components/Playground/blocks/buyer-info.tsx (1)
BuyerForm(11-187)src/components/payment-widget/payment-widget.tsx (1)
PaymentWidget(89-116)
src/components/Playground/blocks/customize.tsx (5)
src/lib/validation.ts (1)
PlaygroundFormData(76-76)src/components/ui/section-header.tsx (1)
SectionHeader(5-14)src/lib/utils.ts (1)
cn(4-6)src/components/ui/error.tsx (1)
Error(3-5)src/components/ui/combobox.tsx (1)
CurrencyCombobox(35-101)
src/components/payment-widget/components/connection-handler.tsx (1)
src/components/payment-widget/components/wallet-connect-modal.tsx (1)
WalletConnectModal(21-106)
src/components/payment-widget/utils/currencies.ts (1)
src/components/payment-widget/constants.ts (1)
RN_API_URL(1-2)
src/components/ui/select.tsx (1)
src/lib/utils.ts (1)
cn(4-6)
src/components/ui/input.tsx (1)
src/lib/utils.ts (1)
cn(4-6)
src/components/ui/radio-group.tsx (1)
src/lib/utils.ts (1)
cn(4-6)
🪛 Biome (2.1.2)
src/components/Playground/blocks/buyer-info.tsx
[error] 3-3: Do not shadow the global "Error" property.
Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.
(lint/suspicious/noShadowRestrictedNames)
src/components/Playground/blocks/seller-info.tsx
[error] 3-3: Do not shadow the global "Error" property.
Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.
(lint/suspicious/noShadowRestrictedNames)
src/components/Playground/blocks/customize.tsx
[error] 5-5: Do not shadow the global "Error" property.
Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.
(lint/suspicious/noShadowRestrictedNames)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
♻️ Duplicate comments (4)
src/components/PaymentStep.tsx (1)
109-122: BuyerInfo address now correctly nested.
Matches prior guidance; no further action.src/lib/validation.ts (3)
2-2: Validator import now used — make sure dependency is pinned and interop flags are set.Great that the import is actually used now. Please verify
validatoris listed and pinned in package.json and that TS interop flags won’t break default import semantics.#!/bin/bash set -euo pipefail echo "== Check validator dep ==" rg -n --no-heading '"validator"\s*:' package.json || echo "Missing validator dep in root package.json" fd package.json -HI -t f -x rg -n --no-heading '"validator"\s*:' {} || true echo -e "\n== Check tsconfig interop flags ==" fd tsconfig.json -HI -t f -x bash -lc 'f="{}"; echo "-> $f"; rg -n --no-heading "\"esModuleInterop\"\\s*:\\s*true" "$f" || echo "esModuleInterop not true"; rg -n --no-heading "\"allowSyntheticDefaultImports\"\\s*:\\s*true" "$f" || echo "allowSyntheticDefaultImports not true"'
27-33: Buyer info: email rule is correct; consider trimming string fields.Email validation matches earlier guidance. Add
.trim()to avoid whitespace‑only values for name/business/phone.
41-54: Company info matches prior guidance (taxId optional).Schema looks consistent with UI. Consider
.trim()on string fields.
🧹 Nitpick comments (13)
src/components/PaymentStep.tsx (7)
21-28: Normalize money strings to 2 decimals to avoid float drift in receipts.
Switch to toFixed(2) for price fields; consider quantity type expectations.- const invoiceItems = Object.values(tickets).map((ticket, index) => ({ + const invoiceItems = Object.values(tickets).map((ticket, index) => ({ id: ticket.id || (index + 1).toString(), description: ticket.name, quantity: ticket.quantity, - unitPrice: ticket.price.toString(), - total: (ticket.price * ticket.quantity).toString(), + unitPrice: ticket.price.toFixed(2), + total: (ticket.price * ticket.quantity).toFixed(2), currency: "USD", }));If the widget’s ReceiptItem.quantity is a string, convert accordingly.
30-37: Remove debug log; standardize totals to 2‑decimal strings.
The stray console.log leaks to prod and totals should match invoiceItems formatting.- const invoiceTotals = { - totalDiscount: '0', - totalTax: '0', - total: total.toString(), - totalUSD: total.toString(), - }; - console.log("ma kaj mona", total, invoiceTotals) + const invoiceTotals = { + totalDiscount: "0.00", + totalTax: "0.00", + total: total.toFixed(2), + totalUSD: total.toFixed(2), + };
80-81: Use fixed-precision for amountInUsd and avoid hard-coded wallet in code.
- amountInUsd should match receipt totals (2 decimals).
- Prefer env-configured recipient wallet for env parity.
- amountInUsd={total.toString()} - recipientWallet="0xb07D2398d2004378cad234DA0EF14f1c94A530e4" + amountInUsd={total.toFixed(2)} + recipientWallet={process.env.NEXT_PUBLIC_RECIPIENT_WALLET as string}
78-80: Gate widget when total is 0 and show a helpful fallback when clientId is missing.
Prevents initiating a $0 payment and avoids silent no-render.- {clientId && ( + {clientId ? ( <PaymentWidget ... > ... </PaymentWidget> - )} + ) : ( + <p className="text-sm text-gray-500"> + Payment unavailable: set NEXT_PUBLIC_RN_API_CLIENT_ID. + </p> + )}Optionally also require total > 0: {clientId && total > 0 ? ( ... ) : (...) }
127-132: Avoid back-navigation to a completed payment; simplify redirect.
Use replace to prevent returning to the payment page; the timeout is fine if intentional.- onSuccess={() => { - clearTickets(); - setTimeout(() => { - router.push("/"); - }, 10000); - }} + onSuccess={() => { + clearTickets(); + setTimeout(() => { + router.replace("/"); + }, 10000); + }}
137-139: Use a semantic button for a11y and keyboard support.
A div is not focusable/clickable by default.- <div className="px-8 py-2 bg-[#099C77] text-white rounded-lg hover:bg-[#087f63] transition-colors text-center"> - Pay with crypto - </div> + <button + type="button" + className="px-8 py-2 bg-[#099C77] text-white rounded-lg hover:bg-[#087f63] transition-colors text-center" + > + Pay with crypto + </button>
82-89: Add walletConnectProjectId to PaymentStep paymentConfig (enables WalletConnect)PaymentWidget passes paymentConfig.walletConnectProjectId into Web3Provider/getWagmiConfig; provide it from env so the WalletConnect connector is enabled.
File: src/components/PaymentStep.tsx (lines 82–89)
paymentConfig={{ rnApiClientId: clientId, + walletConnectProjectId: process.env + .NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID as string, supportedCurrencies: [ "ETH-sepolia-sepolia", "fUSDT-sepolia", "FAU-sepolia", ], }}src/lib/validation.ts (6)
1-1: Prefer named import from zod for broader TS/ESM compatibility.Default import relies on tsconfig interop flags; named import is the canonical, friction‑free choice.
Apply:
-import z from "zod"; +import { z } from "zod";If you keep the default import, ensure
esModuleInteropandallowSyntheticDefaultImportsare enabled.
3-3: Consider adding .strict() and a top-level superRefine for cross-field checks.This prevents silent typos and enables validating
items[*].currency∈paymentConfig.supportedCurrencies.Example (outside diff, append after the object):
export const PlaygroundValidation = z.object({...}) .strict() .superRefine((data, ctx) => { data.receiptInfo.items.forEach((item, i) => { if (item.currency && !data.paymentConfig.supportedCurrencies.includes(item.currency)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Item currency must be in supportedCurrencies", path: ["receiptInfo","items",i,"currency"] }); } }); });
16-16: Tighten element validation inside the currencies array.Even before switching to an enum, trim and disallow empty strings.
- supportedCurrencies: z.array(z.string()).min(1, "At least one supported currency is required"), + supportedCurrencies: z.array(z.string().trim().min(1)).min(1, "At least one supported currency is required"),
65-70: Totals: enforce decimal strings; consider naming consistency.Add numeric validation; optionally rename
totalUSD→totalUsdfor camelCase consistency withamountInUsd.- totals: z.object({ - totalDiscount: z.string(), - totalTax: z.string(), - total: z.string(), - totalUSD: z.string(), - }), + totals: z.object({ + totalDiscount: z.string().trim().regex(/^\d+(\.\d{1,18})?$/, "Use a decimal string"), + totalTax: z.string().trim().regex(/^\d+(\.\d{1,18})?$/, "Use a decimal string"), + total: z.string().trim().regex(/^\d+(\.\d{1,18})?$/, "Use a decimal string"), + totalUSD: z.string().trim().regex(/^\d+(\.\d{1,2})?$/, "Use a 2‑decimal USD amount"), + }),
71-71: Invoice number: trim to avoid whitespace values.- invoiceNumber: z.string().optional(), + invoiceNumber: z.string().trim().optional(),
2-6: Optional: use viem’s isAddress for EVM addresses (project already uses Wagmi/Viem).Reduces an extra dependency and supports checksum validation idiomatically.
Apply:
-import isEthereumAddress from "validator/lib/isEthereumAddress"; +import { isAddress as isEthereumAddress } from "viem";(Keep the zero‑address guard.)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
components.json(1 hunks)src/components/PaymentStep.tsx(3 hunks)src/lib/validation.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- components.json
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: MantisClone
PR: RequestNetwork/rn-checkout#17
File: src/components/ui/tabs.tsx:85-94
Timestamp: 2024-11-07T02:52:54.845Z
Learning: In the playground component (`src/components/ui/tabs.tsx`), ARIA attributes are not necessary; focus on adding them to the payment widget instead.
📚 Learning: 2024-11-07T06:08:13.072Z
Learnt from: aimensahnoun
PR: RequestNetwork/rn-checkout#17
File: src/components/Playground.tsx:46-46
Timestamp: 2024-11-07T06:08:13.072Z
Learning: In the Playground component, the default value for `enableBuyerInfo` is now `true`, and this is reflected in the Zod validation schema.
Applied to files:
src/lib/validation.ts
📚 Learning: 2025-09-17T15:37:38.827Z
Learnt from: bassgeta
PR: RequestNetwork/rn-checkout#28
File: src/components/payment-widget/components/payment-success.tsx:57-60
Timestamp: 2025-09-17T15:37:38.827Z
Learning: In the Request Network checkout project, property mismatches between receiptInfo.receiptNumber and receiptInfo.invoiceNumber in payment widget components should be ignored as they involve outside dependencies.
Applied to files:
src/components/PaymentStep.tsx
🧬 Code graph analysis (1)
src/components/PaymentStep.tsx (1)
src/components/payment-widget/payment-widget.tsx (1)
PaymentWidget(89-116)
🔇 Additional comments (5)
src/components/PaymentStep.tsx (2)
6-6: LGTM: Local widget import matches the new ShadCN-based integration.
38-38: Ensure env is wired and documented.
NEXT_PUBLIC_RN_API_CLIENT_ID must be set at build-time; add to .env.example and README.src/lib/validation.ts (3)
20-23: LGTM on optional UI flags.Optional booleans are fine and align with progressive enhancement.
75-75: Type export is correct and helpful.
6-6: Question: ENS support for recipientWallet?If the widget allows ENS, adjust to accept
z.union([address, ensName])and resolve at submit time; otherwise current rule is fine.
There was a problem hiding this 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
♻️ Duplicate comments (5)
src/lib/validation.ts (5)
16-16: Constrain supportedCurrencies to canonical Request IDs.Free strings risk runtime failures with the widget.
- supportedCurrencies: z.array(z.string()).min(1, "At least one supported currency is required"), + supportedCurrencies: z + .array(z.string().trim()) + .min(1, "At least one supported currency is required") + .refine((arr) => arr.every((id) => isValidCurrencyId(id)), "Unsupported currency id"),If the widget exports no schema, validate against a local allowlist from the currencies API or @requestnetwork/currency.
Does the Request Network Payment Widget specify the canonical format for supportedCurrencies IDs (e.g., "USDC-mainnet"), and does it export a type/schema, or should we validate against the currencies API/@requestnetwork/currency list?Outside this hunk, define/import
isValidCurrencyIdfrom your currency constants/util.
5-5: Harden USD amount validation (trim, numeric, > 0, 2 decimals).Current rule accepts whitespace/non-numeric strings.
- amountInUsd: z.string().min(1, "Amount is required"), + amountInUsd: z + .string() + .trim() + .regex(/^\d+(\.\d{1,2})?$/, "Enter a valid USD amount (e.g., 10 or 10.50)") + .refine((v) => parseFloat(v) > 0, "Amount must be > 0"),
6-6: Block the zero address and trim recipient wallet.Valid checksum can still be the zero address.
- recipientWallet: z.string().min(1, "Recipient wallet is required").refine(isEthereumAddress, "Invalid Ethereum address format"), + recipientWallet: z + .string() + .trim() + .min(1, "Recipient wallet is required") + .refine(isEthereumAddress, "Invalid Ethereum address format") + .refine((a) => a.toLowerCase() !== "0x0000000000000000000000000000000000000000", "Recipient cannot be the zero address"),
12-15: Harden feeInfo (numeric range and non-zero address).Prevent invalid fee percentages and burn addresses.
- feeInfo: z.object({ - feePercentage: z.string(), - feeAddress: z.string(), - }).optional(), + feeInfo: z + .object({ + feePercentage: z + .string() + .trim() + .regex(/^\d+(\.\d{1,2})?$/, "Fee must be a number") + .refine((v) => { + const n = parseFloat(v); + return !Number.isNaN(n) && n >= 0 && n <= 100; + }, "Fee % must be between 0 and 100"), + feeAddress: z + .string() + .trim() + .refine(isEthereumAddress, "Invalid Ethereum address") + .refine((a) => a.toLowerCase() !== "0x0000000000000000000000000000000000000000", "Fee address cannot be the zero address"), + }) + .optional(),
55-64: Strengthen item validation (quantity and monetary fields).Prevent non-positive quantities and enforce decimal strings.
- items: z.array(z.object({ - id: z.string(), - description: z.string(), - quantity: z.number(), - unitPrice: z.string(), - discount: z.string().optional(), - tax: z.string().optional(), - total: z.string(), - currency: z.string().optional(), - })), + items: z.array(z.object({ + id: z.string().trim().min(1), + description: z.string().trim().min(1), + quantity: z.coerce.number().int().positive(), + unitPrice: z.string().trim().regex(/^\d+(\.\d{1,18})?$/, "Use a decimal string"), + discount: z.string().trim().regex(/^\d+(\.\d{1,18})?$/, "Use a decimal string").optional(), + tax: z.string().trim().regex(/^\d+(\.\d{1,18})?$/, "Use a decimal string").optional(), + total: z.string().trim().regex(/^\d+(\.\d{1,18})?$/, "Use a decimal string"), + currency: z.string().trim().optional(), + }).strict())),
🧹 Nitpick comments (5)
src/lib/validation.ts (5)
11-11: Trim API client ID.Avoid passing whitespace-only strings.
- rnApiClientId: z.string().min(1, "API Client ID is required"), + rnApiClientId: z.string().trim().min(1, "API Client ID is required"),
20-23: Make schemas strict to catch typos and unknown keys.Add
.strict()to key objects; prevents silent drops.- uiConfig: z.object({ + uiConfig: z.object({ showRequestScanUrl: z.boolean().optional(), showReceiptDownload: z.boolean().optional(), - }).optional(), + }).strict().optional(), @@ - receiptInfo: z.object({ + receiptInfo: z.object({ @@ - address: z.object({ + address: z.object({ street: z.string(), city: z.string(), state: z.string(), country: z.string(), postalCode: z.string(), - }).optional(), + }).strict().optional(), @@ - address: z.object({ + address: z.object({ street: z.string(), city: z.string(), state: z.string(), postalCode: z.string(), country: z.string(), - }).optional(), + }).strict().optional(), @@ - items: z.array(z.object({ + items: z.array(z.object({ id: z.string(), description: z.string(), quantity: z.number(), unitPrice: z.string(), discount: z.string().optional(), tax: z.string().optional(), total: z.string(), currency: z.string().optional(), - })), + }).strict())), @@ - totals: z.object({ + totals: z.object({ totalDiscount: z.string(), totalTax: z.string(), total: z.string(), totalUSD: z.string(), - }), + }).strict(),Also applies to: 26-26, 33-39, 43-49, 55-55, 65-70
28-32: Trim names; validate company email/website when present.Improve data quality for optional fields.
- email: z.string().email("Invalid email address"), - firstName: z.string().optional(), - lastName: z.string().optional(), - businessName: z.string().optional(), + email: z.string().email("Invalid email address"), + firstName: z.string().trim().optional(), + lastName: z.string().trim().optional(), + businessName: z.string().trim().optional(), @@ - name: z.string().min(1, "Company name is required"), + name: z.string().trim().min(1, "Company name is required"), @@ - email: z.string().optional(), + email: z.string().email("Invalid email").optional(), @@ - website: z.string().optional(), + website: z.string().url("Invalid URL").optional(),Also applies to: 41-54
65-70: Validate totals as decimal strings (and trim).Ensures consistency with item amounts.
- totals: z.object({ - totalDiscount: z.string(), - totalTax: z.string(), - total: z.string(), - totalUSD: z.string(), - }), + totals: z.object({ + totalDiscount: z.string().trim().regex(/^\d+(\.\d{1,18})?$/, "Use a decimal string"), + totalTax: z.string().trim().regex(/^\d+(\.\d{1,18})?$/, "Use a decimal string"), + total: z.string().trim().regex(/^\d+(\.\d{1,18})?$/, "Use a decimal string"), + totalUSD: z.string().trim().regex(/^\d+(\.\d{1,2})?$/, "Use a USD amount (2 decimals)"), + }).strict(),
9-17: Optional: add strict() at top-level too.Guard against unknown top-level keys.
-export const PlaygroundValidation = z.object({ +export const PlaygroundValidation = z.object({ @@ -}); +}).strict();Also applies to: 19-24, 26-73
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/lib/validation.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: MantisClone
PR: RequestNetwork/rn-checkout#17
File: src/components/ui/tabs.tsx:85-94
Timestamp: 2024-11-07T02:52:54.845Z
Learning: In the playground component (`src/components/ui/tabs.tsx`), ARIA attributes are not necessary; focus on adding them to the payment widget instead.
📚 Learning: 2024-11-07T06:08:13.072Z
Learnt from: aimensahnoun
PR: RequestNetwork/rn-checkout#17
File: src/components/Playground.tsx:46-46
Timestamp: 2024-11-07T06:08:13.072Z
Learning: In the Playground component, the default value for `enableBuyerInfo` is now `true`, and this is reflected in the Zod validation schema.
Applied to files:
src/lib/validation.ts
🔇 Additional comments (1)
src/lib/validation.ts (1)
58-58: Confirmed — form registers quantity as a number and schema matches.
- register(...
receiptInfo.items.${index}.quantity...) usesvalueAsNumber: true. (src/components/Playground/blocks/customize.tsx:201–203)- Validation schema declares
quantity: z.number(). (src/lib/validation.ts:58)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approved 👍
Problem
We changed our widget to be distributed via ShadCN and use our API instead of SDK, so we needed to swap them here too
Note that all the added lines are mostly payment widget related components.
Changes
Resolves #29
Summary by CodeRabbit
New Features
New Features / UX
Documentation
Chores
Style