@b3dotfun/sdk 0.1.68-alpha.1 → 0.1.68-alpha.3

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 (22) hide show
  1. package/dist/cjs/anyspend/react/components/checkout/AnySpendCheckout.d.ts +10 -6
  2. package/dist/cjs/anyspend/react/components/checkout/AnySpendCheckout.js +55 -8
  3. package/dist/cjs/anyspend/react/components/checkout/VariablePricingInput.d.ts +17 -0
  4. package/dist/cjs/anyspend/react/components/checkout/VariablePricingInput.js +145 -0
  5. package/dist/cjs/anyspend/react/components/index.d.ts +1 -1
  6. package/dist/cjs/global-account/react/hooks/useAuthentication.js +9 -13
  7. package/dist/esm/anyspend/react/components/checkout/AnySpendCheckout.d.ts +10 -6
  8. package/dist/esm/anyspend/react/components/checkout/AnySpendCheckout.js +55 -8
  9. package/dist/esm/anyspend/react/components/checkout/VariablePricingInput.d.ts +17 -0
  10. package/dist/esm/anyspend/react/components/checkout/VariablePricingInput.js +142 -0
  11. package/dist/esm/anyspend/react/components/index.d.ts +1 -1
  12. package/dist/esm/global-account/react/hooks/useAuthentication.js +9 -13
  13. package/dist/styles/index.css +1 -1
  14. package/dist/types/anyspend/react/components/checkout/AnySpendCheckout.d.ts +10 -6
  15. package/dist/types/anyspend/react/components/checkout/VariablePricingInput.d.ts +17 -0
  16. package/dist/types/anyspend/react/components/index.d.ts +1 -1
  17. package/package.json +1 -1
  18. package/src/anyspend/docs/checkout-sessions.md +20 -3
  19. package/src/anyspend/react/components/checkout/AnySpendCheckout.tsx +73 -18
  20. package/src/anyspend/react/components/checkout/VariablePricingInput.tsx +247 -0
  21. package/src/anyspend/react/components/index.ts +1 -0
  22. package/src/global-account/react/hooks/useAuthentication.ts +9 -14
@@ -1,10 +1,12 @@
1
1
  import { type ReactNode } from "react";
2
- import { type PaymentMethod } from "./CheckoutPaymentPanel";
3
- export type { AnySpendCheckoutClasses } from "../types/classes";
2
+ import type { AddressData, CheckoutFormComponentProps, CheckoutFormSchema, DiscountResult, ShippingOption } from "../../../types/forms";
4
3
  import type { AnySpendCheckoutClasses } from "../types/classes";
5
4
  import type { AnySpendContent, AnySpendSlots, AnySpendTheme } from "../types/customization";
6
- import type { CheckoutFormSchema, CheckoutFormComponentProps, ShippingOption, DiscountResult, AddressData } from "../../../types/forms";
7
- export type { CheckoutFormSchema, CheckoutFormComponentProps, ShippingOption, DiscountResult, AddressData };
5
+ import { type PaymentMethod } from "./CheckoutPaymentPanel";
6
+ import { type VariablePricingConfig } from "./VariablePricingInput";
7
+ export type { AnySpendCheckoutClasses } from "../types/classes";
8
+ export type { VariablePricingConfig } from "./VariablePricingInput";
9
+ export type { AddressData, CheckoutFormComponentProps, CheckoutFormSchema, DiscountResult, ShippingOption };
8
10
  export interface CheckoutItem {
9
11
  id?: string;
10
12
  name: string;
@@ -33,7 +35,7 @@ export interface AnySpendCheckoutProps {
33
35
  destinationTokenAddress: string;
34
36
  /** The destination chain ID */
35
37
  destinationTokenChainId: number;
36
- /** Line items */
38
+ /** Line items. When `variablePricing.enabled` is true, pass a single placeholder item (e.g. amount "0") — the user-entered amount overrides the total. */
37
39
  items: CheckoutItem[];
38
40
  /** Override computed total */
39
41
  totalAmount?: string;
@@ -113,7 +115,9 @@ export interface AnySpendCheckoutProps {
113
115
  onDiscountApplied?: (result: DiscountResult) => void;
114
116
  /** Async function to validate a discount code. Returns DiscountResult. */
115
117
  validateDiscount?: (code: string) => Promise<DiscountResult>;
118
+ /** Variable pricing / name your price config. When enabled, user enters amount before payment. */
119
+ variablePricing?: VariablePricingConfig;
116
120
  /** When true, fees are added on top of the amount (payer pays more, receiver gets exact amount) */
117
121
  feeOnTop?: boolean;
118
122
  }
119
- export declare function AnySpendCheckout({ mode, recipientAddress, destinationTokenAddress, destinationTokenChainId, items, totalAmount: totalAmountOverride, organizationName, organizationLogo, themeColor, buttonText, checkoutSessionId, onSuccess, onError, returnUrl, returnLabel, classes, footer, defaultPaymentMethod, senderAddress, slots, content, theme, showPoints, showOrderId, shipping: shippingProp, tax, discount: discountProp, summaryLines, formSchema, formComponent, onFormSubmit, shippingOptions, collectShippingAddress, onShippingChange: onShippingChangeProp, enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount, feeOnTop, }: AnySpendCheckoutProps): import("react/jsx-runtime").JSX.Element;
123
+ export declare function AnySpendCheckout({ mode, recipientAddress, destinationTokenAddress, destinationTokenChainId, items, totalAmount: totalAmountOverride, organizationName, organizationLogo, themeColor, buttonText, checkoutSessionId, onSuccess, onError, returnUrl, returnLabel, classes, footer, defaultPaymentMethod, senderAddress, slots, content, theme, showPoints, showOrderId, shipping: shippingProp, tax, discount: discountProp, summaryLines, formSchema, formComponent, onFormSubmit, shippingOptions, collectShippingAddress, onShippingChange: onShippingChangeProp, enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount, variablePricing, feeOnTop, }: AnySpendCheckoutProps): import("react/jsx-runtime").JSX.Element;
@@ -3,17 +3,18 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.AnySpendCheckout = AnySpendCheckout;
5
5
  const jsx_runtime_1 = require("react/jsx-runtime");
6
- const react_1 = require("../../../../global-account/react");
7
6
  const constants_1 = require("../../../../anyspend/constants");
7
+ const react_1 = require("../../../../global-account/react");
8
8
  const number_1 = require("../../../../shared/utils/number");
9
9
  const react_2 = require("react");
10
10
  const useAnyspendQuote_1 = require("../../hooks/useAnyspendQuote");
11
11
  const AnySpendFingerprintWrapper_1 = require("../AnySpendFingerprintWrapper");
12
+ const AnySpendCustomizationContext_1 = require("../context/AnySpendCustomizationContext");
12
13
  const CheckoutCartPanel_1 = require("./CheckoutCartPanel");
13
14
  const CheckoutFormPanel_1 = require("./CheckoutFormPanel");
14
15
  const CheckoutLayout_1 = require("./CheckoutLayout");
15
16
  const CheckoutPaymentPanel_1 = require("./CheckoutPaymentPanel");
16
- const AnySpendCustomizationContext_1 = require("../context/AnySpendCustomizationContext");
17
+ const VariablePricingInput_1 = require("./VariablePricingInput");
17
18
  const emptyAddress = { street: "", city: "", state: "", zip: "", country: "" };
18
19
  function AnySpendCheckout({ mode = "page", recipientAddress, destinationTokenAddress, destinationTokenChainId, items, totalAmount: totalAmountOverride, organizationName, organizationLogo, themeColor, buttonText = "Pay", checkoutSessionId, onSuccess, onError, returnUrl, returnLabel, classes, footer, defaultPaymentMethod, senderAddress, slots, content, theme, showPoints, showOrderId, shipping: shippingProp, tax, discount: discountProp, summaryLines,
19
20
  // New form props
@@ -21,7 +22,13 @@ formSchema, formComponent, onFormSubmit,
21
22
  // New shipping props
22
23
  shippingOptions, collectShippingAddress, onShippingChange: onShippingChangeProp,
23
24
  // New discount props
24
- enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount, feeOnTop, }) {
25
+ enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount,
26
+ // Variable pricing
27
+ variablePricing, feeOnTop, }) {
28
+ // ===== Variable pricing state =====
29
+ const [variablePricingAmount, setVariablePricingAmount] = (0, react_2.useState)("0");
30
+ const isVariablePricingActive = variablePricing?.enabled === true;
31
+ const isVariablePricingValid = isVariablePricingActive ? variablePricingAmount !== "0" : true;
25
32
  // ===== Form state =====
26
33
  const [formData, setFormData] = (0, react_2.useState)({});
27
34
  const [selectedShipping, setSelectedShipping] = (0, react_2.useState)(null);
@@ -66,7 +73,25 @@ enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount,
66
73
  return undefined;
67
74
  }, [appliedDiscount, discountProp]);
68
75
  // Compute total from items + adjustments (including dynamic shipping/discount)
76
+ // Variable pricing overrides the total when active
69
77
  const computedTotal = (0, react_2.useMemo)(() => {
78
+ if (isVariablePricingActive && variablePricingAmount !== "0") {
79
+ let total = (0, number_1.safeBigInt)(variablePricingAmount);
80
+ if (effectiveShipping?.amount)
81
+ total += (0, number_1.safeBigInt)(effectiveShipping.amount);
82
+ const taxAmt = typeof tax === "string" ? tax : tax?.amount;
83
+ if (taxAmt)
84
+ total += (0, number_1.safeBigInt)(taxAmt);
85
+ if (effectiveDiscount?.amount)
86
+ total -= (0, number_1.safeBigInt)(effectiveDiscount.amount);
87
+ if (summaryLines) {
88
+ for (const line of summaryLines)
89
+ total += (0, number_1.safeBigInt)(line.amount);
90
+ }
91
+ if (total < BigInt(0))
92
+ total = BigInt(0);
93
+ return total.toString();
94
+ }
70
95
  if (totalAmountOverride)
71
96
  return totalAmountOverride;
72
97
  let total = BigInt(0);
@@ -87,7 +112,16 @@ enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount,
87
112
  if (total < BigInt(0))
88
113
  total = BigInt(0);
89
114
  return total.toString();
90
- }, [items, totalAmountOverride, effectiveShipping, tax, effectiveDiscount, summaryLines]);
115
+ }, [
116
+ items,
117
+ totalAmountOverride,
118
+ effectiveShipping,
119
+ tax,
120
+ effectiveDiscount,
121
+ summaryLines,
122
+ isVariablePricingActive,
123
+ variablePricingAmount,
124
+ ]);
91
125
  // Get destination token metadata
92
126
  const { data: tokenData } = (0, react_1.useTokenData)(destinationTokenChainId, destinationTokenAddress);
93
127
  const tokenSymbol = tokenData?.symbol || "";
@@ -139,14 +173,27 @@ enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount,
139
173
  meta.customerName = formData.name;
140
174
  if (checkoutSessionId)
141
175
  meta.checkoutSessionId = checkoutSessionId;
176
+ if (isVariablePricingActive && variablePricingAmount !== "0") {
177
+ meta.variablePricingAmount = variablePricingAmount;
178
+ }
142
179
  return Object.keys(meta).length > 0 ? meta : undefined;
143
- }, [formData, selectedShipping, shippingAddress, appliedDiscount, checkoutSessionId]);
144
- // Check if required form fields are filled
180
+ }, [
181
+ formData,
182
+ selectedShipping,
183
+ shippingAddress,
184
+ appliedDiscount,
185
+ checkoutSessionId,
186
+ isVariablePricingActive,
187
+ variablePricingAmount,
188
+ ]);
189
+ // Check if required form fields are filled and variable pricing is valid
145
190
  const isFormValid = (0, react_2.useMemo)(() => {
191
+ if (!isVariablePricingValid)
192
+ return false;
146
193
  if (!formSchema)
147
194
  return true;
148
195
  return formSchema.fields.filter(f => f.required).every(f => formData[f.id] != null && formData[f.id] !== "");
149
- }, [formSchema, formData]);
196
+ }, [formSchema, formData, isVariablePricingValid]);
150
197
  // Check if we have a form panel to show
151
198
  const hasFormContent = (formSchema && formSchema.fields.length > 0) ||
152
199
  formComponent ||
@@ -154,5 +201,5 @@ enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount,
154
201
  (shippingOptions && shippingOptions.length > 0) ||
155
202
  collectShippingAddress ||
156
203
  enableDiscountCode;
157
- return ((0, jsx_runtime_1.jsx)(AnySpendFingerprintWrapper_1.AnySpendFingerprintWrapper, { fingerprint: fingerprint, children: (0, jsx_runtime_1.jsx)(AnySpendCustomizationContext_1.AnySpendCustomizationProvider, { slots: slots, content: content, theme: theme, children: (0, jsx_runtime_1.jsx)(CheckoutLayout_1.CheckoutLayout, { mode: mode, paymentPanel: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [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 })] }), 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 }) }) }));
204
+ return ((0, jsx_runtime_1.jsx)(AnySpendFingerprintWrapper_1.AnySpendFingerprintWrapper, { fingerprint: fingerprint, children: (0, jsx_runtime_1.jsx)(AnySpendCustomizationContext_1.AnySpendCustomizationProvider, { slots: slots, content: content, 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 })] }), 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 }) }) }));
158
205
  }
@@ -0,0 +1,17 @@
1
+ export interface VariablePricingConfig {
2
+ enabled: boolean;
3
+ minAmount?: string;
4
+ maxAmount?: string;
5
+ suggestedAmount?: string;
6
+ label?: string;
7
+ currency?: string;
8
+ }
9
+ interface VariablePricingInputProps {
10
+ config: VariablePricingConfig;
11
+ tokenDecimals: number;
12
+ tokenSymbol: string;
13
+ themeColor?: string;
14
+ onChange: (amountWei: string) => void;
15
+ }
16
+ export declare function VariablePricingInput({ config, tokenDecimals, tokenSymbol, themeColor, onChange, }: VariablePricingInputProps): import("react/jsx-runtime").JSX.Element;
17
+ export {};
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ "use client";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.VariablePricingInput = VariablePricingInput;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const cn_1 = require("../../../../shared/utils/cn");
7
+ const number_1 = require("../../../../shared/utils/number");
8
+ const react_1 = require("motion/react");
9
+ const react_2 = require("react");
10
+ function VariablePricingInput({ config, tokenDecimals, tokenSymbol, themeColor, onChange, }) {
11
+ const currency = config.currency || tokenSymbol;
12
+ // Convert suggested amount from wei to display
13
+ const initialValue = (0, react_2.useMemo)(() => {
14
+ if (config.suggestedAmount) {
15
+ try {
16
+ return (0, number_1.formatUnits)(config.suggestedAmount, tokenDecimals);
17
+ }
18
+ catch {
19
+ return "";
20
+ }
21
+ }
22
+ return "";
23
+ }, [config.suggestedAmount, tokenDecimals]);
24
+ const [displayValue, setDisplayValue] = (0, react_2.useState)(initialValue);
25
+ const [error, setError] = (0, react_2.useState)(null);
26
+ // Min/max in display units
27
+ const minDisplay = (0, react_2.useMemo)(() => {
28
+ if (!config.minAmount)
29
+ return null;
30
+ try {
31
+ return parseFloat((0, number_1.formatUnits)(config.minAmount, tokenDecimals));
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }, [config.minAmount, tokenDecimals]);
37
+ const maxDisplay = (0, react_2.useMemo)(() => {
38
+ if (!config.maxAmount)
39
+ return null;
40
+ try {
41
+ return parseFloat((0, number_1.formatUnits)(config.maxAmount, tokenDecimals));
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }, [config.maxAmount, tokenDecimals]);
47
+ // Preset amounts
48
+ const presetAmounts = (0, react_2.useMemo)(() => {
49
+ const presets = [];
50
+ if (config.suggestedAmount) {
51
+ try {
52
+ const suggested = parseFloat((0, number_1.formatUnits)(config.suggestedAmount, tokenDecimals));
53
+ const candidates = [suggested / 2, suggested, suggested * 2];
54
+ for (const val of candidates) {
55
+ if (val <= 0)
56
+ continue;
57
+ if (minDisplay !== null && val < minDisplay)
58
+ continue;
59
+ if (maxDisplay !== null && val > maxDisplay)
60
+ continue;
61
+ const display = val % 1 === 0 ? val.toString() : val.toFixed(2);
62
+ presets.push({ label: `${display}`, value: display });
63
+ }
64
+ }
65
+ catch {
66
+ // skip presets
67
+ }
68
+ }
69
+ return presets;
70
+ }, [config.suggestedAmount, tokenDecimals, minDisplay, maxDisplay]);
71
+ const validate = (0, react_2.useCallback)((value) => {
72
+ const parsed = parseFloat(value);
73
+ if (!value || isNaN(parsed))
74
+ return "Please enter a valid number";
75
+ if (parsed <= 0)
76
+ return "Please enter an amount";
77
+ if (minDisplay !== null && parsed < minDisplay) {
78
+ const display = minDisplay % 1 === 0 ? minDisplay.toString() : minDisplay.toFixed(2);
79
+ return `Minimum amount is ${display} ${currency}`;
80
+ }
81
+ if (maxDisplay !== null && parsed > maxDisplay) {
82
+ const display = maxDisplay % 1 === 0 ? maxDisplay.toString() : maxDisplay.toFixed(2);
83
+ return `Maximum amount is ${display} ${currency}`;
84
+ }
85
+ return null;
86
+ }, [minDisplay, maxDisplay, currency]);
87
+ const convertToWei = (0, react_2.useCallback)((value) => {
88
+ try {
89
+ const [whole, frac = ""] = value.split(".");
90
+ const paddedFrac = frac.padEnd(tokenDecimals, "0").slice(0, tokenDecimals);
91
+ const factor = BigInt(10) ** BigInt(tokenDecimals);
92
+ return (BigInt(whole || "0") * factor + BigInt(paddedFrac)).toString();
93
+ }
94
+ catch {
95
+ return "0";
96
+ }
97
+ }, [tokenDecimals]);
98
+ const handleChange = (0, react_2.useCallback)((value) => {
99
+ setDisplayValue(value);
100
+ const validationError = validate(value);
101
+ setError(validationError);
102
+ if (!validationError && value && parseFloat(value) > 0) {
103
+ onChange(convertToWei(value));
104
+ }
105
+ else {
106
+ onChange("0");
107
+ }
108
+ }, [validate, convertToWei, onChange]);
109
+ const handlePresetClick = (0, react_2.useCallback)((value) => {
110
+ setDisplayValue(value);
111
+ setError(null);
112
+ onChange(convertToWei(value));
113
+ }, [convertToWei, onChange]);
114
+ // Notify parent with initial value on mount
115
+ (0, react_2.useEffect)(() => {
116
+ if (initialValue && !validate(initialValue)) {
117
+ onChange(convertToWei(initialValue));
118
+ }
119
+ // eslint-disable-next-line react-hooks/exhaustive-deps
120
+ }, []);
121
+ const formatHint = () => {
122
+ if (minDisplay !== null && maxDisplay !== null) {
123
+ const minStr = minDisplay % 1 === 0 ? minDisplay.toString() : minDisplay.toFixed(2);
124
+ const maxStr = maxDisplay % 1 === 0 ? maxDisplay.toString() : maxDisplay.toFixed(2);
125
+ return `${minStr} – ${maxStr} ${currency}`;
126
+ }
127
+ if (minDisplay !== null) {
128
+ const minStr = minDisplay % 1 === 0 ? minDisplay.toString() : minDisplay.toFixed(2);
129
+ return `Min: ${minStr} ${currency}`;
130
+ }
131
+ if (maxDisplay !== null) {
132
+ const maxStr = maxDisplay % 1 === 0 ? maxDisplay.toString() : maxDisplay.toFixed(2);
133
+ return `Max: ${maxStr} ${currency}`;
134
+ }
135
+ return null;
136
+ };
137
+ const hint = formatHint();
138
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "anyspend-variable-pricing mb-6", children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "variable-pricing-amount", className: "mb-3 block text-lg font-semibold text-gray-900 dark:text-gray-100", children: config.label || "Enter amount" }), presetAmounts.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "mb-3 flex flex-wrap gap-2", children: presetAmounts.map(preset => ((0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => handlePresetClick(preset.value), className: (0, cn_1.cn)("rounded-full border px-4 py-2 text-sm font-medium transition-colors", displayValue === preset.value
139
+ ? "border-transparent text-white"
140
+ : "border-gray-200 text-gray-700 hover:bg-gray-50 dark:border-neutral-600 dark:text-gray-300 dark:hover:bg-neutral-800"), style: displayValue === preset.value ? { backgroundColor: themeColor || "hsl(var(--as-brand))" } : undefined, children: [preset.label, " ", currency] }, preset.value))) })), (0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)("input", { id: "variable-pricing-amount", type: "text", inputMode: "decimal", value: displayValue, onChange: e => {
141
+ // Normalize comma decimal separators for locale compatibility
142
+ const normalized = e.target.value.replace(",", ".");
143
+ handleChange(normalized);
144
+ }, className: "w-full rounded-xl border border-gray-200 bg-gray-50 px-4 py-3.5 pr-16 text-lg font-semibold text-gray-900 placeholder:text-gray-300 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-neutral-600 dark:bg-neutral-800 dark:text-gray-100 dark:placeholder:text-neutral-600 dark:focus:border-blue-400", placeholder: "0.00" }), (0, jsx_runtime_1.jsx)("span", { className: "absolute right-4 top-1/2 -translate-y-1/2 text-sm font-medium text-gray-400 dark:text-gray-500", children: currency })] }), hint && (0, jsx_runtime_1.jsx)("p", { className: "mt-1.5 text-xs text-gray-500 dark:text-gray-400", children: hint }), (0, jsx_runtime_1.jsx)(react_1.AnimatePresence, { initial: false, children: error && ((0, jsx_runtime_1.jsx)(react_1.motion.p, { initial: { opacity: 0, height: 0 }, animate: { opacity: 1, height: "auto" }, exit: { opacity: 0, height: 0 }, transition: { duration: 0.15, ease: "easeOut" }, className: "mt-1.5 text-sm text-red-500", children: error }, "variable-price-error")) })] }));
145
+ }
@@ -1,5 +1,5 @@
1
1
  export { AnySpendCheckout } from "./checkout/AnySpendCheckout";
2
- export type { AnySpendCheckoutProps, CheckoutItem, CheckoutSummaryLine, AnySpendCheckoutClasses, CheckoutFormSchema, CheckoutFormComponentProps, ShippingOption, DiscountResult, AddressData, } from "./checkout/AnySpendCheckout";
2
+ export type { AnySpendCheckoutProps, CheckoutItem, CheckoutSummaryLine, AnySpendCheckoutClasses, CheckoutFormSchema, CheckoutFormComponentProps, ShippingOption, DiscountResult, AddressData, VariablePricingConfig, } from "./checkout/AnySpendCheckout";
3
3
  export { AnySpendCheckoutTrigger } from "./checkout/AnySpendCheckoutTrigger";
4
4
  export type { AnySpendCheckoutTriggerProps } from "./checkout/AnySpendCheckoutTrigger";
5
5
  export type { PaymentMethod } from "./checkout/CheckoutPaymentPanel";
@@ -132,21 +132,17 @@ function useAuthentication(partnerId) {
132
132
  }
133
133
  }, [activeWallet, partnerId, authenticate, setIsAuthenticated, setIsAuthenticating, setUser, setHasStartedConnecting]);
134
134
  const logout = (0, react_2.useCallback)(async (callback) => {
135
- if (activeWallet) {
136
- debug("@@logout:activeWallet", activeWallet);
137
- disconnect(activeWallet);
138
- debug("@@logout:activeWallet", activeWallet);
139
- }
140
- // Log out of each wallet
135
+ // Only disconnect ecosystem/smart wallets, preserve EOA wallets (e.g. MetaMask)
136
+ // so they remain available after re-login
141
137
  wallets.forEach(wallet => {
142
- console.log("@@logging out", wallet);
143
- disconnect(wallet);
138
+ debug("@@logout:wallet", wallet.id);
139
+ if (wallet.id.startsWith("ecosystem.") || wallet.id === "smart") {
140
+ disconnect(wallet);
141
+ }
144
142
  });
145
- // Delete localStorage thirdweb:connected-wallet-ids
146
- // https://npc-labs.slack.com/archives/C070E6HNG85/p1750185115273099
143
+ // Clear user-specific storage but preserve wallet connection state
144
+ // so EOA wallets (e.g. MetaMask) can auto-reconnect on next login
147
145
  if (typeof localStorage !== "undefined") {
148
- localStorage.removeItem("thirdweb:connected-wallet-ids");
149
- localStorage.removeItem("wagmi.store");
150
146
  localStorage.removeItem("lastAuthProvider");
151
147
  localStorage.removeItem("b3-user");
152
148
  }
@@ -159,7 +155,7 @@ function useAuthentication(partnerId) {
159
155
  if (onLogoutCallback) {
160
156
  await onLogoutCallback();
161
157
  }
162
- }, [activeWallet, disconnect, wallets, setIsAuthenticated, setUser, setIsConnected, onLogoutCallback]);
158
+ }, [disconnect, wallets, setIsAuthenticated, setUser, setIsConnected, onLogoutCallback]);
163
159
  const onConnect = (0, react_2.useCallback)(async (_walleAutoConnectedWith, allConnectedWallets) => {
164
160
  debug("@@useAuthentication:onConnect", { _walleAutoConnectedWith, allConnectedWallets });
165
161
  try {
@@ -1,10 +1,12 @@
1
1
  import { type ReactNode } from "react";
2
- import { type PaymentMethod } from "./CheckoutPaymentPanel";
3
- export type { AnySpendCheckoutClasses } from "../types/classes";
2
+ import type { AddressData, CheckoutFormComponentProps, CheckoutFormSchema, DiscountResult, ShippingOption } from "../../../types/forms";
4
3
  import type { AnySpendCheckoutClasses } from "../types/classes";
5
4
  import type { AnySpendContent, AnySpendSlots, AnySpendTheme } from "../types/customization";
6
- import type { CheckoutFormSchema, CheckoutFormComponentProps, ShippingOption, DiscountResult, AddressData } from "../../../types/forms";
7
- export type { CheckoutFormSchema, CheckoutFormComponentProps, ShippingOption, DiscountResult, AddressData };
5
+ import { type PaymentMethod } from "./CheckoutPaymentPanel";
6
+ import { type VariablePricingConfig } from "./VariablePricingInput";
7
+ export type { AnySpendCheckoutClasses } from "../types/classes";
8
+ export type { VariablePricingConfig } from "./VariablePricingInput";
9
+ export type { AddressData, CheckoutFormComponentProps, CheckoutFormSchema, DiscountResult, ShippingOption };
8
10
  export interface CheckoutItem {
9
11
  id?: string;
10
12
  name: string;
@@ -33,7 +35,7 @@ export interface AnySpendCheckoutProps {
33
35
  destinationTokenAddress: string;
34
36
  /** The destination chain ID */
35
37
  destinationTokenChainId: number;
36
- /** Line items */
38
+ /** Line items. When `variablePricing.enabled` is true, pass a single placeholder item (e.g. amount "0") — the user-entered amount overrides the total. */
37
39
  items: CheckoutItem[];
38
40
  /** Override computed total */
39
41
  totalAmount?: string;
@@ -113,7 +115,9 @@ export interface AnySpendCheckoutProps {
113
115
  onDiscountApplied?: (result: DiscountResult) => void;
114
116
  /** Async function to validate a discount code. Returns DiscountResult. */
115
117
  validateDiscount?: (code: string) => Promise<DiscountResult>;
118
+ /** Variable pricing / name your price config. When enabled, user enters amount before payment. */
119
+ variablePricing?: VariablePricingConfig;
116
120
  /** When true, fees are added on top of the amount (payer pays more, receiver gets exact amount) */
117
121
  feeOnTop?: boolean;
118
122
  }
119
- export declare function AnySpendCheckout({ mode, recipientAddress, destinationTokenAddress, destinationTokenChainId, items, totalAmount: totalAmountOverride, organizationName, organizationLogo, themeColor, buttonText, checkoutSessionId, onSuccess, onError, returnUrl, returnLabel, classes, footer, defaultPaymentMethod, senderAddress, slots, content, theme, showPoints, showOrderId, shipping: shippingProp, tax, discount: discountProp, summaryLines, formSchema, formComponent, onFormSubmit, shippingOptions, collectShippingAddress, onShippingChange: onShippingChangeProp, enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount, feeOnTop, }: AnySpendCheckoutProps): import("react/jsx-runtime").JSX.Element;
123
+ export declare function AnySpendCheckout({ mode, recipientAddress, destinationTokenAddress, destinationTokenChainId, items, totalAmount: totalAmountOverride, organizationName, organizationLogo, themeColor, buttonText, checkoutSessionId, onSuccess, onError, returnUrl, returnLabel, classes, footer, defaultPaymentMethod, senderAddress, slots, content, theme, showPoints, showOrderId, shipping: shippingProp, tax, discount: discountProp, summaryLines, formSchema, formComponent, onFormSubmit, shippingOptions, collectShippingAddress, onShippingChange: onShippingChangeProp, enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount, variablePricing, feeOnTop, }: AnySpendCheckoutProps): import("react/jsx-runtime").JSX.Element;
@@ -1,16 +1,17 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useTokenData } from "../../../../global-account/react/index.js";
4
3
  import { USDC_BASE } from "../../../../anyspend/constants/index.js";
4
+ import { useTokenData } from "../../../../global-account/react/index.js";
5
5
  import { formatUnits, safeBigInt } from "../../../../shared/utils/number.js";
6
6
  import { useCallback, useMemo, useState } from "react";
7
7
  import { useAnyspendQuote } from "../../hooks/useAnyspendQuote.js";
8
8
  import { AnySpendFingerprintWrapper, getFingerprintConfig } from "../AnySpendFingerprintWrapper.js";
9
+ import { AnySpendCustomizationProvider } from "../context/AnySpendCustomizationContext.js";
9
10
  import { CheckoutCartPanel } from "./CheckoutCartPanel.js";
10
11
  import { CheckoutFormPanel } from "./CheckoutFormPanel.js";
11
12
  import { CheckoutLayout } from "./CheckoutLayout.js";
12
13
  import { CheckoutPaymentPanel } from "./CheckoutPaymentPanel.js";
13
- import { AnySpendCustomizationProvider } from "../context/AnySpendCustomizationContext.js";
14
+ import { VariablePricingInput } from "./VariablePricingInput.js";
14
15
  const emptyAddress = { street: "", city: "", state: "", zip: "", country: "" };
15
16
  export function AnySpendCheckout({ mode = "page", recipientAddress, destinationTokenAddress, destinationTokenChainId, items, totalAmount: totalAmountOverride, organizationName, organizationLogo, themeColor, buttonText = "Pay", checkoutSessionId, onSuccess, onError, returnUrl, returnLabel, classes, footer, defaultPaymentMethod, senderAddress, slots, content, theme, showPoints, showOrderId, shipping: shippingProp, tax, discount: discountProp, summaryLines,
16
17
  // New form props
@@ -18,7 +19,13 @@ formSchema, formComponent, onFormSubmit,
18
19
  // New shipping props
19
20
  shippingOptions, collectShippingAddress, onShippingChange: onShippingChangeProp,
20
21
  // New discount props
21
- enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount, feeOnTop, }) {
22
+ enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount,
23
+ // Variable pricing
24
+ variablePricing, feeOnTop, }) {
25
+ // ===== Variable pricing state =====
26
+ const [variablePricingAmount, setVariablePricingAmount] = useState("0");
27
+ const isVariablePricingActive = variablePricing?.enabled === true;
28
+ const isVariablePricingValid = isVariablePricingActive ? variablePricingAmount !== "0" : true;
22
29
  // ===== Form state =====
23
30
  const [formData, setFormData] = useState({});
24
31
  const [selectedShipping, setSelectedShipping] = useState(null);
@@ -63,7 +70,25 @@ enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount,
63
70
  return undefined;
64
71
  }, [appliedDiscount, discountProp]);
65
72
  // Compute total from items + adjustments (including dynamic shipping/discount)
73
+ // Variable pricing overrides the total when active
66
74
  const computedTotal = useMemo(() => {
75
+ if (isVariablePricingActive && variablePricingAmount !== "0") {
76
+ let total = safeBigInt(variablePricingAmount);
77
+ if (effectiveShipping?.amount)
78
+ total += safeBigInt(effectiveShipping.amount);
79
+ const taxAmt = typeof tax === "string" ? tax : tax?.amount;
80
+ if (taxAmt)
81
+ total += safeBigInt(taxAmt);
82
+ if (effectiveDiscount?.amount)
83
+ total -= safeBigInt(effectiveDiscount.amount);
84
+ if (summaryLines) {
85
+ for (const line of summaryLines)
86
+ total += safeBigInt(line.amount);
87
+ }
88
+ if (total < BigInt(0))
89
+ total = BigInt(0);
90
+ return total.toString();
91
+ }
67
92
  if (totalAmountOverride)
68
93
  return totalAmountOverride;
69
94
  let total = BigInt(0);
@@ -84,7 +109,16 @@ enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount,
84
109
  if (total < BigInt(0))
85
110
  total = BigInt(0);
86
111
  return total.toString();
87
- }, [items, totalAmountOverride, effectiveShipping, tax, effectiveDiscount, summaryLines]);
112
+ }, [
113
+ items,
114
+ totalAmountOverride,
115
+ effectiveShipping,
116
+ tax,
117
+ effectiveDiscount,
118
+ summaryLines,
119
+ isVariablePricingActive,
120
+ variablePricingAmount,
121
+ ]);
88
122
  // Get destination token metadata
89
123
  const { data: tokenData } = useTokenData(destinationTokenChainId, destinationTokenAddress);
90
124
  const tokenSymbol = tokenData?.symbol || "";
@@ -136,14 +170,27 @@ enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount,
136
170
  meta.customerName = formData.name;
137
171
  if (checkoutSessionId)
138
172
  meta.checkoutSessionId = checkoutSessionId;
173
+ if (isVariablePricingActive && variablePricingAmount !== "0") {
174
+ meta.variablePricingAmount = variablePricingAmount;
175
+ }
139
176
  return Object.keys(meta).length > 0 ? meta : undefined;
140
- }, [formData, selectedShipping, shippingAddress, appliedDiscount, checkoutSessionId]);
141
- // Check if required form fields are filled
177
+ }, [
178
+ formData,
179
+ selectedShipping,
180
+ shippingAddress,
181
+ appliedDiscount,
182
+ checkoutSessionId,
183
+ isVariablePricingActive,
184
+ variablePricingAmount,
185
+ ]);
186
+ // Check if required form fields are filled and variable pricing is valid
142
187
  const isFormValid = useMemo(() => {
188
+ if (!isVariablePricingValid)
189
+ return false;
143
190
  if (!formSchema)
144
191
  return true;
145
192
  return formSchema.fields.filter(f => f.required).every(f => formData[f.id] != null && formData[f.id] !== "");
146
- }, [formSchema, formData]);
193
+ }, [formSchema, formData, isVariablePricingValid]);
147
194
  // Check if we have a form panel to show
148
195
  const hasFormContent = (formSchema && formSchema.fields.length > 0) ||
149
196
  formComponent ||
@@ -151,5 +198,5 @@ enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount,
151
198
  (shippingOptions && shippingOptions.length > 0) ||
152
199
  collectShippingAddress ||
153
200
  enableDiscountCode;
154
- return (_jsx(AnySpendFingerprintWrapper, { fingerprint: fingerprint, children: _jsx(AnySpendCustomizationProvider, { slots: slots, content: content, theme: theme, children: _jsx(CheckoutLayout, { mode: mode, paymentPanel: _jsxs(_Fragment, { children: [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 })] }), 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 }) }) }));
201
+ return (_jsx(AnySpendFingerprintWrapper, { fingerprint: fingerprint, children: _jsx(AnySpendCustomizationProvider, { slots: slots, content: content, 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 })] }), 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 }) }) }));
155
202
  }
@@ -0,0 +1,17 @@
1
+ export interface VariablePricingConfig {
2
+ enabled: boolean;
3
+ minAmount?: string;
4
+ maxAmount?: string;
5
+ suggestedAmount?: string;
6
+ label?: string;
7
+ currency?: string;
8
+ }
9
+ interface VariablePricingInputProps {
10
+ config: VariablePricingConfig;
11
+ tokenDecimals: number;
12
+ tokenSymbol: string;
13
+ themeColor?: string;
14
+ onChange: (amountWei: string) => void;
15
+ }
16
+ export declare function VariablePricingInput({ config, tokenDecimals, tokenSymbol, themeColor, onChange, }: VariablePricingInputProps): import("react/jsx-runtime").JSX.Element;
17
+ export {};