@b3dotfun/sdk 0.1.69-alpha.20 → 0.1.69-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/cjs/anyspend/react/components/checkout/AnySpendCheckout.js +6 -5
  2. package/dist/cjs/anyspend/react/components/checkout/CartItemRow.d.ts +2 -1
  3. package/dist/cjs/anyspend/react/components/checkout/CartSummary.d.ts +6 -4
  4. package/dist/cjs/anyspend/react/components/checkout/CartSummary.js +13 -11
  5. package/dist/cjs/anyspend/react/components/checkout/CheckoutCartPanel.d.ts +3 -1
  6. package/dist/cjs/anyspend/react/components/checkout/CheckoutCartPanel.js +5 -4
  7. package/dist/cjs/anyspend/react/components/checkout/CheckoutFormPanel.d.ts +3 -1
  8. package/dist/cjs/anyspend/react/components/checkout/CheckoutFormPanel.js +2 -2
  9. package/dist/cjs/anyspend/react/components/checkout/DiscountCodeInput.d.ts +3 -1
  10. package/dist/cjs/anyspend/react/components/checkout/DiscountCodeInput.js +3 -6
  11. package/dist/cjs/anyspend/react/components/checkout/PriceSkeleton.d.ts +5 -0
  12. package/dist/cjs/anyspend/react/components/checkout/PriceSkeleton.js +9 -0
  13. package/dist/cjs/anyspend/react/components/checkout/ShippingSelector.d.ts +3 -1
  14. package/dist/cjs/anyspend/react/components/checkout/ShippingSelector.js +3 -2
  15. package/dist/esm/anyspend/react/components/checkout/AnySpendCheckout.js +6 -5
  16. package/dist/esm/anyspend/react/components/checkout/CartItemRow.d.ts +2 -1
  17. package/dist/esm/anyspend/react/components/checkout/CartSummary.d.ts +6 -4
  18. package/dist/esm/anyspend/react/components/checkout/CartSummary.js +13 -11
  19. package/dist/esm/anyspend/react/components/checkout/CheckoutCartPanel.d.ts +3 -1
  20. package/dist/esm/anyspend/react/components/checkout/CheckoutCartPanel.js +5 -4
  21. package/dist/esm/anyspend/react/components/checkout/CheckoutFormPanel.d.ts +3 -1
  22. package/dist/esm/anyspend/react/components/checkout/CheckoutFormPanel.js +2 -2
  23. package/dist/esm/anyspend/react/components/checkout/DiscountCodeInput.d.ts +3 -1
  24. package/dist/esm/anyspend/react/components/checkout/DiscountCodeInput.js +3 -6
  25. package/dist/esm/anyspend/react/components/checkout/PriceSkeleton.d.ts +5 -0
  26. package/dist/esm/anyspend/react/components/checkout/PriceSkeleton.js +6 -0
  27. package/dist/esm/anyspend/react/components/checkout/ShippingSelector.d.ts +3 -1
  28. package/dist/esm/anyspend/react/components/checkout/ShippingSelector.js +3 -2
  29. package/dist/types/anyspend/react/components/checkout/CartItemRow.d.ts +2 -1
  30. package/dist/types/anyspend/react/components/checkout/CartSummary.d.ts +6 -4
  31. package/dist/types/anyspend/react/components/checkout/CheckoutCartPanel.d.ts +3 -1
  32. package/dist/types/anyspend/react/components/checkout/CheckoutFormPanel.d.ts +3 -1
  33. package/dist/types/anyspend/react/components/checkout/DiscountCodeInput.d.ts +3 -1
  34. package/dist/types/anyspend/react/components/checkout/PriceSkeleton.d.ts +5 -0
  35. package/dist/types/anyspend/react/components/checkout/ShippingSelector.d.ts +3 -1
  36. package/package.json +1 -1
  37. package/src/anyspend/react/components/checkout/AnySpendCheckout.tsx +10 -4
  38. package/src/anyspend/react/components/checkout/CartItemRow.tsx +2 -1
  39. package/src/anyspend/react/components/checkout/CartSummary.tsx +24 -20
  40. package/src/anyspend/react/components/checkout/CheckoutCartPanel.tsx +12 -3
  41. package/src/anyspend/react/components/checkout/CheckoutFormPanel.tsx +5 -0
  42. package/src/anyspend/react/components/checkout/DiscountCodeInput.tsx +15 -5
  43. package/src/anyspend/react/components/checkout/PriceSkeleton.tsx +19 -0
  44. package/src/anyspend/react/components/checkout/ShippingSelector.tsx +5 -1
@@ -122,10 +122,11 @@ variablePricing, feeOnTop, kycEnabled = false, callbackMetadata: callbackMetadat
122
122
  isVariablePricingActive,
123
123
  variablePricingAmount,
124
124
  ]);
125
- // Get destination token metadata
126
- const { data: tokenData } = (0, react_1.useTokenData)(destinationTokenChainId, destinationTokenAddress);
127
- const tokenSymbol = tokenData?.symbol || "";
128
- const tokenDecimals = tokenData?.decimals || 18;
125
+ // Get destination token metadata. While loading, skip price-dependent UI
126
+ // since the 18-decimal fallback is wrong for non-18 tokens (e.g. USDC).
127
+ const { data: tokenData, isLoading: isTokenDataLoading } = (0, react_1.useTokenData)(destinationTokenChainId, destinationTokenAddress);
128
+ const tokenSymbol = tokenData?.symbol ?? "";
129
+ const tokenDecimals = tokenData?.decimals ?? 18;
129
130
  // Resolve USD equivalent for non-stablecoin tokens (shown in cart summary)
130
131
  const isStablecoin = (0, react_2.useMemo)(() => {
131
132
  return [
@@ -209,5 +210,5 @@ variablePricing, feeOnTop, kycEnabled = false, callbackMetadata: callbackMetadat
209
210
  successTitle: "Payment Successful",
210
211
  successDescription: "Your payment has been processed successfully.",
211
212
  ...content,
212
- }, theme: theme, children: (0, jsx_runtime_1.jsx)(CheckoutLayout_1.CheckoutLayout, { mode: mode, paymentPanel: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [isVariablePricingActive && tokenData && variablePricing && ((0, jsx_runtime_1.jsx)(VariablePricingInput_1.VariablePricingInput, { config: variablePricing, tokenDecimals: tokenDecimals, tokenSymbol: tokenSymbol, themeColor: themeColor, onChange: setVariablePricingAmount })), hasFormContent && ((0, jsx_runtime_1.jsxs)("div", { className: "mb-6", children: [(0, jsx_runtime_1.jsx)(CheckoutFormPanel_1.CheckoutFormPanel, { formSchema: formSchema, formComponent: formComponent, shippingOptions: shippingOptions, collectShippingAddress: collectShippingAddress, enableDiscountCode: enableDiscountCode, validateDiscount: validateDiscount, tokenSymbol: tokenSymbol, tokenDecimals: tokenDecimals, classes: classes, formData: formData, onFormDataChange: handleFormDataChange, selectedShipping: selectedShipping, onShippingChange: handleShippingChange, appliedDiscount: appliedDiscount, onDiscountApplied: handleDiscountApplied, onDiscountRemoved: handleDiscountRemoved, shippingAddress: shippingAddress, onShippingAddressChange: setShippingAddress, checkoutFormSlot: slots?.checkoutForm }), (0, jsx_runtime_1.jsx)("div", { className: "mt-6 border-t border-gray-200 dark:border-neutral-700" })] })), (0, jsx_runtime_1.jsx)(CheckoutPaymentPanel_1.CheckoutPaymentPanel, { recipientAddress: recipientAddress, destinationTokenAddress: destinationTokenAddress, destinationTokenChainId: destinationTokenChainId, totalAmount: computedTotal, buttonText: buttonText, themeColor: themeColor, returnUrl: returnUrl, returnLabel: returnLabel, onSuccess: onSuccess, onError: onError, classes: classes, defaultPaymentMethod: defaultPaymentMethod, senderAddress: senderAddress, showPoints: showPoints, showOrderId: showOrderId, callbackMetadata: checkoutFormMetadata, isFormValid: isFormValid, feeOnTop: feeOnTop, kycEnabled: kycEnabled })] }), cartPanel: (0, jsx_runtime_1.jsx)(CheckoutCartPanel_1.CheckoutCartPanel, { items: items, totalAmount: computedTotal, tokenSymbol: tokenSymbol, tokenDecimals: tokenDecimals, organizationName: organizationName, organizationLogo: organizationLogo, classes: classes, footer: footer, shipping: effectiveShipping, tax: typeof tax === "string" ? { amount: tax } : tax, discount: effectiveDiscount, summaryLines: summaryLines, usdEquivalent: usdEquivalent }), classes: classes }) }) }));
213
+ }, theme: theme, children: (0, jsx_runtime_1.jsx)(CheckoutLayout_1.CheckoutLayout, { mode: mode, paymentPanel: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [isVariablePricingActive && tokenData && variablePricing && ((0, jsx_runtime_1.jsx)(VariablePricingInput_1.VariablePricingInput, { config: variablePricing, tokenDecimals: tokenDecimals, tokenSymbol: tokenSymbol, themeColor: themeColor, onChange: setVariablePricingAmount })), hasFormContent && ((0, jsx_runtime_1.jsxs)("div", { className: "mb-6", children: [(0, jsx_runtime_1.jsx)(CheckoutFormPanel_1.CheckoutFormPanel, { formSchema: formSchema, formComponent: formComponent, shippingOptions: shippingOptions, collectShippingAddress: collectShippingAddress, enableDiscountCode: enableDiscountCode, validateDiscount: validateDiscount, tokenSymbol: tokenSymbol, tokenDecimals: tokenDecimals, pricesLoading: isTokenDataLoading, classes: classes, formData: formData, onFormDataChange: handleFormDataChange, selectedShipping: selectedShipping, onShippingChange: handleShippingChange, appliedDiscount: appliedDiscount, onDiscountApplied: handleDiscountApplied, onDiscountRemoved: handleDiscountRemoved, shippingAddress: shippingAddress, onShippingAddressChange: setShippingAddress, checkoutFormSlot: slots?.checkoutForm }), (0, jsx_runtime_1.jsx)("div", { className: "mt-6 border-t border-gray-200 dark:border-neutral-700" })] })), (0, jsx_runtime_1.jsx)(CheckoutPaymentPanel_1.CheckoutPaymentPanel, { recipientAddress: recipientAddress, destinationTokenAddress: destinationTokenAddress, destinationTokenChainId: destinationTokenChainId, totalAmount: computedTotal, buttonText: buttonText, themeColor: themeColor, returnUrl: returnUrl, returnLabel: returnLabel, onSuccess: onSuccess, onError: onError, classes: classes, defaultPaymentMethod: defaultPaymentMethod, senderAddress: senderAddress, showPoints: showPoints, showOrderId: showOrderId, callbackMetadata: checkoutFormMetadata, isFormValid: isFormValid, feeOnTop: feeOnTop, kycEnabled: kycEnabled })] }), cartPanel: (0, jsx_runtime_1.jsx)(CheckoutCartPanel_1.CheckoutCartPanel, { items: items, totalAmount: computedTotal, tokenSymbol: tokenSymbol, tokenDecimals: tokenDecimals, pricesLoading: isTokenDataLoading, organizationName: organizationName, organizationLogo: organizationLogo, classes: classes, footer: footer, shipping: effectiveShipping, tax: typeof tax === "string" ? { amount: tax } : tax, discount: effectiveDiscount, summaryLines: summaryLines, usdEquivalent: usdEquivalent }), classes: classes }) }) }));
213
214
  }
@@ -1,7 +1,8 @@
1
+ import type { ReactNode } from "react";
1
2
  import type { CheckoutItem, AnySpendCheckoutClasses } from "./AnySpendCheckout";
2
3
  interface CartItemRowProps {
3
4
  item: CheckoutItem;
4
- formattedPrice: string;
5
+ formattedPrice: ReactNode;
5
6
  classes?: AnySpendCheckoutClasses;
6
7
  }
7
8
  export declare function CartItemRow({ item, formattedPrice, classes }: CartItemRowProps): import("react/jsx-runtime").JSX.Element;
@@ -1,8 +1,8 @@
1
- import type { AnySpendCheckoutClasses } from "./AnySpendCheckout";
2
- import type { CheckoutSummaryLine } from "./AnySpendCheckout";
1
+ import { type ReactNode } from "react";
2
+ import type { AnySpendCheckoutClasses, CheckoutSummaryLine } from "./AnySpendCheckout";
3
3
  interface CartSummaryProps {
4
4
  /** Formatted total (final amount after all adjustments) */
5
- total: string;
5
+ total: ReactNode;
6
6
  tokenSymbol?: string;
7
7
  classes?: AnySpendCheckoutClasses;
8
8
  /** Formatted subtotal (sum of items only — shown when adjustments exist) */
@@ -25,6 +25,8 @@ interface CartSummaryProps {
25
25
  summaryLines?: CheckoutSummaryLine[];
26
26
  /** Formatted USD equivalent (e.g. "$5.56") — shown for non-stablecoin tokens */
27
27
  usdEquivalent?: string | null;
28
+ /** When true, hide adjustment rows and USD equivalent (caller passes a skeleton as `total`). */
29
+ pricesLoading?: boolean;
28
30
  }
29
- export declare function CartSummary({ total, tokenSymbol, classes, subtotal, tokenDecimals, shipping, tax, discount, summaryLines, usdEquivalent, }: CartSummaryProps): import("react/jsx-runtime").JSX.Element;
31
+ export declare function CartSummary({ total, tokenSymbol, classes, subtotal, tokenDecimals, shipping, tax, discount, summaryLines, usdEquivalent, pricesLoading, }: CartSummaryProps): import("react/jsx-runtime").JSX.Element;
30
32
  export {};
@@ -6,15 +6,17 @@ const jsx_runtime_1 = require("react/jsx-runtime");
6
6
  const cn_1 = require("../../../../shared/utils/cn");
7
7
  const number_1 = require("../../../../shared/utils/number");
8
8
  const react_1 = require("react");
9
- function CartSummary({ total, tokenSymbol, classes, subtotal, tokenDecimals = 18, shipping, tax, discount, summaryLines, usdEquivalent, }) {
10
- const hasAdjustments = !!shipping?.amount || !!tax?.amount || !!discount?.amount || (summaryLines && summaryLines.length > 0);
11
- const formattedShipping = (0, react_1.useMemo)(() => (shipping?.amount ? (0, number_1.formatTokenAmount)((0, number_1.safeBigInt)(shipping.amount), tokenDecimals) : null), [shipping?.amount, tokenDecimals]);
12
- const formattedTax = (0, react_1.useMemo)(() => (tax?.amount ? (0, number_1.formatTokenAmount)((0, number_1.safeBigInt)(tax.amount), tokenDecimals) : null), [tax?.amount, tokenDecimals]);
13
- const formattedDiscount = (0, react_1.useMemo)(() => (discount?.amount ? (0, number_1.formatTokenAmount)((0, number_1.safeBigInt)(discount.amount), tokenDecimals) : null), [discount?.amount, tokenDecimals]);
14
- const formattedSummaryLines = (0, react_1.useMemo)(() => summaryLines?.map(line => ({
15
- ...line,
16
- formattedAmount: (0, number_1.formatTokenAmount)((0, number_1.safeBigInt)(line.amount), tokenDecimals),
17
- isNegative: (0, number_1.safeBigInt)(line.amount) < BigInt(0),
18
- })), [summaryLines, tokenDecimals]);
19
- return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("border-t border-gray-200 pt-3 dark:border-neutral-700", classes?.cartSummary), children: [hasAdjustments && subtotal && ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex items-center justify-between py-1", classes?.cartSubtotal), children: [(0, jsx_runtime_1.jsx)("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Subtotal" }), (0, jsx_runtime_1.jsxs)("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: [subtotal, " ", tokenSymbol] })] })), formattedShipping && ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex items-center justify-between py-1", classes?.cartSummaryLine), children: [(0, jsx_runtime_1.jsx)("span", { className: (0, cn_1.cn)("text-sm text-gray-500 dark:text-gray-400", classes?.cartSummaryLineLabel), children: shipping?.label || "Shipping" }), (0, jsx_runtime_1.jsxs)("span", { className: (0, cn_1.cn)("text-sm text-gray-500 dark:text-gray-400", classes?.cartSummaryLineAmount), children: [formattedShipping, " ", tokenSymbol] })] })), formattedTax && ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex items-center justify-between py-1", classes?.cartSummaryLine), children: [(0, jsx_runtime_1.jsxs)("span", { className: (0, cn_1.cn)("text-sm text-gray-500 dark:text-gray-400", classes?.cartSummaryLineLabel), children: [tax?.label || "Tax", tax?.rate && (0, jsx_runtime_1.jsxs)("span", { className: "ml-1 text-xs text-gray-400 dark:text-gray-500", children: ["(", tax.rate, ")"] })] }), (0, jsx_runtime_1.jsxs)("span", { className: (0, cn_1.cn)("text-sm text-gray-500 dark:text-gray-400", classes?.cartSummaryLineAmount), children: [formattedTax, " ", tokenSymbol] })] })), formattedDiscount && ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex items-center justify-between py-1", classes?.cartDiscount), children: [(0, jsx_runtime_1.jsxs)("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: [discount?.label || "Discount", discount?.code && ((0, jsx_runtime_1.jsx)("span", { className: "ml-1 rounded bg-green-50 px-1.5 py-0.5 text-xs font-medium text-green-600 dark:bg-green-900/20 dark:text-green-400", children: discount.code }))] }), (0, jsx_runtime_1.jsxs)("span", { className: "text-sm font-medium text-green-600 dark:text-green-400", children: ["-", formattedDiscount, " ", tokenSymbol] })] })), formattedSummaryLines?.map(line => ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex items-center justify-between py-1", classes?.cartSummaryLine), children: [(0, jsx_runtime_1.jsxs)("span", { className: (0, cn_1.cn)("text-sm text-gray-500 dark:text-gray-400", classes?.cartSummaryLineLabel), children: [line.label, line.description && ((0, jsx_runtime_1.jsxs)("span", { className: "ml-1 text-xs text-gray-400 dark:text-gray-500", children: ["(", line.description, ")"] }))] }), (0, jsx_runtime_1.jsxs)("span", { className: (0, cn_1.cn)("text-sm", line.isNegative ? "font-medium text-green-600 dark:text-green-400" : "text-gray-500 dark:text-gray-400", classes?.cartSummaryLineAmount), children: [line.formattedAmount, " ", tokenSymbol] })] }, line.label))), (0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex flex-col", hasAdjustments && "mt-1 border-t border-gray-100 pt-2 dark:border-neutral-800"), children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-base font-semibold text-gray-900 dark:text-gray-100", children: "Total" }), (0, jsx_runtime_1.jsxs)("span", { className: (0, cn_1.cn)("text-base font-semibold text-gray-900 dark:text-gray-100", classes?.cartTotal), children: [total, " ", tokenSymbol] })] }), usdEquivalent && ((0, jsx_runtime_1.jsx)("div", { className: "flex justify-end", children: (0, jsx_runtime_1.jsxs)("span", { className: "text-xs text-gray-400 dark:text-gray-500", children: ["~", usdEquivalent, " USD"] }) }))] })] }));
9
+ function CartSummary({ total, tokenSymbol, classes, subtotal, tokenDecimals = 18, shipping, tax, discount, summaryLines, usdEquivalent, pricesLoading = false, }) {
10
+ const formattedShipping = (0, react_1.useMemo)(() => (!pricesLoading && shipping?.amount ? (0, number_1.formatTokenAmount)((0, number_1.safeBigInt)(shipping.amount), tokenDecimals) : null), [pricesLoading, shipping?.amount, tokenDecimals]);
11
+ const formattedTax = (0, react_1.useMemo)(() => (!pricesLoading && tax?.amount ? (0, number_1.formatTokenAmount)((0, number_1.safeBigInt)(tax.amount), tokenDecimals) : null), [pricesLoading, tax?.amount, tokenDecimals]);
12
+ const formattedDiscount = (0, react_1.useMemo)(() => (!pricesLoading && discount?.amount ? (0, number_1.formatTokenAmount)((0, number_1.safeBigInt)(discount.amount), tokenDecimals) : null), [pricesLoading, discount?.amount, tokenDecimals]);
13
+ const formattedSummaryLines = (0, react_1.useMemo)(() => pricesLoading
14
+ ? undefined
15
+ : summaryLines?.map(line => ({
16
+ ...line,
17
+ formattedAmount: (0, number_1.formatTokenAmount)((0, number_1.safeBigInt)(line.amount), tokenDecimals),
18
+ isNegative: (0, number_1.safeBigInt)(line.amount) < BigInt(0),
19
+ })), [pricesLoading, summaryLines, tokenDecimals]);
20
+ const hasAdjustments = !!formattedShipping || !!formattedTax || !!formattedDiscount || !!formattedSummaryLines?.length;
21
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("border-t border-gray-200 pt-3 dark:border-neutral-700", classes?.cartSummary), children: [hasAdjustments && subtotal && ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex items-center justify-between py-1", classes?.cartSubtotal), children: [(0, jsx_runtime_1.jsx)("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Subtotal" }), (0, jsx_runtime_1.jsxs)("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: [subtotal, " ", tokenSymbol] })] })), formattedShipping && ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex items-center justify-between py-1", classes?.cartSummaryLine), children: [(0, jsx_runtime_1.jsx)("span", { className: (0, cn_1.cn)("text-sm text-gray-500 dark:text-gray-400", classes?.cartSummaryLineLabel), children: shipping?.label || "Shipping" }), (0, jsx_runtime_1.jsxs)("span", { className: (0, cn_1.cn)("text-sm text-gray-500 dark:text-gray-400", classes?.cartSummaryLineAmount), children: [formattedShipping, " ", tokenSymbol] })] })), formattedTax && ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex items-center justify-between py-1", classes?.cartSummaryLine), children: [(0, jsx_runtime_1.jsxs)("span", { className: (0, cn_1.cn)("text-sm text-gray-500 dark:text-gray-400", classes?.cartSummaryLineLabel), children: [tax?.label || "Tax", tax?.rate && (0, jsx_runtime_1.jsxs)("span", { className: "ml-1 text-xs text-gray-400 dark:text-gray-500", children: ["(", tax.rate, ")"] })] }), (0, jsx_runtime_1.jsxs)("span", { className: (0, cn_1.cn)("text-sm text-gray-500 dark:text-gray-400", classes?.cartSummaryLineAmount), children: [formattedTax, " ", tokenSymbol] })] })), formattedDiscount && ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex items-center justify-between py-1", classes?.cartDiscount), children: [(0, jsx_runtime_1.jsxs)("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: [discount?.label || "Discount", discount?.code && ((0, jsx_runtime_1.jsx)("span", { className: "ml-1 rounded bg-green-50 px-1.5 py-0.5 text-xs font-medium text-green-600 dark:bg-green-900/20 dark:text-green-400", children: discount.code }))] }), (0, jsx_runtime_1.jsxs)("span", { className: "text-sm font-medium text-green-600 dark:text-green-400", children: ["-", formattedDiscount, " ", tokenSymbol] })] })), formattedSummaryLines?.map(line => ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex items-center justify-between py-1", classes?.cartSummaryLine), children: [(0, jsx_runtime_1.jsxs)("span", { className: (0, cn_1.cn)("text-sm text-gray-500 dark:text-gray-400", classes?.cartSummaryLineLabel), children: [line.label, line.description && ((0, jsx_runtime_1.jsxs)("span", { className: "ml-1 text-xs text-gray-400 dark:text-gray-500", children: ["(", line.description, ")"] }))] }), (0, jsx_runtime_1.jsxs)("span", { className: (0, cn_1.cn)("text-sm", line.isNegative ? "font-medium text-green-600 dark:text-green-400" : "text-gray-500 dark:text-gray-400", classes?.cartSummaryLineAmount), children: [line.formattedAmount, " ", tokenSymbol] })] }, line.label))), (0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex flex-col", hasAdjustments && "mt-1 border-t border-gray-100 pt-2 dark:border-neutral-800"), children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-base font-semibold text-gray-900 dark:text-gray-100", children: "Total" }), (0, jsx_runtime_1.jsxs)("span", { className: (0, cn_1.cn)("text-base font-semibold text-gray-900 dark:text-gray-100", classes?.cartTotal), children: [total, " ", tokenSymbol] })] }), !pricesLoading && usdEquivalent && ((0, jsx_runtime_1.jsx)("div", { className: "flex justify-end", children: (0, jsx_runtime_1.jsxs)("span", { className: "text-xs text-gray-400 dark:text-gray-500", children: ["~", usdEquivalent, " USD"] }) }))] })] }));
20
22
  }
@@ -5,6 +5,8 @@ interface CheckoutCartPanelProps {
5
5
  totalAmount: string;
6
6
  tokenSymbol?: string;
7
7
  tokenDecimals?: number;
8
+ /** True while token decimals/symbol are still loading — prevents rendering mis-decimalized prices. */
9
+ pricesLoading?: boolean;
8
10
  organizationName?: string;
9
11
  organizationLogo?: string;
10
12
  classes?: AnySpendCheckoutClasses;
@@ -28,5 +30,5 @@ interface CheckoutCartPanelProps {
28
30
  /** Formatted USD equivalent (e.g. "$5.56") — shown for non-stablecoin tokens */
29
31
  usdEquivalent?: string | null;
30
32
  }
31
- export declare function CheckoutCartPanel({ items, totalAmount, tokenSymbol, tokenDecimals, organizationName, organizationLogo, classes, footer, shipping, tax, discount, summaryLines, usdEquivalent, }: CheckoutCartPanelProps): import("react/jsx-runtime").JSX.Element;
33
+ export declare function CheckoutCartPanel({ items, totalAmount, tokenSymbol, tokenDecimals, pricesLoading, organizationName, organizationLogo, classes, footer, shipping, tax, discount, summaryLines, usdEquivalent, }: CheckoutCartPanelProps): import("react/jsx-runtime").JSX.Element;
32
34
  export {};
@@ -9,7 +9,8 @@ const react_1 = require("react");
9
9
  const CartItemRow_1 = require("./CartItemRow");
10
10
  const CartSummary_1 = require("./CartSummary");
11
11
  const PoweredByBranding_1 = require("./PoweredByBranding");
12
- function CheckoutCartPanel({ items, totalAmount, tokenSymbol = "", tokenDecimals = 18, organizationName, organizationLogo, classes, footer, shipping, tax, discount, summaryLines, usdEquivalent, }) {
12
+ const PriceSkeleton_1 = require("./PriceSkeleton");
13
+ function CheckoutCartPanel({ items, totalAmount, tokenSymbol = "", tokenDecimals = 18, pricesLoading = false, organizationName, organizationLogo, classes, footer, shipping, tax, discount, summaryLines, usdEquivalent, }) {
13
14
  const formattedTotal = (0, react_1.useMemo)(() => (0, number_1.formatTokenAmount)((0, number_1.safeBigInt)(totalAmount), tokenDecimals), [totalAmount, tokenDecimals]);
14
15
  // Compute subtotal from items only (before adjustments)
15
16
  const formattedSubtotal = (0, react_1.useMemo)(() => {
@@ -19,9 +20,9 @@ function CheckoutCartPanel({ items, totalAmount, tokenSymbol = "", tokenDecimals
19
20
  }
20
21
  return (0, number_1.formatTokenAmount)(subtotal, tokenDecimals);
21
22
  }, [items, tokenDecimals]);
22
- return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("anyspend-cart-panel flex flex-col", classes?.cartPanel), children: [(0, jsx_runtime_1.jsx)("h2", { className: (0, cn_1.cn)("anyspend-cart-title mb-4 text-lg font-semibold text-gray-900 dark:text-gray-100", classes?.cartTitle), children: "Order Summary" }), (0, jsx_runtime_1.jsx)("div", { className: "anyspend-cart-items divide-y divide-gray-100 dark:divide-gray-800", children: items.map((item, index) => {
23
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("anyspend-cart-panel flex flex-col", classes?.cartPanel), "aria-busy": pricesLoading || undefined, children: [(0, jsx_runtime_1.jsx)("h2", { className: (0, cn_1.cn)("anyspend-cart-title mb-4 text-lg font-semibold text-gray-900 dark:text-gray-100", classes?.cartTitle), children: "Order Summary" }), (0, jsx_runtime_1.jsx)("div", { className: "anyspend-cart-items divide-y divide-gray-100 dark:divide-gray-800", children: items.map((item, index) => {
23
24
  const itemTotal = (0, number_1.safeBigInt)(item.amount) * BigInt(item.quantity);
24
- const formattedPrice = `${(0, number_1.formatTokenAmount)(itemTotal, tokenDecimals)} ${tokenSymbol}`;
25
+ const formattedPrice = pricesLoading ? ((0, jsx_runtime_1.jsx)(PriceSkeleton_1.PriceSkeleton, {})) : (`${(0, number_1.formatTokenAmount)(itemTotal, tokenDecimals)} ${tokenSymbol}`);
25
26
  return (0, jsx_runtime_1.jsx)(CartItemRow_1.CartItemRow, { item: item, formattedPrice: formattedPrice, classes: classes }, item.id || index);
26
- }) }), (0, jsx_runtime_1.jsx)(CartSummary_1.CartSummary, { total: formattedTotal, tokenSymbol: tokenSymbol, classes: classes, subtotal: formattedSubtotal, tokenDecimals: tokenDecimals, shipping: shipping, tax: tax, discount: discount, summaryLines: summaryLines, usdEquivalent: usdEquivalent }), footer === undefined ? ((0, jsx_runtime_1.jsx)(PoweredByBranding_1.PoweredByBranding, { organizationName: organizationName, organizationLogo: organizationLogo, classes: classes })) : (footer)] }));
27
+ }) }), (0, jsx_runtime_1.jsx)(CartSummary_1.CartSummary, { total: pricesLoading ? (0, jsx_runtime_1.jsx)(PriceSkeleton_1.PriceSkeleton, { className: "w-20" }) : formattedTotal, tokenSymbol: tokenSymbol, classes: classes, subtotal: formattedSubtotal, tokenDecimals: tokenDecimals, shipping: shipping, tax: tax, discount: discount, summaryLines: summaryLines, usdEquivalent: usdEquivalent, pricesLoading: pricesLoading }), footer === undefined ? ((0, jsx_runtime_1.jsx)(PoweredByBranding_1.PoweredByBranding, { organizationName: organizationName, organizationLogo: organizationLogo, classes: classes })) : (footer)] }));
27
28
  }
@@ -16,6 +16,8 @@ interface CheckoutFormPanelProps {
16
16
  /** Token info for display */
17
17
  tokenSymbol?: string;
18
18
  tokenDecimals?: number;
19
+ /** True while token decimals/symbol are still loading — prevents rendering mis-decimalized prices. */
20
+ pricesLoading?: boolean;
19
21
  /** CSS class overrides */
20
22
  classes?: AnySpendCheckoutClasses;
21
23
  /** Current form data (lifted state) */
@@ -35,5 +37,5 @@ interface CheckoutFormPanelProps {
35
37
  /** Slot overrides */
36
38
  checkoutFormSlot?: (props: CheckoutFormComponentProps) => React.ReactNode;
37
39
  }
38
- export declare function CheckoutFormPanel({ formSchema, formComponent: FormComponent, shippingOptions, collectShippingAddress, enableDiscountCode, validateDiscount, tokenSymbol, tokenDecimals, classes, formData, onFormDataChange, selectedShipping, onShippingChange, appliedDiscount, onDiscountApplied, onDiscountRemoved, shippingAddress, onShippingAddressChange, checkoutFormSlot, }: CheckoutFormPanelProps): import("react/jsx-runtime").JSX.Element | null;
40
+ export declare function CheckoutFormPanel({ formSchema, formComponent: FormComponent, shippingOptions, collectShippingAddress, enableDiscountCode, validateDiscount, tokenSymbol, tokenDecimals, pricesLoading, classes, formData, onFormDataChange, selectedShipping, onShippingChange, appliedDiscount, onDiscountApplied, onDiscountRemoved, shippingAddress, onShippingAddressChange, checkoutFormSlot, }: CheckoutFormPanelProps): import("react/jsx-runtime").JSX.Element | null;
39
41
  export {};
@@ -10,7 +10,7 @@ const AddressForm_1 = require("./AddressForm");
10
10
  const ShippingSelector_1 = require("./ShippingSelector");
11
11
  const DiscountCodeInput_1 = require("./DiscountCodeInput");
12
12
  const emptyAddress = { street: "", city: "", state: "", zip: "", country: "" };
13
- function CheckoutFormPanel({ formSchema, formComponent: FormComponent, shippingOptions, collectShippingAddress, enableDiscountCode, validateDiscount, tokenSymbol, tokenDecimals, classes, formData, onFormDataChange, selectedShipping, onShippingChange, appliedDiscount, onDiscountApplied, onDiscountRemoved, shippingAddress, onShippingAddressChange, checkoutFormSlot, }) {
13
+ function CheckoutFormPanel({ formSchema, formComponent: FormComponent, shippingOptions, collectShippingAddress, enableDiscountCode, validateDiscount, tokenSymbol, tokenDecimals, pricesLoading = false, classes, formData, onFormDataChange, selectedShipping, onShippingChange, appliedDiscount, onDiscountApplied, onDiscountRemoved, shippingAddress, onShippingAddressChange, checkoutFormSlot, }) {
14
14
  const [errors, setErrors] = (0, react_1.useState)({});
15
15
  const hasFormFields = formSchema && formSchema.fields.length > 0;
16
16
  const hasShipping = shippingOptions && shippingOptions.length > 0;
@@ -56,7 +56,7 @@ function CheckoutFormPanel({ formSchema, formComponent: FormComponent, shippingO
56
56
  if (!hasAnyContent)
57
57
  return null;
58
58
  // Shared shipping + discount section
59
- const shippingAndDiscount = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [hasShipping && shippingOptions && ((0, jsx_runtime_1.jsx)(ShippingSelector_1.ShippingSelector, { options: shippingOptions, selectedId: selectedShipping?.id || null, onSelect: onShippingChange, tokenSymbol: tokenSymbol, tokenDecimals: tokenDecimals })), enableDiscountCode && validateDiscount && ((0, jsx_runtime_1.jsx)(DiscountCodeInput_1.DiscountCodeInput, { onApply: handleDiscountApply, appliedDiscount: appliedDiscount, onRemove: onDiscountRemoved, tokenSymbol: tokenSymbol, tokenDecimals: tokenDecimals }))] }));
59
+ const shippingAndDiscount = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [hasShipping && shippingOptions && ((0, jsx_runtime_1.jsx)(ShippingSelector_1.ShippingSelector, { options: shippingOptions, selectedId: selectedShipping?.id || null, onSelect: onShippingChange, tokenSymbol: tokenSymbol, tokenDecimals: tokenDecimals, pricesLoading: pricesLoading })), enableDiscountCode && validateDiscount && ((0, jsx_runtime_1.jsx)(DiscountCodeInput_1.DiscountCodeInput, { onApply: handleDiscountApply, appliedDiscount: appliedDiscount, onRemove: onDiscountRemoved, tokenSymbol: tokenSymbol, tokenDecimals: tokenDecimals, pricesLoading: pricesLoading }))] }));
60
60
  // Render custom form slot if provided
61
61
  if (checkoutFormSlot) {
62
62
  return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("anyspend-form-panel space-y-4", classes?.formPanel), children: [(0, jsx_runtime_1.jsx)("div", { className: "anyspend-form-slot", children: checkoutFormSlot({
@@ -6,7 +6,9 @@ interface DiscountCodeInputProps {
6
6
  loading?: boolean;
7
7
  tokenSymbol?: string;
8
8
  tokenDecimals?: number;
9
+ /** True while token decimals/symbol are still loading — renders applied amount as skeleton. */
10
+ pricesLoading?: boolean;
9
11
  className?: string;
10
12
  }
11
- export declare function DiscountCodeInput({ onApply, appliedDiscount, onRemove, loading, tokenSymbol, tokenDecimals, className, }: DiscountCodeInputProps): import("react/jsx-runtime").JSX.Element;
13
+ export declare function DiscountCodeInput({ onApply, appliedDiscount, onRemove, loading, tokenSymbol, tokenDecimals, pricesLoading, className, }: DiscountCodeInputProps): import("react/jsx-runtime").JSX.Element;
12
14
  export {};
@@ -7,13 +7,14 @@ const cn_1 = require("../../../../shared/utils/cn");
7
7
  const number_1 = require("../../../../shared/utils/number");
8
8
  const lucide_react_1 = require("lucide-react");
9
9
  const react_1 = require("react");
10
+ const PriceSkeleton_1 = require("./PriceSkeleton");
10
11
  function formatAmount(amount, decimals, symbol) {
11
12
  const bi = (0, number_1.safeBigInt)(amount);
12
13
  if (bi === BigInt(0))
13
14
  return "Free";
14
15
  return `${(0, number_1.formatTokenAmount)(bi, decimals)} ${symbol}`;
15
16
  }
16
- function DiscountCodeInput({ onApply, appliedDiscount, onRemove, loading = false, tokenSymbol = "", tokenDecimals = 6, className, }) {
17
+ function DiscountCodeInput({ onApply, appliedDiscount, onRemove, loading = false, tokenSymbol = "", tokenDecimals = 6, pricesLoading = false, className, }) {
17
18
  const [code, setCode] = (0, react_1.useState)("");
18
19
  const [error, setError] = (0, react_1.useState)(null);
19
20
  const [isValidating, setIsValidating] = (0, react_1.useState)(false);
@@ -44,11 +45,7 @@ function DiscountCodeInput({ onApply, appliedDiscount, onRemove, loading = false
44
45
  };
45
46
  // Show applied discount state
46
47
  if (appliedDiscount?.valid) {
47
- return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("anyspend-discount anyspend-discount-applied space-y-2", className), children: [(0, jsx_runtime_1.jsx)("div", { className: "anyspend-discount-title text-sm font-semibold text-gray-900 dark:text-gray-100", children: "Discount" }), (0, jsx_runtime_1.jsxs)("div", { className: "anyspend-discount-badge flex items-center justify-between rounded-lg border border-green-200 bg-green-50 px-3 py-2 dark:border-green-800 dark:bg-green-900/20", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "h-4 w-4 text-green-600 dark:text-green-400" }), (0, jsx_runtime_1.jsx)("span", { className: "anyspend-discount-value text-sm font-medium text-green-700 dark:text-green-300", children: appliedDiscount.discount_type === "percentage"
48
- ? `${appliedDiscount.discount_value}% off`
49
- : appliedDiscount.discount_amount
50
- ? `-${formatAmount(appliedDiscount.discount_amount, tokenDecimals, tokenSymbol)}`
51
- : "Discount applied" })] }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: handleRemove, className: "anyspend-discount-remove rounded p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300", children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { className: "h-3.5 w-3.5" }) })] })] }));
48
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("anyspend-discount anyspend-discount-applied space-y-2", className), children: [(0, jsx_runtime_1.jsx)("div", { className: "anyspend-discount-title text-sm font-semibold text-gray-900 dark:text-gray-100", children: "Discount" }), (0, jsx_runtime_1.jsxs)("div", { className: "anyspend-discount-badge flex items-center justify-between rounded-lg border border-green-200 bg-green-50 px-3 py-2 dark:border-green-800 dark:bg-green-900/20", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "h-4 w-4 text-green-600 dark:text-green-400" }), (0, jsx_runtime_1.jsx)("span", { className: "anyspend-discount-value text-sm font-medium text-green-700 dark:text-green-300", children: appliedDiscount.discount_type === "percentage" ? (`${appliedDiscount.discount_value}% off`) : appliedDiscount.discount_amount ? (pricesLoading ? ((0, jsx_runtime_1.jsx)(PriceSkeleton_1.PriceSkeleton, {})) : (`-${formatAmount(appliedDiscount.discount_amount, tokenDecimals, tokenSymbol)}`)) : ("Discount applied") })] }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: handleRemove, className: "anyspend-discount-remove rounded p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300", children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { className: "h-3.5 w-3.5" }) })] })] }));
52
49
  }
53
50
  return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("anyspend-discount space-y-2", className), children: [(0, jsx_runtime_1.jsx)("div", { className: "anyspend-discount-title text-sm font-semibold text-gray-900 dark:text-gray-100", children: "Discount Code" }), (0, jsx_runtime_1.jsxs)("div", { className: "anyspend-discount-input-row flex gap-2", children: [(0, jsx_runtime_1.jsx)("input", { type: "text", value: code, onChange: e => {
54
51
  setCode(e.target.value.toUpperCase());
@@ -0,0 +1,5 @@
1
+ interface PriceSkeletonProps {
2
+ className?: string;
3
+ }
4
+ export declare function PriceSkeleton({ className }: PriceSkeletonProps): import("react/jsx-runtime").JSX.Element;
5
+ export {};
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ "use client";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.PriceSkeleton = PriceSkeleton;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const cn_1 = require("../../../../shared/utils/cn");
7
+ function PriceSkeleton({ className }) {
8
+ return ((0, jsx_runtime_1.jsx)("span", { className: (0, cn_1.cn)("animate-pulse-fade bg-b3-react-background inline-block h-4 w-16 rounded-md align-middle", className), "aria-hidden": "true" }));
9
+ }
@@ -5,7 +5,9 @@ interface ShippingSelectorProps {
5
5
  onSelect: (option: ShippingOption) => void;
6
6
  tokenSymbol?: string;
7
7
  tokenDecimals?: number;
8
+ /** True while token decimals/symbol are still loading — renders price as skeleton. */
9
+ pricesLoading?: boolean;
8
10
  className?: string;
9
11
  }
10
- export declare function ShippingSelector({ options, selectedId, onSelect, tokenSymbol, tokenDecimals, className, }: ShippingSelectorProps): import("react/jsx-runtime").JSX.Element | null;
12
+ export declare function ShippingSelector({ options, selectedId, onSelect, tokenSymbol, tokenDecimals, pricesLoading, className, }: ShippingSelectorProps): import("react/jsx-runtime").JSX.Element | null;
11
13
  export {};
@@ -5,16 +5,17 @@ exports.ShippingSelector = ShippingSelector;
5
5
  const jsx_runtime_1 = require("react/jsx-runtime");
6
6
  const cn_1 = require("../../../../shared/utils/cn");
7
7
  const number_1 = require("../../../../shared/utils/number");
8
+ const PriceSkeleton_1 = require("./PriceSkeleton");
8
9
  function formatAmount(amount, decimals, symbol) {
9
10
  const bi = (0, number_1.safeBigInt)(amount);
10
11
  if (bi === BigInt(0))
11
12
  return "Free";
12
13
  return `${(0, number_1.formatTokenAmount)(bi, decimals)} ${symbol}`;
13
14
  }
14
- function ShippingSelector({ options, selectedId, onSelect, tokenSymbol = "", tokenDecimals = 6, className, }) {
15
+ function ShippingSelector({ options, selectedId, onSelect, tokenSymbol = "", tokenDecimals = 6, pricesLoading = false, className, }) {
15
16
  if (options.length === 0)
16
17
  return null;
17
18
  return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("anyspend-shipping-selector space-y-2", className), children: [(0, jsx_runtime_1.jsx)("div", { className: "anyspend-shipping-title text-sm font-semibold text-gray-900 dark:text-gray-100", children: "Shipping" }), (0, jsx_runtime_1.jsx)("div", { className: "anyspend-shipping-options space-y-2", children: options.map(option => ((0, jsx_runtime_1.jsxs)("label", { className: (0, cn_1.cn)("anyspend-shipping-option flex cursor-pointer items-center justify-between rounded-lg border p-3 transition-colors", selectedId === option.id
18
19
  ? "anyspend-shipping-option-selected border-blue-500 bg-blue-50 dark:border-blue-400 dark:bg-blue-900/20"
19
- : "border-gray-200 bg-white hover:border-gray-300 dark:border-neutral-600 dark:bg-neutral-800 dark:hover:border-neutral-500"), children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)("input", { type: "radio", name: "shipping", checked: selectedId === option.id, onChange: () => onSelect(option), className: "h-4 w-4 border-gray-300 text-blue-500 focus:ring-blue-500" }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { className: "anyspend-shipping-option-name text-sm font-medium text-gray-900 dark:text-gray-100", children: option.name }), (option.description || option.estimated_days) && ((0, jsx_runtime_1.jsx)("div", { className: "anyspend-shipping-option-detail text-xs text-gray-500 dark:text-gray-400", children: option.description || (option.estimated_days && `${option.estimated_days}`) }))] })] }), (0, jsx_runtime_1.jsx)("div", { className: "anyspend-shipping-option-price text-sm font-medium text-gray-900 dark:text-gray-100", children: formatAmount(option.amount, tokenDecimals, tokenSymbol) })] }, option.id))) })] }));
20
+ : "border-gray-200 bg-white hover:border-gray-300 dark:border-neutral-600 dark:bg-neutral-800 dark:hover:border-neutral-500"), children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)("input", { type: "radio", name: "shipping", checked: selectedId === option.id, onChange: () => onSelect(option), className: "h-4 w-4 border-gray-300 text-blue-500 focus:ring-blue-500" }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { className: "anyspend-shipping-option-name text-sm font-medium text-gray-900 dark:text-gray-100", children: option.name }), (option.description || option.estimated_days) && ((0, jsx_runtime_1.jsx)("div", { className: "anyspend-shipping-option-detail text-xs text-gray-500 dark:text-gray-400", children: option.description || (option.estimated_days && `${option.estimated_days}`) }))] })] }), (0, jsx_runtime_1.jsx)("div", { className: "anyspend-shipping-option-price text-sm font-medium text-gray-900 dark:text-gray-100", children: pricesLoading ? (0, jsx_runtime_1.jsx)(PriceSkeleton_1.PriceSkeleton, {}) : formatAmount(option.amount, tokenDecimals, tokenSymbol) })] }, option.id))) })] }));
20
21
  }
@@ -119,10 +119,11 @@ variablePricing, feeOnTop, kycEnabled = false, callbackMetadata: callbackMetadat
119
119
  isVariablePricingActive,
120
120
  variablePricingAmount,
121
121
  ]);
122
- // Get destination token metadata
123
- const { data: tokenData } = useTokenData(destinationTokenChainId, destinationTokenAddress);
124
- const tokenSymbol = tokenData?.symbol || "";
125
- const tokenDecimals = tokenData?.decimals || 18;
122
+ // Get destination token metadata. While loading, skip price-dependent UI
123
+ // since the 18-decimal fallback is wrong for non-18 tokens (e.g. USDC).
124
+ const { data: tokenData, isLoading: isTokenDataLoading } = useTokenData(destinationTokenChainId, destinationTokenAddress);
125
+ const tokenSymbol = tokenData?.symbol ?? "";
126
+ const tokenDecimals = tokenData?.decimals ?? 18;
126
127
  // Resolve USD equivalent for non-stablecoin tokens (shown in cart summary)
127
128
  const isStablecoin = useMemo(() => {
128
129
  return [
@@ -206,5 +207,5 @@ variablePricing, feeOnTop, kycEnabled = false, callbackMetadata: callbackMetadat
206
207
  successTitle: "Payment Successful",
207
208
  successDescription: "Your payment has been processed successfully.",
208
209
  ...content,
209
- }, theme: theme, children: _jsx(CheckoutLayout, { mode: mode, paymentPanel: _jsxs(_Fragment, { children: [isVariablePricingActive && tokenData && variablePricing && (_jsx(VariablePricingInput, { config: variablePricing, tokenDecimals: tokenDecimals, tokenSymbol: tokenSymbol, themeColor: themeColor, onChange: setVariablePricingAmount })), hasFormContent && (_jsxs("div", { className: "mb-6", children: [_jsx(CheckoutFormPanel, { formSchema: formSchema, formComponent: formComponent, shippingOptions: shippingOptions, collectShippingAddress: collectShippingAddress, enableDiscountCode: enableDiscountCode, validateDiscount: validateDiscount, tokenSymbol: tokenSymbol, tokenDecimals: tokenDecimals, classes: classes, formData: formData, onFormDataChange: handleFormDataChange, selectedShipping: selectedShipping, onShippingChange: handleShippingChange, appliedDiscount: appliedDiscount, onDiscountApplied: handleDiscountApplied, onDiscountRemoved: handleDiscountRemoved, shippingAddress: shippingAddress, onShippingAddressChange: setShippingAddress, checkoutFormSlot: slots?.checkoutForm }), _jsx("div", { className: "mt-6 border-t border-gray-200 dark:border-neutral-700" })] })), _jsx(CheckoutPaymentPanel, { recipientAddress: recipientAddress, destinationTokenAddress: destinationTokenAddress, destinationTokenChainId: destinationTokenChainId, totalAmount: computedTotal, buttonText: buttonText, themeColor: themeColor, returnUrl: returnUrl, returnLabel: returnLabel, onSuccess: onSuccess, onError: onError, classes: classes, defaultPaymentMethod: defaultPaymentMethod, senderAddress: senderAddress, showPoints: showPoints, showOrderId: showOrderId, callbackMetadata: checkoutFormMetadata, isFormValid: isFormValid, feeOnTop: feeOnTop, kycEnabled: kycEnabled })] }), cartPanel: _jsx(CheckoutCartPanel, { items: items, totalAmount: computedTotal, tokenSymbol: tokenSymbol, tokenDecimals: tokenDecimals, organizationName: organizationName, organizationLogo: organizationLogo, classes: classes, footer: footer, shipping: effectiveShipping, tax: typeof tax === "string" ? { amount: tax } : tax, discount: effectiveDiscount, summaryLines: summaryLines, usdEquivalent: usdEquivalent }), classes: classes }) }) }));
210
+ }, theme: theme, children: _jsx(CheckoutLayout, { mode: mode, paymentPanel: _jsxs(_Fragment, { children: [isVariablePricingActive && tokenData && variablePricing && (_jsx(VariablePricingInput, { config: variablePricing, tokenDecimals: tokenDecimals, tokenSymbol: tokenSymbol, themeColor: themeColor, onChange: setVariablePricingAmount })), hasFormContent && (_jsxs("div", { className: "mb-6", children: [_jsx(CheckoutFormPanel, { formSchema: formSchema, formComponent: formComponent, shippingOptions: shippingOptions, collectShippingAddress: collectShippingAddress, enableDiscountCode: enableDiscountCode, validateDiscount: validateDiscount, tokenSymbol: tokenSymbol, tokenDecimals: tokenDecimals, pricesLoading: isTokenDataLoading, classes: classes, formData: formData, onFormDataChange: handleFormDataChange, selectedShipping: selectedShipping, onShippingChange: handleShippingChange, appliedDiscount: appliedDiscount, onDiscountApplied: handleDiscountApplied, onDiscountRemoved: handleDiscountRemoved, shippingAddress: shippingAddress, onShippingAddressChange: setShippingAddress, checkoutFormSlot: slots?.checkoutForm }), _jsx("div", { className: "mt-6 border-t border-gray-200 dark:border-neutral-700" })] })), _jsx(CheckoutPaymentPanel, { recipientAddress: recipientAddress, destinationTokenAddress: destinationTokenAddress, destinationTokenChainId: destinationTokenChainId, totalAmount: computedTotal, buttonText: buttonText, themeColor: themeColor, returnUrl: returnUrl, returnLabel: returnLabel, onSuccess: onSuccess, onError: onError, classes: classes, defaultPaymentMethod: defaultPaymentMethod, senderAddress: senderAddress, showPoints: showPoints, showOrderId: showOrderId, callbackMetadata: checkoutFormMetadata, isFormValid: isFormValid, feeOnTop: feeOnTop, kycEnabled: kycEnabled })] }), cartPanel: _jsx(CheckoutCartPanel, { items: items, totalAmount: computedTotal, tokenSymbol: tokenSymbol, tokenDecimals: tokenDecimals, pricesLoading: isTokenDataLoading, organizationName: organizationName, organizationLogo: organizationLogo, classes: classes, footer: footer, shipping: effectiveShipping, tax: typeof tax === "string" ? { amount: tax } : tax, discount: effectiveDiscount, summaryLines: summaryLines, usdEquivalent: usdEquivalent }), classes: classes }) }) }));
210
211
  }
@@ -1,7 +1,8 @@
1
+ import type { ReactNode } from "react";
1
2
  import type { CheckoutItem, AnySpendCheckoutClasses } from "./AnySpendCheckout";
2
3
  interface CartItemRowProps {
3
4
  item: CheckoutItem;
4
- formattedPrice: string;
5
+ formattedPrice: ReactNode;
5
6
  classes?: AnySpendCheckoutClasses;
6
7
  }
7
8
  export declare function CartItemRow({ item, formattedPrice, classes }: CartItemRowProps): import("react/jsx-runtime").JSX.Element;
@@ -1,8 +1,8 @@
1
- import type { AnySpendCheckoutClasses } from "./AnySpendCheckout";
2
- import type { CheckoutSummaryLine } from "./AnySpendCheckout";
1
+ import { type ReactNode } from "react";
2
+ import type { AnySpendCheckoutClasses, CheckoutSummaryLine } from "./AnySpendCheckout";
3
3
  interface CartSummaryProps {
4
4
  /** Formatted total (final amount after all adjustments) */
5
- total: string;
5
+ total: ReactNode;
6
6
  tokenSymbol?: string;
7
7
  classes?: AnySpendCheckoutClasses;
8
8
  /** Formatted subtotal (sum of items only — shown when adjustments exist) */
@@ -25,6 +25,8 @@ interface CartSummaryProps {
25
25
  summaryLines?: CheckoutSummaryLine[];
26
26
  /** Formatted USD equivalent (e.g. "$5.56") — shown for non-stablecoin tokens */
27
27
  usdEquivalent?: string | null;
28
+ /** When true, hide adjustment rows and USD equivalent (caller passes a skeleton as `total`). */
29
+ pricesLoading?: boolean;
28
30
  }
29
- export declare function CartSummary({ total, tokenSymbol, classes, subtotal, tokenDecimals, shipping, tax, discount, summaryLines, usdEquivalent, }: CartSummaryProps): import("react/jsx-runtime").JSX.Element;
31
+ export declare function CartSummary({ total, tokenSymbol, classes, subtotal, tokenDecimals, shipping, tax, discount, summaryLines, usdEquivalent, pricesLoading, }: CartSummaryProps): import("react/jsx-runtime").JSX.Element;
30
32
  export {};
@@ -3,15 +3,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { cn } from "../../../../shared/utils/cn.js";
4
4
  import { formatTokenAmount, safeBigInt } from "../../../../shared/utils/number.js";
5
5
  import { useMemo } from "react";
6
- export function CartSummary({ total, tokenSymbol, classes, subtotal, tokenDecimals = 18, shipping, tax, discount, summaryLines, usdEquivalent, }) {
7
- const hasAdjustments = !!shipping?.amount || !!tax?.amount || !!discount?.amount || (summaryLines && summaryLines.length > 0);
8
- const formattedShipping = useMemo(() => (shipping?.amount ? formatTokenAmount(safeBigInt(shipping.amount), tokenDecimals) : null), [shipping?.amount, tokenDecimals]);
9
- const formattedTax = useMemo(() => (tax?.amount ? formatTokenAmount(safeBigInt(tax.amount), tokenDecimals) : null), [tax?.amount, tokenDecimals]);
10
- const formattedDiscount = useMemo(() => (discount?.amount ? formatTokenAmount(safeBigInt(discount.amount), tokenDecimals) : null), [discount?.amount, tokenDecimals]);
11
- const formattedSummaryLines = useMemo(() => summaryLines?.map(line => ({
12
- ...line,
13
- formattedAmount: formatTokenAmount(safeBigInt(line.amount), tokenDecimals),
14
- isNegative: safeBigInt(line.amount) < BigInt(0),
15
- })), [summaryLines, tokenDecimals]);
16
- return (_jsxs("div", { className: cn("border-t border-gray-200 pt-3 dark:border-neutral-700", classes?.cartSummary), children: [hasAdjustments && subtotal && (_jsxs("div", { className: cn("flex items-center justify-between py-1", classes?.cartSubtotal), children: [_jsx("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Subtotal" }), _jsxs("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: [subtotal, " ", tokenSymbol] })] })), formattedShipping && (_jsxs("div", { className: cn("flex items-center justify-between py-1", classes?.cartSummaryLine), children: [_jsx("span", { className: cn("text-sm text-gray-500 dark:text-gray-400", classes?.cartSummaryLineLabel), children: shipping?.label || "Shipping" }), _jsxs("span", { className: cn("text-sm text-gray-500 dark:text-gray-400", classes?.cartSummaryLineAmount), children: [formattedShipping, " ", tokenSymbol] })] })), formattedTax && (_jsxs("div", { className: cn("flex items-center justify-between py-1", classes?.cartSummaryLine), children: [_jsxs("span", { className: cn("text-sm text-gray-500 dark:text-gray-400", classes?.cartSummaryLineLabel), children: [tax?.label || "Tax", tax?.rate && _jsxs("span", { className: "ml-1 text-xs text-gray-400 dark:text-gray-500", children: ["(", tax.rate, ")"] })] }), _jsxs("span", { className: cn("text-sm text-gray-500 dark:text-gray-400", classes?.cartSummaryLineAmount), children: [formattedTax, " ", tokenSymbol] })] })), formattedDiscount && (_jsxs("div", { className: cn("flex items-center justify-between py-1", classes?.cartDiscount), children: [_jsxs("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: [discount?.label || "Discount", discount?.code && (_jsx("span", { className: "ml-1 rounded bg-green-50 px-1.5 py-0.5 text-xs font-medium text-green-600 dark:bg-green-900/20 dark:text-green-400", children: discount.code }))] }), _jsxs("span", { className: "text-sm font-medium text-green-600 dark:text-green-400", children: ["-", formattedDiscount, " ", tokenSymbol] })] })), formattedSummaryLines?.map(line => (_jsxs("div", { className: cn("flex items-center justify-between py-1", classes?.cartSummaryLine), children: [_jsxs("span", { className: cn("text-sm text-gray-500 dark:text-gray-400", classes?.cartSummaryLineLabel), children: [line.label, line.description && (_jsxs("span", { className: "ml-1 text-xs text-gray-400 dark:text-gray-500", children: ["(", line.description, ")"] }))] }), _jsxs("span", { className: cn("text-sm", line.isNegative ? "font-medium text-green-600 dark:text-green-400" : "text-gray-500 dark:text-gray-400", classes?.cartSummaryLineAmount), children: [line.formattedAmount, " ", tokenSymbol] })] }, line.label))), _jsxs("div", { className: cn("flex flex-col", hasAdjustments && "mt-1 border-t border-gray-100 pt-2 dark:border-neutral-800"), children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-base font-semibold text-gray-900 dark:text-gray-100", children: "Total" }), _jsxs("span", { className: cn("text-base font-semibold text-gray-900 dark:text-gray-100", classes?.cartTotal), children: [total, " ", tokenSymbol] })] }), usdEquivalent && (_jsx("div", { className: "flex justify-end", children: _jsxs("span", { className: "text-xs text-gray-400 dark:text-gray-500", children: ["~", usdEquivalent, " USD"] }) }))] })] }));
6
+ export function CartSummary({ total, tokenSymbol, classes, subtotal, tokenDecimals = 18, shipping, tax, discount, summaryLines, usdEquivalent, pricesLoading = false, }) {
7
+ const formattedShipping = useMemo(() => (!pricesLoading && shipping?.amount ? formatTokenAmount(safeBigInt(shipping.amount), tokenDecimals) : null), [pricesLoading, shipping?.amount, tokenDecimals]);
8
+ const formattedTax = useMemo(() => (!pricesLoading && tax?.amount ? formatTokenAmount(safeBigInt(tax.amount), tokenDecimals) : null), [pricesLoading, tax?.amount, tokenDecimals]);
9
+ const formattedDiscount = useMemo(() => (!pricesLoading && discount?.amount ? formatTokenAmount(safeBigInt(discount.amount), tokenDecimals) : null), [pricesLoading, discount?.amount, tokenDecimals]);
10
+ const formattedSummaryLines = useMemo(() => pricesLoading
11
+ ? undefined
12
+ : summaryLines?.map(line => ({
13
+ ...line,
14
+ formattedAmount: formatTokenAmount(safeBigInt(line.amount), tokenDecimals),
15
+ isNegative: safeBigInt(line.amount) < BigInt(0),
16
+ })), [pricesLoading, summaryLines, tokenDecimals]);
17
+ const hasAdjustments = !!formattedShipping || !!formattedTax || !!formattedDiscount || !!formattedSummaryLines?.length;
18
+ return (_jsxs("div", { className: cn("border-t border-gray-200 pt-3 dark:border-neutral-700", classes?.cartSummary), children: [hasAdjustments && subtotal && (_jsxs("div", { className: cn("flex items-center justify-between py-1", classes?.cartSubtotal), children: [_jsx("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Subtotal" }), _jsxs("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: [subtotal, " ", tokenSymbol] })] })), formattedShipping && (_jsxs("div", { className: cn("flex items-center justify-between py-1", classes?.cartSummaryLine), children: [_jsx("span", { className: cn("text-sm text-gray-500 dark:text-gray-400", classes?.cartSummaryLineLabel), children: shipping?.label || "Shipping" }), _jsxs("span", { className: cn("text-sm text-gray-500 dark:text-gray-400", classes?.cartSummaryLineAmount), children: [formattedShipping, " ", tokenSymbol] })] })), formattedTax && (_jsxs("div", { className: cn("flex items-center justify-between py-1", classes?.cartSummaryLine), children: [_jsxs("span", { className: cn("text-sm text-gray-500 dark:text-gray-400", classes?.cartSummaryLineLabel), children: [tax?.label || "Tax", tax?.rate && _jsxs("span", { className: "ml-1 text-xs text-gray-400 dark:text-gray-500", children: ["(", tax.rate, ")"] })] }), _jsxs("span", { className: cn("text-sm text-gray-500 dark:text-gray-400", classes?.cartSummaryLineAmount), children: [formattedTax, " ", tokenSymbol] })] })), formattedDiscount && (_jsxs("div", { className: cn("flex items-center justify-between py-1", classes?.cartDiscount), children: [_jsxs("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: [discount?.label || "Discount", discount?.code && (_jsx("span", { className: "ml-1 rounded bg-green-50 px-1.5 py-0.5 text-xs font-medium text-green-600 dark:bg-green-900/20 dark:text-green-400", children: discount.code }))] }), _jsxs("span", { className: "text-sm font-medium text-green-600 dark:text-green-400", children: ["-", formattedDiscount, " ", tokenSymbol] })] })), formattedSummaryLines?.map(line => (_jsxs("div", { className: cn("flex items-center justify-between py-1", classes?.cartSummaryLine), children: [_jsxs("span", { className: cn("text-sm text-gray-500 dark:text-gray-400", classes?.cartSummaryLineLabel), children: [line.label, line.description && (_jsxs("span", { className: "ml-1 text-xs text-gray-400 dark:text-gray-500", children: ["(", line.description, ")"] }))] }), _jsxs("span", { className: cn("text-sm", line.isNegative ? "font-medium text-green-600 dark:text-green-400" : "text-gray-500 dark:text-gray-400", classes?.cartSummaryLineAmount), children: [line.formattedAmount, " ", tokenSymbol] })] }, line.label))), _jsxs("div", { className: cn("flex flex-col", hasAdjustments && "mt-1 border-t border-gray-100 pt-2 dark:border-neutral-800"), children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-base font-semibold text-gray-900 dark:text-gray-100", children: "Total" }), _jsxs("span", { className: cn("text-base font-semibold text-gray-900 dark:text-gray-100", classes?.cartTotal), children: [total, " ", tokenSymbol] })] }), !pricesLoading && usdEquivalent && (_jsx("div", { className: "flex justify-end", children: _jsxs("span", { className: "text-xs text-gray-400 dark:text-gray-500", children: ["~", usdEquivalent, " USD"] }) }))] })] }));
17
19
  }
@@ -5,6 +5,8 @@ interface CheckoutCartPanelProps {
5
5
  totalAmount: string;
6
6
  tokenSymbol?: string;
7
7
  tokenDecimals?: number;
8
+ /** True while token decimals/symbol are still loading — prevents rendering mis-decimalized prices. */
9
+ pricesLoading?: boolean;
8
10
  organizationName?: string;
9
11
  organizationLogo?: string;
10
12
  classes?: AnySpendCheckoutClasses;
@@ -28,5 +30,5 @@ interface CheckoutCartPanelProps {
28
30
  /** Formatted USD equivalent (e.g. "$5.56") — shown for non-stablecoin tokens */
29
31
  usdEquivalent?: string | null;
30
32
  }
31
- export declare function CheckoutCartPanel({ items, totalAmount, tokenSymbol, tokenDecimals, organizationName, organizationLogo, classes, footer, shipping, tax, discount, summaryLines, usdEquivalent, }: CheckoutCartPanelProps): import("react/jsx-runtime").JSX.Element;
33
+ export declare function CheckoutCartPanel({ items, totalAmount, tokenSymbol, tokenDecimals, pricesLoading, organizationName, organizationLogo, classes, footer, shipping, tax, discount, summaryLines, usdEquivalent, }: CheckoutCartPanelProps): import("react/jsx-runtime").JSX.Element;
32
34
  export {};
@@ -6,7 +6,8 @@ import { useMemo } from "react";
6
6
  import { CartItemRow } from "./CartItemRow.js";
7
7
  import { CartSummary } from "./CartSummary.js";
8
8
  import { PoweredByBranding } from "./PoweredByBranding.js";
9
- export function CheckoutCartPanel({ items, totalAmount, tokenSymbol = "", tokenDecimals = 18, organizationName, organizationLogo, classes, footer, shipping, tax, discount, summaryLines, usdEquivalent, }) {
9
+ import { PriceSkeleton } from "./PriceSkeleton.js";
10
+ export function CheckoutCartPanel({ items, totalAmount, tokenSymbol = "", tokenDecimals = 18, pricesLoading = false, organizationName, organizationLogo, classes, footer, shipping, tax, discount, summaryLines, usdEquivalent, }) {
10
11
  const formattedTotal = useMemo(() => formatTokenAmount(safeBigInt(totalAmount), tokenDecimals), [totalAmount, tokenDecimals]);
11
12
  // Compute subtotal from items only (before adjustments)
12
13
  const formattedSubtotal = useMemo(() => {
@@ -16,9 +17,9 @@ export function CheckoutCartPanel({ items, totalAmount, tokenSymbol = "", tokenD
16
17
  }
17
18
  return formatTokenAmount(subtotal, tokenDecimals);
18
19
  }, [items, tokenDecimals]);
19
- return (_jsxs("div", { className: cn("anyspend-cart-panel flex flex-col", classes?.cartPanel), children: [_jsx("h2", { className: cn("anyspend-cart-title mb-4 text-lg font-semibold text-gray-900 dark:text-gray-100", classes?.cartTitle), children: "Order Summary" }), _jsx("div", { className: "anyspend-cart-items divide-y divide-gray-100 dark:divide-gray-800", children: items.map((item, index) => {
20
+ return (_jsxs("div", { className: cn("anyspend-cart-panel flex flex-col", classes?.cartPanel), "aria-busy": pricesLoading || undefined, children: [_jsx("h2", { className: cn("anyspend-cart-title mb-4 text-lg font-semibold text-gray-900 dark:text-gray-100", classes?.cartTitle), children: "Order Summary" }), _jsx("div", { className: "anyspend-cart-items divide-y divide-gray-100 dark:divide-gray-800", children: items.map((item, index) => {
20
21
  const itemTotal = safeBigInt(item.amount) * BigInt(item.quantity);
21
- const formattedPrice = `${formatTokenAmount(itemTotal, tokenDecimals)} ${tokenSymbol}`;
22
+ const formattedPrice = pricesLoading ? (_jsx(PriceSkeleton, {})) : (`${formatTokenAmount(itemTotal, tokenDecimals)} ${tokenSymbol}`);
22
23
  return _jsx(CartItemRow, { item: item, formattedPrice: formattedPrice, classes: classes }, item.id || index);
23
- }) }), _jsx(CartSummary, { total: formattedTotal, tokenSymbol: tokenSymbol, classes: classes, subtotal: formattedSubtotal, tokenDecimals: tokenDecimals, shipping: shipping, tax: tax, discount: discount, summaryLines: summaryLines, usdEquivalent: usdEquivalent }), footer === undefined ? (_jsx(PoweredByBranding, { organizationName: organizationName, organizationLogo: organizationLogo, classes: classes })) : (footer)] }));
24
+ }) }), _jsx(CartSummary, { total: pricesLoading ? _jsx(PriceSkeleton, { className: "w-20" }) : formattedTotal, tokenSymbol: tokenSymbol, classes: classes, subtotal: formattedSubtotal, tokenDecimals: tokenDecimals, shipping: shipping, tax: tax, discount: discount, summaryLines: summaryLines, usdEquivalent: usdEquivalent, pricesLoading: pricesLoading }), footer === undefined ? (_jsx(PoweredByBranding, { organizationName: organizationName, organizationLogo: organizationLogo, classes: classes })) : (footer)] }));
24
25
  }
@@ -16,6 +16,8 @@ interface CheckoutFormPanelProps {
16
16
  /** Token info for display */
17
17
  tokenSymbol?: string;
18
18
  tokenDecimals?: number;
19
+ /** True while token decimals/symbol are still loading — prevents rendering mis-decimalized prices. */
20
+ pricesLoading?: boolean;
19
21
  /** CSS class overrides */
20
22
  classes?: AnySpendCheckoutClasses;
21
23
  /** Current form data (lifted state) */
@@ -35,5 +37,5 @@ interface CheckoutFormPanelProps {
35
37
  /** Slot overrides */
36
38
  checkoutFormSlot?: (props: CheckoutFormComponentProps) => React.ReactNode;
37
39
  }
38
- export declare function CheckoutFormPanel({ formSchema, formComponent: FormComponent, shippingOptions, collectShippingAddress, enableDiscountCode, validateDiscount, tokenSymbol, tokenDecimals, classes, formData, onFormDataChange, selectedShipping, onShippingChange, appliedDiscount, onDiscountApplied, onDiscountRemoved, shippingAddress, onShippingAddressChange, checkoutFormSlot, }: CheckoutFormPanelProps): import("react/jsx-runtime").JSX.Element | null;
40
+ export declare function CheckoutFormPanel({ formSchema, formComponent: FormComponent, shippingOptions, collectShippingAddress, enableDiscountCode, validateDiscount, tokenSymbol, tokenDecimals, pricesLoading, classes, formData, onFormDataChange, selectedShipping, onShippingChange, appliedDiscount, onDiscountApplied, onDiscountRemoved, shippingAddress, onShippingAddressChange, checkoutFormSlot, }: CheckoutFormPanelProps): import("react/jsx-runtime").JSX.Element | null;
39
41
  export {};
@@ -7,7 +7,7 @@ import { AddressForm } from "./AddressForm.js";
7
7
  import { ShippingSelector } from "./ShippingSelector.js";
8
8
  import { DiscountCodeInput } from "./DiscountCodeInput.js";
9
9
  const emptyAddress = { street: "", city: "", state: "", zip: "", country: "" };
10
- export function CheckoutFormPanel({ formSchema, formComponent: FormComponent, shippingOptions, collectShippingAddress, enableDiscountCode, validateDiscount, tokenSymbol, tokenDecimals, classes, formData, onFormDataChange, selectedShipping, onShippingChange, appliedDiscount, onDiscountApplied, onDiscountRemoved, shippingAddress, onShippingAddressChange, checkoutFormSlot, }) {
10
+ export function CheckoutFormPanel({ formSchema, formComponent: FormComponent, shippingOptions, collectShippingAddress, enableDiscountCode, validateDiscount, tokenSymbol, tokenDecimals, pricesLoading = false, classes, formData, onFormDataChange, selectedShipping, onShippingChange, appliedDiscount, onDiscountApplied, onDiscountRemoved, shippingAddress, onShippingAddressChange, checkoutFormSlot, }) {
11
11
  const [errors, setErrors] = useState({});
12
12
  const hasFormFields = formSchema && formSchema.fields.length > 0;
13
13
  const hasShipping = shippingOptions && shippingOptions.length > 0;
@@ -53,7 +53,7 @@ export function CheckoutFormPanel({ formSchema, formComponent: FormComponent, sh
53
53
  if (!hasAnyContent)
54
54
  return null;
55
55
  // Shared shipping + discount section
56
- const shippingAndDiscount = (_jsxs(_Fragment, { children: [hasShipping && shippingOptions && (_jsx(ShippingSelector, { options: shippingOptions, selectedId: selectedShipping?.id || null, onSelect: onShippingChange, tokenSymbol: tokenSymbol, tokenDecimals: tokenDecimals })), enableDiscountCode && validateDiscount && (_jsx(DiscountCodeInput, { onApply: handleDiscountApply, appliedDiscount: appliedDiscount, onRemove: onDiscountRemoved, tokenSymbol: tokenSymbol, tokenDecimals: tokenDecimals }))] }));
56
+ const shippingAndDiscount = (_jsxs(_Fragment, { children: [hasShipping && shippingOptions && (_jsx(ShippingSelector, { options: shippingOptions, selectedId: selectedShipping?.id || null, onSelect: onShippingChange, tokenSymbol: tokenSymbol, tokenDecimals: tokenDecimals, pricesLoading: pricesLoading })), enableDiscountCode && validateDiscount && (_jsx(DiscountCodeInput, { onApply: handleDiscountApply, appliedDiscount: appliedDiscount, onRemove: onDiscountRemoved, tokenSymbol: tokenSymbol, tokenDecimals: tokenDecimals, pricesLoading: pricesLoading }))] }));
57
57
  // Render custom form slot if provided
58
58
  if (checkoutFormSlot) {
59
59
  return (_jsxs("div", { className: cn("anyspend-form-panel space-y-4", classes?.formPanel), children: [_jsx("div", { className: "anyspend-form-slot", children: checkoutFormSlot({
@@ -6,7 +6,9 @@ interface DiscountCodeInputProps {
6
6
  loading?: boolean;
7
7
  tokenSymbol?: string;
8
8
  tokenDecimals?: number;
9
+ /** True while token decimals/symbol are still loading — renders applied amount as skeleton. */
10
+ pricesLoading?: boolean;
9
11
  className?: string;
10
12
  }
11
- export declare function DiscountCodeInput({ onApply, appliedDiscount, onRemove, loading, tokenSymbol, tokenDecimals, className, }: DiscountCodeInputProps): import("react/jsx-runtime").JSX.Element;
13
+ export declare function DiscountCodeInput({ onApply, appliedDiscount, onRemove, loading, tokenSymbol, tokenDecimals, pricesLoading, className, }: DiscountCodeInputProps): import("react/jsx-runtime").JSX.Element;
12
14
  export {};
@@ -4,13 +4,14 @@ import { cn } from "../../../../shared/utils/cn.js";
4
4
  import { formatTokenAmount, safeBigInt } from "../../../../shared/utils/number.js";
5
5
  import { X, Loader2, Check } from "lucide-react";
6
6
  import { useState, useCallback } from "react";
7
+ import { PriceSkeleton } from "./PriceSkeleton.js";
7
8
  function formatAmount(amount, decimals, symbol) {
8
9
  const bi = safeBigInt(amount);
9
10
  if (bi === BigInt(0))
10
11
  return "Free";
11
12
  return `${formatTokenAmount(bi, decimals)} ${symbol}`;
12
13
  }
13
- export function DiscountCodeInput({ onApply, appliedDiscount, onRemove, loading = false, tokenSymbol = "", tokenDecimals = 6, className, }) {
14
+ export function DiscountCodeInput({ onApply, appliedDiscount, onRemove, loading = false, tokenSymbol = "", tokenDecimals = 6, pricesLoading = false, className, }) {
14
15
  const [code, setCode] = useState("");
15
16
  const [error, setError] = useState(null);
16
17
  const [isValidating, setIsValidating] = useState(false);
@@ -41,11 +42,7 @@ export function DiscountCodeInput({ onApply, appliedDiscount, onRemove, loading
41
42
  };
42
43
  // Show applied discount state
43
44
  if (appliedDiscount?.valid) {
44
- return (_jsxs("div", { className: cn("anyspend-discount anyspend-discount-applied space-y-2", className), children: [_jsx("div", { className: "anyspend-discount-title text-sm font-semibold text-gray-900 dark:text-gray-100", children: "Discount" }), _jsxs("div", { className: "anyspend-discount-badge flex items-center justify-between rounded-lg border border-green-200 bg-green-50 px-3 py-2 dark:border-green-800 dark:bg-green-900/20", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Check, { className: "h-4 w-4 text-green-600 dark:text-green-400" }), _jsx("span", { className: "anyspend-discount-value text-sm font-medium text-green-700 dark:text-green-300", children: appliedDiscount.discount_type === "percentage"
45
- ? `${appliedDiscount.discount_value}% off`
46
- : appliedDiscount.discount_amount
47
- ? `-${formatAmount(appliedDiscount.discount_amount, tokenDecimals, tokenSymbol)}`
48
- : "Discount applied" })] }), _jsx("button", { type: "button", onClick: handleRemove, className: "anyspend-discount-remove rounded p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300", children: _jsx(X, { className: "h-3.5 w-3.5" }) })] })] }));
45
+ return (_jsxs("div", { className: cn("anyspend-discount anyspend-discount-applied space-y-2", className), children: [_jsx("div", { className: "anyspend-discount-title text-sm font-semibold text-gray-900 dark:text-gray-100", children: "Discount" }), _jsxs("div", { className: "anyspend-discount-badge flex items-center justify-between rounded-lg border border-green-200 bg-green-50 px-3 py-2 dark:border-green-800 dark:bg-green-900/20", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Check, { className: "h-4 w-4 text-green-600 dark:text-green-400" }), _jsx("span", { className: "anyspend-discount-value text-sm font-medium text-green-700 dark:text-green-300", children: appliedDiscount.discount_type === "percentage" ? (`${appliedDiscount.discount_value}% off`) : appliedDiscount.discount_amount ? (pricesLoading ? (_jsx(PriceSkeleton, {})) : (`-${formatAmount(appliedDiscount.discount_amount, tokenDecimals, tokenSymbol)}`)) : ("Discount applied") })] }), _jsx("button", { type: "button", onClick: handleRemove, className: "anyspend-discount-remove rounded p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300", children: _jsx(X, { className: "h-3.5 w-3.5" }) })] })] }));
49
46
  }
50
47
  return (_jsxs("div", { className: cn("anyspend-discount space-y-2", className), children: [_jsx("div", { className: "anyspend-discount-title text-sm font-semibold text-gray-900 dark:text-gray-100", children: "Discount Code" }), _jsxs("div", { className: "anyspend-discount-input-row flex gap-2", children: [_jsx("input", { type: "text", value: code, onChange: e => {
51
48
  setCode(e.target.value.toUpperCase());
@@ -0,0 +1,5 @@
1
+ interface PriceSkeletonProps {
2
+ className?: string;
3
+ }
4
+ export declare function PriceSkeleton({ className }: PriceSkeletonProps): import("react/jsx-runtime").JSX.Element;
5
+ export {};
@@ -0,0 +1,6 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { cn } from "../../../../shared/utils/cn.js";
4
+ export function PriceSkeleton({ className }) {
5
+ return (_jsx("span", { className: cn("animate-pulse-fade bg-b3-react-background inline-block h-4 w-16 rounded-md align-middle", className), "aria-hidden": "true" }));
6
+ }
@@ -5,7 +5,9 @@ interface ShippingSelectorProps {
5
5
  onSelect: (option: ShippingOption) => void;
6
6
  tokenSymbol?: string;
7
7
  tokenDecimals?: number;
8
+ /** True while token decimals/symbol are still loading — renders price as skeleton. */
9
+ pricesLoading?: boolean;
8
10
  className?: string;
9
11
  }
10
- export declare function ShippingSelector({ options, selectedId, onSelect, tokenSymbol, tokenDecimals, className, }: ShippingSelectorProps): import("react/jsx-runtime").JSX.Element | null;
12
+ export declare function ShippingSelector({ options, selectedId, onSelect, tokenSymbol, tokenDecimals, pricesLoading, className, }: ShippingSelectorProps): import("react/jsx-runtime").JSX.Element | null;
11
13
  export {};
@@ -2,16 +2,17 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { cn } from "../../../../shared/utils/cn.js";
4
4
  import { formatTokenAmount, safeBigInt } from "../../../../shared/utils/number.js";
5
+ import { PriceSkeleton } from "./PriceSkeleton.js";
5
6
  function formatAmount(amount, decimals, symbol) {
6
7
  const bi = safeBigInt(amount);
7
8
  if (bi === BigInt(0))
8
9
  return "Free";
9
10
  return `${formatTokenAmount(bi, decimals)} ${symbol}`;
10
11
  }
11
- export function ShippingSelector({ options, selectedId, onSelect, tokenSymbol = "", tokenDecimals = 6, className, }) {
12
+ export function ShippingSelector({ options, selectedId, onSelect, tokenSymbol = "", tokenDecimals = 6, pricesLoading = false, className, }) {
12
13
  if (options.length === 0)
13
14
  return null;
14
15
  return (_jsxs("div", { className: cn("anyspend-shipping-selector space-y-2", className), children: [_jsx("div", { className: "anyspend-shipping-title text-sm font-semibold text-gray-900 dark:text-gray-100", children: "Shipping" }), _jsx("div", { className: "anyspend-shipping-options space-y-2", children: options.map(option => (_jsxs("label", { className: cn("anyspend-shipping-option flex cursor-pointer items-center justify-between rounded-lg border p-3 transition-colors", selectedId === option.id
15
16
  ? "anyspend-shipping-option-selected border-blue-500 bg-blue-50 dark:border-blue-400 dark:bg-blue-900/20"
16
- : "border-gray-200 bg-white hover:border-gray-300 dark:border-neutral-600 dark:bg-neutral-800 dark:hover:border-neutral-500"), children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("input", { type: "radio", name: "shipping", checked: selectedId === option.id, onChange: () => onSelect(option), className: "h-4 w-4 border-gray-300 text-blue-500 focus:ring-blue-500" }), _jsxs("div", { children: [_jsx("div", { className: "anyspend-shipping-option-name text-sm font-medium text-gray-900 dark:text-gray-100", children: option.name }), (option.description || option.estimated_days) && (_jsx("div", { className: "anyspend-shipping-option-detail text-xs text-gray-500 dark:text-gray-400", children: option.description || (option.estimated_days && `${option.estimated_days}`) }))] })] }), _jsx("div", { className: "anyspend-shipping-option-price text-sm font-medium text-gray-900 dark:text-gray-100", children: formatAmount(option.amount, tokenDecimals, tokenSymbol) })] }, option.id))) })] }));
17
+ : "border-gray-200 bg-white hover:border-gray-300 dark:border-neutral-600 dark:bg-neutral-800 dark:hover:border-neutral-500"), children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("input", { type: "radio", name: "shipping", checked: selectedId === option.id, onChange: () => onSelect(option), className: "h-4 w-4 border-gray-300 text-blue-500 focus:ring-blue-500" }), _jsxs("div", { children: [_jsx("div", { className: "anyspend-shipping-option-name text-sm font-medium text-gray-900 dark:text-gray-100", children: option.name }), (option.description || option.estimated_days) && (_jsx("div", { className: "anyspend-shipping-option-detail text-xs text-gray-500 dark:text-gray-400", children: option.description || (option.estimated_days && `${option.estimated_days}`) }))] })] }), _jsx("div", { className: "anyspend-shipping-option-price text-sm font-medium text-gray-900 dark:text-gray-100", children: pricesLoading ? _jsx(PriceSkeleton, {}) : formatAmount(option.amount, tokenDecimals, tokenSymbol) })] }, option.id))) })] }));
17
18
  }
@@ -1,7 +1,8 @@
1
+ import type { ReactNode } from "react";
1
2
  import type { CheckoutItem, AnySpendCheckoutClasses } from "./AnySpendCheckout";
2
3
  interface CartItemRowProps {
3
4
  item: CheckoutItem;
4
- formattedPrice: string;
5
+ formattedPrice: ReactNode;
5
6
  classes?: AnySpendCheckoutClasses;
6
7
  }
7
8
  export declare function CartItemRow({ item, formattedPrice, classes }: CartItemRowProps): import("react/jsx-runtime").JSX.Element;
@@ -1,8 +1,8 @@
1
- import type { AnySpendCheckoutClasses } from "./AnySpendCheckout";
2
- import type { CheckoutSummaryLine } from "./AnySpendCheckout";
1
+ import { type ReactNode } from "react";
2
+ import type { AnySpendCheckoutClasses, CheckoutSummaryLine } from "./AnySpendCheckout";
3
3
  interface CartSummaryProps {
4
4
  /** Formatted total (final amount after all adjustments) */
5
- total: string;
5
+ total: ReactNode;
6
6
  tokenSymbol?: string;
7
7
  classes?: AnySpendCheckoutClasses;
8
8
  /** Formatted subtotal (sum of items only — shown when adjustments exist) */
@@ -25,6 +25,8 @@ interface CartSummaryProps {
25
25
  summaryLines?: CheckoutSummaryLine[];
26
26
  /** Formatted USD equivalent (e.g. "$5.56") — shown for non-stablecoin tokens */
27
27
  usdEquivalent?: string | null;
28
+ /** When true, hide adjustment rows and USD equivalent (caller passes a skeleton as `total`). */
29
+ pricesLoading?: boolean;
28
30
  }
29
- export declare function CartSummary({ total, tokenSymbol, classes, subtotal, tokenDecimals, shipping, tax, discount, summaryLines, usdEquivalent, }: CartSummaryProps): import("react/jsx-runtime").JSX.Element;
31
+ export declare function CartSummary({ total, tokenSymbol, classes, subtotal, tokenDecimals, shipping, tax, discount, summaryLines, usdEquivalent, pricesLoading, }: CartSummaryProps): import("react/jsx-runtime").JSX.Element;
30
32
  export {};
@@ -5,6 +5,8 @@ interface CheckoutCartPanelProps {
5
5
  totalAmount: string;
6
6
  tokenSymbol?: string;
7
7
  tokenDecimals?: number;
8
+ /** True while token decimals/symbol are still loading — prevents rendering mis-decimalized prices. */
9
+ pricesLoading?: boolean;
8
10
  organizationName?: string;
9
11
  organizationLogo?: string;
10
12
  classes?: AnySpendCheckoutClasses;
@@ -28,5 +30,5 @@ interface CheckoutCartPanelProps {
28
30
  /** Formatted USD equivalent (e.g. "$5.56") — shown for non-stablecoin tokens */
29
31
  usdEquivalent?: string | null;
30
32
  }
31
- export declare function CheckoutCartPanel({ items, totalAmount, tokenSymbol, tokenDecimals, organizationName, organizationLogo, classes, footer, shipping, tax, discount, summaryLines, usdEquivalent, }: CheckoutCartPanelProps): import("react/jsx-runtime").JSX.Element;
33
+ export declare function CheckoutCartPanel({ items, totalAmount, tokenSymbol, tokenDecimals, pricesLoading, organizationName, organizationLogo, classes, footer, shipping, tax, discount, summaryLines, usdEquivalent, }: CheckoutCartPanelProps): import("react/jsx-runtime").JSX.Element;
32
34
  export {};
@@ -16,6 +16,8 @@ interface CheckoutFormPanelProps {
16
16
  /** Token info for display */
17
17
  tokenSymbol?: string;
18
18
  tokenDecimals?: number;
19
+ /** True while token decimals/symbol are still loading — prevents rendering mis-decimalized prices. */
20
+ pricesLoading?: boolean;
19
21
  /** CSS class overrides */
20
22
  classes?: AnySpendCheckoutClasses;
21
23
  /** Current form data (lifted state) */
@@ -35,5 +37,5 @@ interface CheckoutFormPanelProps {
35
37
  /** Slot overrides */
36
38
  checkoutFormSlot?: (props: CheckoutFormComponentProps) => React.ReactNode;
37
39
  }
38
- export declare function CheckoutFormPanel({ formSchema, formComponent: FormComponent, shippingOptions, collectShippingAddress, enableDiscountCode, validateDiscount, tokenSymbol, tokenDecimals, classes, formData, onFormDataChange, selectedShipping, onShippingChange, appliedDiscount, onDiscountApplied, onDiscountRemoved, shippingAddress, onShippingAddressChange, checkoutFormSlot, }: CheckoutFormPanelProps): import("react/jsx-runtime").JSX.Element | null;
40
+ export declare function CheckoutFormPanel({ formSchema, formComponent: FormComponent, shippingOptions, collectShippingAddress, enableDiscountCode, validateDiscount, tokenSymbol, tokenDecimals, pricesLoading, classes, formData, onFormDataChange, selectedShipping, onShippingChange, appliedDiscount, onDiscountApplied, onDiscountRemoved, shippingAddress, onShippingAddressChange, checkoutFormSlot, }: CheckoutFormPanelProps): import("react/jsx-runtime").JSX.Element | null;
39
41
  export {};
@@ -6,7 +6,9 @@ interface DiscountCodeInputProps {
6
6
  loading?: boolean;
7
7
  tokenSymbol?: string;
8
8
  tokenDecimals?: number;
9
+ /** True while token decimals/symbol are still loading — renders applied amount as skeleton. */
10
+ pricesLoading?: boolean;
9
11
  className?: string;
10
12
  }
11
- export declare function DiscountCodeInput({ onApply, appliedDiscount, onRemove, loading, tokenSymbol, tokenDecimals, className, }: DiscountCodeInputProps): import("react/jsx-runtime").JSX.Element;
13
+ export declare function DiscountCodeInput({ onApply, appliedDiscount, onRemove, loading, tokenSymbol, tokenDecimals, pricesLoading, className, }: DiscountCodeInputProps): import("react/jsx-runtime").JSX.Element;
12
14
  export {};
@@ -0,0 +1,5 @@
1
+ interface PriceSkeletonProps {
2
+ className?: string;
3
+ }
4
+ export declare function PriceSkeleton({ className }: PriceSkeletonProps): import("react/jsx-runtime").JSX.Element;
5
+ export {};
@@ -5,7 +5,9 @@ interface ShippingSelectorProps {
5
5
  onSelect: (option: ShippingOption) => void;
6
6
  tokenSymbol?: string;
7
7
  tokenDecimals?: number;
8
+ /** True while token decimals/symbol are still loading — renders price as skeleton. */
9
+ pricesLoading?: boolean;
8
10
  className?: string;
9
11
  }
10
- export declare function ShippingSelector({ options, selectedId, onSelect, tokenSymbol, tokenDecimals, className, }: ShippingSelectorProps): import("react/jsx-runtime").JSX.Element | null;
12
+ export declare function ShippingSelector({ options, selectedId, onSelect, tokenSymbol, tokenDecimals, pricesLoading, className, }: ShippingSelectorProps): import("react/jsx-runtime").JSX.Element | null;
11
13
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b3dotfun/sdk",
3
- "version": "0.1.69-alpha.20",
3
+ "version": "0.1.69-alpha.21",
4
4
  "source": "src/index.ts",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "react-native": "./dist/cjs/index.native.js",
@@ -295,10 +295,14 @@ export function AnySpendCheckout({
295
295
  variablePricingAmount,
296
296
  ]);
297
297
 
298
- // Get destination token metadata
299
- const { data: tokenData } = useTokenData(destinationTokenChainId, destinationTokenAddress);
300
- const tokenSymbol = tokenData?.symbol || "";
301
- const tokenDecimals = tokenData?.decimals || 18;
298
+ // Get destination token metadata. While loading, skip price-dependent UI
299
+ // since the 18-decimal fallback is wrong for non-18 tokens (e.g. USDC).
300
+ const { data: tokenData, isLoading: isTokenDataLoading } = useTokenData(
301
+ destinationTokenChainId,
302
+ destinationTokenAddress,
303
+ );
304
+ const tokenSymbol = tokenData?.symbol ?? "";
305
+ const tokenDecimals = tokenData?.decimals ?? 18;
302
306
 
303
307
  // Resolve USD equivalent for non-stablecoin tokens (shown in cart summary)
304
308
  const isStablecoin = useMemo(() => {
@@ -412,6 +416,7 @@ export function AnySpendCheckout({
412
416
  validateDiscount={validateDiscount}
413
417
  tokenSymbol={tokenSymbol}
414
418
  tokenDecimals={tokenDecimals}
419
+ pricesLoading={isTokenDataLoading}
415
420
  classes={classes}
416
421
  formData={formData}
417
422
  onFormDataChange={handleFormDataChange}
@@ -456,6 +461,7 @@ export function AnySpendCheckout({
456
461
  totalAmount={computedTotal}
457
462
  tokenSymbol={tokenSymbol}
458
463
  tokenDecimals={tokenDecimals}
464
+ pricesLoading={isTokenDataLoading}
459
465
  organizationName={organizationName}
460
466
  organizationLogo={organizationLogo}
461
467
  classes={classes}
@@ -1,11 +1,12 @@
1
1
  "use client";
2
2
 
3
3
  import { cn } from "@b3dotfun/sdk/shared/utils/cn";
4
+ import type { ReactNode } from "react";
4
5
  import type { CheckoutItem, AnySpendCheckoutClasses } from "./AnySpendCheckout";
5
6
 
6
7
  interface CartItemRowProps {
7
8
  item: CheckoutItem;
8
- formattedPrice: string;
9
+ formattedPrice: ReactNode;
9
10
  classes?: AnySpendCheckoutClasses;
10
11
  }
11
12
 
@@ -2,13 +2,12 @@
2
2
 
3
3
  import { cn } from "@b3dotfun/sdk/shared/utils/cn";
4
4
  import { formatTokenAmount, safeBigInt } from "@b3dotfun/sdk/shared/utils/number";
5
- import { useMemo } from "react";
6
- import type { AnySpendCheckoutClasses } from "./AnySpendCheckout";
7
- import type { CheckoutSummaryLine } from "./AnySpendCheckout";
5
+ import { useMemo, type ReactNode } from "react";
6
+ import type { AnySpendCheckoutClasses, CheckoutSummaryLine } from "./AnySpendCheckout";
8
7
 
9
8
  interface CartSummaryProps {
10
9
  /** Formatted total (final amount after all adjustments) */
11
- total: string;
10
+ total: ReactNode;
12
11
  tokenSymbol?: string;
13
12
  classes?: AnySpendCheckoutClasses;
14
13
  /** Formatted subtotal (sum of items only — shown when adjustments exist) */
@@ -20,6 +19,8 @@ interface CartSummaryProps {
20
19
  summaryLines?: CheckoutSummaryLine[];
21
20
  /** Formatted USD equivalent (e.g. "$5.56") — shown for non-stablecoin tokens */
22
21
  usdEquivalent?: string | null;
22
+ /** When true, hide adjustment rows and USD equivalent (caller passes a skeleton as `total`). */
23
+ pricesLoading?: boolean;
23
24
  }
24
25
 
25
26
  export function CartSummary({
@@ -33,35 +34,38 @@ export function CartSummary({
33
34
  discount,
34
35
  summaryLines,
35
36
  usdEquivalent,
37
+ pricesLoading = false,
36
38
  }: CartSummaryProps) {
37
- const hasAdjustments =
38
- !!shipping?.amount || !!tax?.amount || !!discount?.amount || (summaryLines && summaryLines.length > 0);
39
-
40
39
  const formattedShipping = useMemo(
41
- () => (shipping?.amount ? formatTokenAmount(safeBigInt(shipping.amount), tokenDecimals) : null),
42
- [shipping?.amount, tokenDecimals],
40
+ () => (!pricesLoading && shipping?.amount ? formatTokenAmount(safeBigInt(shipping.amount), tokenDecimals) : null),
41
+ [pricesLoading, shipping?.amount, tokenDecimals],
43
42
  );
44
43
 
45
44
  const formattedTax = useMemo(
46
- () => (tax?.amount ? formatTokenAmount(safeBigInt(tax.amount), tokenDecimals) : null),
47
- [tax?.amount, tokenDecimals],
45
+ () => (!pricesLoading && tax?.amount ? formatTokenAmount(safeBigInt(tax.amount), tokenDecimals) : null),
46
+ [pricesLoading, tax?.amount, tokenDecimals],
48
47
  );
49
48
 
50
49
  const formattedDiscount = useMemo(
51
- () => (discount?.amount ? formatTokenAmount(safeBigInt(discount.amount), tokenDecimals) : null),
52
- [discount?.amount, tokenDecimals],
50
+ () => (!pricesLoading && discount?.amount ? formatTokenAmount(safeBigInt(discount.amount), tokenDecimals) : null),
51
+ [pricesLoading, discount?.amount, tokenDecimals],
53
52
  );
54
53
 
55
54
  const formattedSummaryLines = useMemo(
56
55
  () =>
57
- summaryLines?.map(line => ({
58
- ...line,
59
- formattedAmount: formatTokenAmount(safeBigInt(line.amount), tokenDecimals),
60
- isNegative: safeBigInt(line.amount) < BigInt(0),
61
- })),
62
- [summaryLines, tokenDecimals],
56
+ pricesLoading
57
+ ? undefined
58
+ : summaryLines?.map(line => ({
59
+ ...line,
60
+ formattedAmount: formatTokenAmount(safeBigInt(line.amount), tokenDecimals),
61
+ isNegative: safeBigInt(line.amount) < BigInt(0),
62
+ })),
63
+ [pricesLoading, summaryLines, tokenDecimals],
63
64
  );
64
65
 
66
+ const hasAdjustments =
67
+ !!formattedShipping || !!formattedTax || !!formattedDiscount || !!formattedSummaryLines?.length;
68
+
65
69
  return (
66
70
  <div className={cn("border-t border-gray-200 pt-3 dark:border-neutral-700", classes?.cartSummary)}>
67
71
  {/* Subtotal — only shown when adjustments exist */}
@@ -147,7 +151,7 @@ export function CartSummary({
147
151
  {total} {tokenSymbol}
148
152
  </span>
149
153
  </div>
150
- {usdEquivalent && (
154
+ {!pricesLoading && usdEquivalent && (
151
155
  <div className="flex justify-end">
152
156
  <span className="text-xs text-gray-400 dark:text-gray-500">~{usdEquivalent} USD</span>
153
157
  </div>
@@ -7,12 +7,15 @@ import type { CheckoutItem, CheckoutSummaryLine, AnySpendCheckoutClasses } from
7
7
  import { CartItemRow } from "./CartItemRow";
8
8
  import { CartSummary } from "./CartSummary";
9
9
  import { PoweredByBranding } from "./PoweredByBranding";
10
+ import { PriceSkeleton } from "./PriceSkeleton";
10
11
 
11
12
  interface CheckoutCartPanelProps {
12
13
  items: CheckoutItem[];
13
14
  totalAmount: string;
14
15
  tokenSymbol?: string;
15
16
  tokenDecimals?: number;
17
+ /** True while token decimals/symbol are still loading — prevents rendering mis-decimalized prices. */
18
+ pricesLoading?: boolean;
16
19
  organizationName?: string;
17
20
  organizationLogo?: string;
18
21
  classes?: AnySpendCheckoutClasses;
@@ -31,6 +34,7 @@ export function CheckoutCartPanel({
31
34
  totalAmount,
32
35
  tokenSymbol = "",
33
36
  tokenDecimals = 18,
37
+ pricesLoading = false,
34
38
  organizationName,
35
39
  organizationLogo,
36
40
  classes,
@@ -56,7 +60,7 @@ export function CheckoutCartPanel({
56
60
  }, [items, tokenDecimals]);
57
61
 
58
62
  return (
59
- <div className={cn("anyspend-cart-panel flex flex-col", classes?.cartPanel)}>
63
+ <div className={cn("anyspend-cart-panel flex flex-col", classes?.cartPanel)} aria-busy={pricesLoading || undefined}>
60
64
  <h2
61
65
  className={cn(
62
66
  "anyspend-cart-title mb-4 text-lg font-semibold text-gray-900 dark:text-gray-100",
@@ -69,14 +73,18 @@ export function CheckoutCartPanel({
69
73
  <div className="anyspend-cart-items divide-y divide-gray-100 dark:divide-gray-800">
70
74
  {items.map((item, index) => {
71
75
  const itemTotal = safeBigInt(item.amount) * BigInt(item.quantity);
72
- const formattedPrice = `${formatTokenAmount(itemTotal, tokenDecimals)} ${tokenSymbol}`;
76
+ const formattedPrice: ReactNode = pricesLoading ? (
77
+ <PriceSkeleton />
78
+ ) : (
79
+ `${formatTokenAmount(itemTotal, tokenDecimals)} ${tokenSymbol}`
80
+ );
73
81
 
74
82
  return <CartItemRow key={item.id || index} item={item} formattedPrice={formattedPrice} classes={classes} />;
75
83
  })}
76
84
  </div>
77
85
 
78
86
  <CartSummary
79
- total={formattedTotal}
87
+ total={pricesLoading ? <PriceSkeleton className="w-20" /> : formattedTotal}
80
88
  tokenSymbol={tokenSymbol}
81
89
  classes={classes}
82
90
  subtotal={formattedSubtotal}
@@ -86,6 +94,7 @@ export function CheckoutCartPanel({
86
94
  discount={discount}
87
95
  summaryLines={summaryLines}
88
96
  usdEquivalent={usdEquivalent}
97
+ pricesLoading={pricesLoading}
89
98
  />
90
99
 
91
100
  {footer === undefined ? (
@@ -31,6 +31,8 @@ interface CheckoutFormPanelProps {
31
31
  /** Token info for display */
32
32
  tokenSymbol?: string;
33
33
  tokenDecimals?: number;
34
+ /** True while token decimals/symbol are still loading — prevents rendering mis-decimalized prices. */
35
+ pricesLoading?: boolean;
34
36
  /** CSS class overrides */
35
37
  classes?: AnySpendCheckoutClasses;
36
38
  /** Current form data (lifted state) */
@@ -62,6 +64,7 @@ export function CheckoutFormPanel({
62
64
  validateDiscount,
63
65
  tokenSymbol,
64
66
  tokenDecimals,
67
+ pricesLoading = false,
65
68
  classes,
66
69
  formData,
67
70
  onFormDataChange,
@@ -144,6 +147,7 @@ export function CheckoutFormPanel({
144
147
  onSelect={onShippingChange}
145
148
  tokenSymbol={tokenSymbol}
146
149
  tokenDecimals={tokenDecimals}
150
+ pricesLoading={pricesLoading}
147
151
  />
148
152
  )}
149
153
  {enableDiscountCode && validateDiscount && (
@@ -153,6 +157,7 @@ export function CheckoutFormPanel({
153
157
  onRemove={onDiscountRemoved}
154
158
  tokenSymbol={tokenSymbol}
155
159
  tokenDecimals={tokenDecimals}
160
+ pricesLoading={pricesLoading}
156
161
  />
157
162
  )}
158
163
  </>
@@ -5,6 +5,7 @@ import { formatTokenAmount, safeBigInt } from "@b3dotfun/sdk/shared/utils/number
5
5
  import { X, Loader2, Check } from "lucide-react";
6
6
  import { useState, useCallback } from "react";
7
7
  import type { DiscountResult } from "../../../types/forms";
8
+ import { PriceSkeleton } from "./PriceSkeleton";
8
9
 
9
10
  interface DiscountCodeInputProps {
10
11
  onApply: (code: string) => Promise<DiscountResult>;
@@ -13,6 +14,8 @@ interface DiscountCodeInputProps {
13
14
  loading?: boolean;
14
15
  tokenSymbol?: string;
15
16
  tokenDecimals?: number;
17
+ /** True while token decimals/symbol are still loading — renders applied amount as skeleton. */
18
+ pricesLoading?: boolean;
16
19
  className?: string;
17
20
  }
18
21
 
@@ -29,6 +32,7 @@ export function DiscountCodeInput({
29
32
  loading = false,
30
33
  tokenSymbol = "",
31
34
  tokenDecimals = 6,
35
+ pricesLoading = false,
32
36
  className,
33
37
  }: DiscountCodeInputProps) {
34
38
  const [code, setCode] = useState("");
@@ -67,11 +71,17 @@ export function DiscountCodeInput({
67
71
  <div className="flex items-center gap-2">
68
72
  <Check className="h-4 w-4 text-green-600 dark:text-green-400" />
69
73
  <span className="anyspend-discount-value text-sm font-medium text-green-700 dark:text-green-300">
70
- {appliedDiscount.discount_type === "percentage"
71
- ? `${appliedDiscount.discount_value}% off`
72
- : appliedDiscount.discount_amount
73
- ? `-${formatAmount(appliedDiscount.discount_amount, tokenDecimals, tokenSymbol)}`
74
- : "Discount applied"}
74
+ {appliedDiscount.discount_type === "percentage" ? (
75
+ `${appliedDiscount.discount_value}% off`
76
+ ) : appliedDiscount.discount_amount ? (
77
+ pricesLoading ? (
78
+ <PriceSkeleton />
79
+ ) : (
80
+ `-${formatAmount(appliedDiscount.discount_amount, tokenDecimals, tokenSymbol)}`
81
+ )
82
+ ) : (
83
+ "Discount applied"
84
+ )}
75
85
  </span>
76
86
  </div>
77
87
  <button
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ import { cn } from "@b3dotfun/sdk/shared/utils/cn";
4
+
5
+ interface PriceSkeletonProps {
6
+ className?: string;
7
+ }
8
+
9
+ export function PriceSkeleton({ className }: PriceSkeletonProps) {
10
+ return (
11
+ <span
12
+ className={cn(
13
+ "animate-pulse-fade bg-b3-react-background inline-block h-4 w-16 rounded-md align-middle",
14
+ className,
15
+ )}
16
+ aria-hidden="true"
17
+ />
18
+ );
19
+ }
@@ -3,6 +3,7 @@
3
3
  import { cn } from "@b3dotfun/sdk/shared/utils/cn";
4
4
  import { formatTokenAmount, safeBigInt } from "@b3dotfun/sdk/shared/utils/number";
5
5
  import type { ShippingOption } from "../../../types/forms";
6
+ import { PriceSkeleton } from "./PriceSkeleton";
6
7
 
7
8
  interface ShippingSelectorProps {
8
9
  options: ShippingOption[];
@@ -10,6 +11,8 @@ interface ShippingSelectorProps {
10
11
  onSelect: (option: ShippingOption) => void;
11
12
  tokenSymbol?: string;
12
13
  tokenDecimals?: number;
14
+ /** True while token decimals/symbol are still loading — renders price as skeleton. */
15
+ pricesLoading?: boolean;
13
16
  className?: string;
14
17
  }
15
18
 
@@ -25,6 +28,7 @@ export function ShippingSelector({
25
28
  onSelect,
26
29
  tokenSymbol = "",
27
30
  tokenDecimals = 6,
31
+ pricesLoading = false,
28
32
  className,
29
33
  }: ShippingSelectorProps) {
30
34
  if (options.length === 0) return null;
@@ -63,7 +67,7 @@ export function ShippingSelector({
63
67
  </div>
64
68
  </div>
65
69
  <div className="anyspend-shipping-option-price text-sm font-medium text-gray-900 dark:text-gray-100">
66
- {formatAmount(option.amount, tokenDecimals, tokenSymbol)}
70
+ {pricesLoading ? <PriceSkeleton /> : formatAmount(option.amount, tokenDecimals, tokenSymbol)}
67
71
  </div>
68
72
  </label>
69
73
  ))}