@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.
- package/dist/cjs/anyspend/react/components/checkout/AnySpendCheckout.js +6 -5
- package/dist/cjs/anyspend/react/components/checkout/CartItemRow.d.ts +2 -1
- package/dist/cjs/anyspend/react/components/checkout/CartSummary.d.ts +6 -4
- package/dist/cjs/anyspend/react/components/checkout/CartSummary.js +13 -11
- package/dist/cjs/anyspend/react/components/checkout/CheckoutCartPanel.d.ts +3 -1
- package/dist/cjs/anyspend/react/components/checkout/CheckoutCartPanel.js +5 -4
- package/dist/cjs/anyspend/react/components/checkout/CheckoutFormPanel.d.ts +3 -1
- package/dist/cjs/anyspend/react/components/checkout/CheckoutFormPanel.js +2 -2
- package/dist/cjs/anyspend/react/components/checkout/DiscountCodeInput.d.ts +3 -1
- package/dist/cjs/anyspend/react/components/checkout/DiscountCodeInput.js +3 -6
- package/dist/cjs/anyspend/react/components/checkout/PriceSkeleton.d.ts +5 -0
- package/dist/cjs/anyspend/react/components/checkout/PriceSkeleton.js +9 -0
- package/dist/cjs/anyspend/react/components/checkout/ShippingSelector.d.ts +3 -1
- package/dist/cjs/anyspend/react/components/checkout/ShippingSelector.js +3 -2
- package/dist/esm/anyspend/react/components/checkout/AnySpendCheckout.js +6 -5
- package/dist/esm/anyspend/react/components/checkout/CartItemRow.d.ts +2 -1
- package/dist/esm/anyspend/react/components/checkout/CartSummary.d.ts +6 -4
- package/dist/esm/anyspend/react/components/checkout/CartSummary.js +13 -11
- package/dist/esm/anyspend/react/components/checkout/CheckoutCartPanel.d.ts +3 -1
- package/dist/esm/anyspend/react/components/checkout/CheckoutCartPanel.js +5 -4
- package/dist/esm/anyspend/react/components/checkout/CheckoutFormPanel.d.ts +3 -1
- package/dist/esm/anyspend/react/components/checkout/CheckoutFormPanel.js +2 -2
- package/dist/esm/anyspend/react/components/checkout/DiscountCodeInput.d.ts +3 -1
- package/dist/esm/anyspend/react/components/checkout/DiscountCodeInput.js +3 -6
- package/dist/esm/anyspend/react/components/checkout/PriceSkeleton.d.ts +5 -0
- package/dist/esm/anyspend/react/components/checkout/PriceSkeleton.js +6 -0
- package/dist/esm/anyspend/react/components/checkout/ShippingSelector.d.ts +3 -1
- package/dist/esm/anyspend/react/components/checkout/ShippingSelector.js +3 -2
- package/dist/types/anyspend/react/components/checkout/CartItemRow.d.ts +2 -1
- package/dist/types/anyspend/react/components/checkout/CartSummary.d.ts +6 -4
- package/dist/types/anyspend/react/components/checkout/CheckoutCartPanel.d.ts +3 -1
- package/dist/types/anyspend/react/components/checkout/CheckoutFormPanel.d.ts +3 -1
- package/dist/types/anyspend/react/components/checkout/DiscountCodeInput.d.ts +3 -1
- package/dist/types/anyspend/react/components/checkout/PriceSkeleton.d.ts +5 -0
- package/dist/types/anyspend/react/components/checkout/ShippingSelector.d.ts +3 -1
- package/package.json +1 -1
- package/src/anyspend/react/components/checkout/AnySpendCheckout.tsx +10 -4
- package/src/anyspend/react/components/checkout/CartItemRow.tsx +2 -1
- package/src/anyspend/react/components/checkout/CartSummary.tsx +24 -20
- package/src/anyspend/react/components/checkout/CheckoutCartPanel.tsx +12 -3
- package/src/anyspend/react/components/checkout/CheckoutFormPanel.tsx +5 -0
- package/src/anyspend/react/components/checkout/DiscountCodeInput.tsx +15 -5
- package/src/anyspend/react/components/checkout/PriceSkeleton.tsx +19 -0
- 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
|
-
|
|
127
|
-
const
|
|
128
|
-
const
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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,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
|
-
|
|
124
|
-
const
|
|
125
|
-
const
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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,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:
|
|
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
|
|
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:
|
|
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 {};
|
|
@@ -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
|
@@ -295,10 +295,14 @@ export function AnySpendCheckout({
|
|
|
295
295
|
variablePricingAmount,
|
|
296
296
|
]);
|
|
297
297
|
|
|
298
|
-
// Get destination token metadata
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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 =
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
))}
|