Skip to content

Conversation

@bassgeta
Copy link
Contributor

@bassgeta bassgeta commented Sep 12, 2025

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

  • removed old widget
  • installed the new widget via ShadCN CLI
  • refactored the form to have 3 separate components instead of one huge component
  • updated the form to adhere to new props

Resolves #29

Summary by CodeRabbit

  • New Features

    • Full crypto Payment Widget: multi-wallet flow, currency selection, checkout, confirmation and success screens with dynamic receipt numbers and richer receipt details.
    • PDF receipt download and direct Request Scan link.
  • New Features / UX

    • Revamped interactive Playground: configure, preview, and copy integration code for the widget.
  • Documentation

    • Comprehensive Payment Widget README with usage, props, and examples.
  • Chores

    • Updated dependencies (UI libs, wagmi/viem, react-query, html2pdf, etc.).
  • Style

    • Minor UI refinements to buttons, inputs, dialogs, selects, and radio controls.

@bassgeta bassgeta self-assigned this Sep 12, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 12, 2025

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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.

📥 Commits

Reviewing files that changed from the base of the PR and between 7fa6999 and 471bdc0.

📒 Files selected for processing (1)
  • src/lib/validation.ts (1 hunks)

Walkthrough

Adds 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

Cohort / File(s) Summary
Config & deps
components.json, package.json
Add registries to components.json; bump and add dependencies (wagmi, viem, @tanstack/react-query, html2pdf.js, radix packages, lucide-react, react-hook-form, etc.).
App route
src/app/playground/page.tsx
Update Playground import to explicit @/components/Playground/index.
Checkout integration
src/components/PaymentStep.tsx
Replace external widget with local PaymentWidget usage; gate on NEXT_PUBLIC_RN_API_CLIENT_ID, pass nested paymentConfig/uiConfig/receiptInfo, compute invoice items/totals, handle onSuccess/onError, and generate receiptNumber.
Playground refactor
Removed:
src/components/Playground.tsx
Added/Modified:
src/components/Playground/index.tsx, src/components/Playground/blocks/*
Remove old monolithic Playground; add tabbed Playground with FormProvider, CustomizeForm, SellerForm, BuyerForm, live Preview, integration code generator, and copy actions.
Payment Widget — core & API
src/components/payment-widget/payment-widget.tsx, .../payment-widget.types.ts, .../README.md
New PaymentWidget wrapper, public types (PaymentConfig/UiConfig/PaymentWidgetProps), Web3Provider wiring, and comprehensive README.
Payment Widget — context & web3
.../context/payment-widget-context.tsx, .../context/web3-context.tsx
Add PaymentWidgetProvider and usePaymentWidgetContext; Web3Provider to instantiate wagmi + react-query with cached config.
Payment Widget — modal & connection
.../components/payment-modal.tsx, .../components/connection-handler.tsx, .../components/wallet-connect-modal.tsx, .../components/disconnect-wallet.tsx
New multi-step payment modal and connectivity UI (connect/disconnect, connection handler).
Payment Widget — step UIs
.../components/currency-select.tsx, .../components/buyer-info-form.tsx, .../components/payment-confirmation.tsx, .../components/payment-success.tsx
Add currency selection (fetch conversion routes), buyer info form, payment confirmation (executes payment), and success view with optional PDF download / Request Scan link.
Receipts & PDF
.../components/receipt/receipt-template.tsx, .../components/receipt/styles.css, .../utils/receipt.ts, .../types/html2pdf.d.ts
Add receipt PDF template and styles, receipt utilities, and TypeScript types for html2pdf.js.
Payment execution & helpers
.../hooks/use-payment.ts, .../utils/payment.ts, .../utils/currencies.ts, .../utils/chains.ts, .../utils/wagmi.ts, .../constants.ts, .../types/index.ts
New usePayment hook, createPayout / executePayment flow, currency route fetcher, chain mapping, wagmi config factory, shared constants, and widget types.
UI primitives
src/components/ui/button.tsx, src/components/ui/combobox.tsx, src/components/ui/dialog.tsx, src/components/ui/input.tsx, src/components/ui/radio-group.tsx, src/components/ui/select.tsx
Button spacing and SVG utility classes; combobox import moved to src/lib/constants.ts; dialog whitespace normalization; input prop typing change and styling tweaks; add RadioGroup and Select wrappers.
Lib & validation
src/lib/constants.ts, src/lib/currencies.ts (removed), src/lib/validation.ts, src/lib/utils.ts
Move/expand CURRENCY_ID into src/lib/constants.ts, remove old src/lib/currencies.ts, replace validation schema with PlaygroundValidation and PlaygroundFormData (zod), minor utils formatting.

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
Loading
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
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • rodrigopavezi
  • sstefdev
  • MantisClone

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "feat: use new payment widget" is concise and accurately summarizes the primary change in this PR — replacing the old widget with a new payment-widget implementation (new src/components/payment-widget files and updated Playground/PaymentStep usage) and related integration work. It is specific enough for a teammate scanning history and contains no noisy or ambiguous language.
Linked Issues Check ✅ Passed The diff implements a local, API-based PaymentWidget and the surrounding ecosystem (context, hooks, modal, receipt generation), replaces the previous SDK usage in PaymentStep and Playground, and updates dependencies and shadcn registry entries to support the new distribution — all of which map directly to linked issue #29's objectives to integrate the new API-based widget and refactor the demo. New types, props, and payment flows were added so the demo code now targets the new widget API. Based on the provided file summaries, the code changes satisfy the coding requirements described in the linked issue.
Out of Scope Changes Check ✅ Passed Most modifications are directly related to the widget integration (payment-widget sources, Playground refactor, dependency/registry updates and UI primitives added by the shadcn CLI). Minor formatting/styling tweaks and the relocation of CURRENCY_ID to src/lib/constants.ts appear incidental to the refactor rather than unrelated feature work, and I do not see significant files or features added that fall outside the stated objective.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Comment @coderabbitai help to get the list of available commands and usage tips.

@bassgeta bassgeta force-pushed the feat/use-new-checkout branch from fe5761c to 84eaf20 Compare September 15, 2025 09:59
@bassgeta bassgeta changed the title feat: use new payment widget and realign the for mvalidation; split u… feat: use new payment widget Sep 15, 2025
@bassgeta bassgeta marked this pull request as ready for review September 17, 2025 15:16
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 useMemo

You’re intentionally memoizing to prevent WC double-init, but the useMemo dep walletConnectProjectId is ignored once configRef.current is 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 walletConnectProjectId can 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 Error

Biome is right: importing Error shadows 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 autoComplete attributes 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, totals are 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 submit

Currently only top-level fields are cleaned. Trim and convert empty address subfields to undefined too.

-    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 UX

Small enhancement: set autoComplete on 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.amount is rendered as crypto here, but in PaymentSuccess it is currently set from amountInUsd (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-900 are 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: Import isAddress to 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 missing rnApiClientId and invalid recipientWallet.

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() }) by requiredChain.id to 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 ids
src/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 unused preventDefault; the handler isn’t attached to a form submit.

-  const handleExecutePayment = async (e: React.FormEvent) => {
-    e.preventDefault();
+  const handleExecutePayment = async () => {

154-156: Add role="alert" and aria-live for 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 memoizing receiptParams keyed by its inputs.

I can send a small useMemo patch if you want.

src/components/payment-widget/payment-widget.tsx (2)

34-40: Defensive check for supportedCurrencies.

Avoid possible runtime error if supportedCurrencies is 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 useEffect to log only on transitions.

src/components/Playground/index.tsx (4)

120-129: Normalize supportedCurrencies to an array before codegen.

CurrencyCombobox can yield a comma-delimited string; codegen should handle both shapes to avoid emitting supportedCurrencies: "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.

codeRef is 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 making walletAddress optional or validate at runtime (e.g., with viem’s isAddress) 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 with isAddress/isHex; fail fast with a typed PaymentError.

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 isAddress import as noted above.)


128-145: Minor: include Accept header for explicit JSON negotiation.
Harmless clarity; some proxies behave better with explicit Accept.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 8737040 and e94542d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is 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.tsx
  • src/components/payment-widget/payment-widget.tsx
  • src/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.tsx
  • src/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)

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 validator is 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 esModuleInterop and allowSyntheticDefaultImports are enabled.


3-3: Consider adding .strict() and a top-level superRefine for cross-field checks.

This prevents silent typos and enables validating items[*].currencypaymentConfig.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 totalUSDtotalUsd for camelCase consistency with amountInUsd.

-    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

📥 Commits

Reviewing files that changed from the base of the PR and between e94542d and 085013b.

📒 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 isValidCurrencyId from 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

📥 Commits

Reviewing files that changed from the base of the PR and between 085013b and 7fa6999.

📒 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...) uses valueAsNumber: true. (src/components/Playground/blocks/customize.tsx:201–203)
  • Validation schema declares quantity: z.number(). (src/lib/validation.ts:58)

Copy link
Member

@MantisClone MantisClone left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved 👍

@bassgeta bassgeta merged commit b5b5488 into main Sep 17, 2025
2 checks passed
@bassgeta bassgeta deleted the feat/use-new-checkout branch September 17, 2025 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Checkout - Integrate new API-based Payment Widget into the Checkout Demo

3 participants