@b3dotfun/sdk 0.1.65-alpha.3 → 0.1.65-alpha.4
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/AnySpendCollectorClubPurchase.d.ts +6 -1
- package/dist/cjs/anyspend/react/components/AnySpendCollectorClubPurchase.js +126 -9
- package/dist/cjs/global-account/react/stores/useModalStore.d.ts +2 -0
- package/dist/esm/anyspend/react/components/AnySpendCollectorClubPurchase.d.ts +6 -1
- package/dist/esm/anyspend/react/components/AnySpendCollectorClubPurchase.js +128 -11
- package/dist/esm/global-account/react/stores/useModalStore.d.ts +2 -0
- package/dist/types/anyspend/react/components/AnySpendCollectorClubPurchase.d.ts +6 -1
- package/dist/types/global-account/react/stores/useModalStore.d.ts +2 -0
- package/package.json +1 -1
- package/src/anyspend/react/components/AnySpendCollectorClubPurchase.tsx +169 -10
- package/src/global-account/react/stores/useModalStore.ts +2 -0
|
@@ -69,5 +69,10 @@ export interface AnySpendCollectorClubPurchaseProps {
|
|
|
69
69
|
* Force fiat payment
|
|
70
70
|
*/
|
|
71
71
|
forceFiatPayment?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Optional discount code to apply to the purchase.
|
|
74
|
+
* When provided, validates on-chain and adjusts the price accordingly.
|
|
75
|
+
*/
|
|
76
|
+
discountCode?: string;
|
|
72
77
|
}
|
|
73
|
-
export declare function AnySpendCollectorClubPurchase({ loadOrder, mode, activeTab, packId, packAmount, pricePerPack, paymentToken, recipientAddress, spenderAddress, isStaging, onSuccess, header, showRecipient, vendingMachineId, packType, forceFiatPayment, }: AnySpendCollectorClubPurchaseProps): import("react/jsx-runtime").JSX.Element;
|
|
78
|
+
export declare function AnySpendCollectorClubPurchase({ loadOrder, mode, activeTab, packId, packAmount, pricePerPack, paymentToken, recipientAddress, spenderAddress, isStaging, onSuccess, header, showRecipient, vendingMachineId, packType, forceFiatPayment, discountCode, }: AnySpendCollectorClubPurchaseProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -29,9 +29,11 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
31
31
|
const constants_1 = require("../../../anyspend/constants");
|
|
32
|
+
const constants_2 = require("../../../shared/constants");
|
|
32
33
|
const number_1 = require("../../../shared/utils/number");
|
|
33
34
|
const react_1 = require("react");
|
|
34
35
|
const viem_1 = require("viem");
|
|
36
|
+
const chains_1 = require("viem/chains");
|
|
35
37
|
const AnySpendCustom_1 = require("./AnySpendCustom");
|
|
36
38
|
// Collector Club Shop contract addresses on Base
|
|
37
39
|
const CC_SHOP_ADDRESS = "0x47366E64E4917dd4DdC04Fb9DC507c1dD2b87294";
|
|
@@ -49,7 +51,35 @@ const BUY_PACKS_FOR_ABI = {
|
|
|
49
51
|
stateMutability: "nonpayable",
|
|
50
52
|
type: "function",
|
|
51
53
|
};
|
|
52
|
-
|
|
54
|
+
// ABI for buyPacksForWithDiscount function (with discount code)
|
|
55
|
+
const BUY_PACKS_FOR_WITH_DISCOUNT_ABI = {
|
|
56
|
+
inputs: [
|
|
57
|
+
{ internalType: "address", name: "user", type: "address" },
|
|
58
|
+
{ internalType: "uint256", name: "packId", type: "uint256" },
|
|
59
|
+
{ internalType: "uint256", name: "amount", type: "uint256" },
|
|
60
|
+
{ internalType: "string", name: "discountCode", type: "string" },
|
|
61
|
+
],
|
|
62
|
+
name: "buyPacksForWithDiscount",
|
|
63
|
+
outputs: [],
|
|
64
|
+
stateMutability: "nonpayable",
|
|
65
|
+
type: "function",
|
|
66
|
+
};
|
|
67
|
+
// ABI for isDiscountCodeValid view function
|
|
68
|
+
const IS_DISCOUNT_CODE_VALID_ABI = {
|
|
69
|
+
inputs: [{ internalType: "string", name: "code", type: "string" }],
|
|
70
|
+
name: "isDiscountCodeValid",
|
|
71
|
+
outputs: [
|
|
72
|
+
{ internalType: "bool", name: "isValid", type: "bool" },
|
|
73
|
+
{ internalType: "uint256", name: "discountAmount", type: "uint256" },
|
|
74
|
+
],
|
|
75
|
+
stateMutability: "view",
|
|
76
|
+
type: "function",
|
|
77
|
+
};
|
|
78
|
+
const basePublicClient = (0, viem_1.createPublicClient)({
|
|
79
|
+
chain: chains_1.base,
|
|
80
|
+
transport: (0, viem_1.http)(constants_2.PUBLIC_BASE_RPC_URL),
|
|
81
|
+
});
|
|
82
|
+
function AnySpendCollectorClubPurchase({ loadOrder, mode = "modal", activeTab = "crypto", packId, packAmount, pricePerPack, paymentToken = constants_1.USDC_BASE, recipientAddress, spenderAddress, isStaging = false, onSuccess, header, showRecipient = true, vendingMachineId, packType, forceFiatPayment, discountCode, }) {
|
|
53
83
|
const ccShopAddress = isStaging ? CC_SHOP_ADDRESS_STAGING : CC_SHOP_ADDRESS;
|
|
54
84
|
// Calculate total amount needed (pricePerPack * packAmount)
|
|
55
85
|
const totalAmount = (0, react_1.useMemo)(() => {
|
|
@@ -61,15 +91,89 @@ function AnySpendCollectorClubPurchase({ loadOrder, mode = "modal", activeTab =
|
|
|
61
91
|
return "0";
|
|
62
92
|
}
|
|
63
93
|
}, [pricePerPack, packAmount]);
|
|
64
|
-
//
|
|
94
|
+
// Discount code validation state
|
|
95
|
+
const [discountInfo, setDiscountInfo] = (0, react_1.useState)({
|
|
96
|
+
isValid: false,
|
|
97
|
+
discountAmount: BigInt(0),
|
|
98
|
+
isLoading: false,
|
|
99
|
+
error: null,
|
|
100
|
+
});
|
|
101
|
+
// Validate discount code on-chain when provided
|
|
102
|
+
(0, react_1.useEffect)(() => {
|
|
103
|
+
if (!discountCode) {
|
|
104
|
+
setDiscountInfo({ isValid: false, discountAmount: BigInt(0), isLoading: false, error: null });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
let cancelled = false;
|
|
108
|
+
const validateDiscount = async () => {
|
|
109
|
+
setDiscountInfo(prev => ({ ...prev, isLoading: true, error: null }));
|
|
110
|
+
try {
|
|
111
|
+
const result = await basePublicClient.readContract({
|
|
112
|
+
address: ccShopAddress,
|
|
113
|
+
abi: [IS_DISCOUNT_CODE_VALID_ABI],
|
|
114
|
+
functionName: "isDiscountCodeValid",
|
|
115
|
+
args: [discountCode],
|
|
116
|
+
});
|
|
117
|
+
if (cancelled)
|
|
118
|
+
return;
|
|
119
|
+
const [isValid, discountAmount] = result;
|
|
120
|
+
if (!isValid) {
|
|
121
|
+
setDiscountInfo({
|
|
122
|
+
isValid: false,
|
|
123
|
+
discountAmount: BigInt(0),
|
|
124
|
+
isLoading: false,
|
|
125
|
+
error: "Invalid or expired discount code",
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
setDiscountInfo({ isValid: true, discountAmount, isLoading: false, error: null });
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (cancelled)
|
|
133
|
+
return;
|
|
134
|
+
console.error("Failed to validate discount code", { discountCode, error });
|
|
135
|
+
setDiscountInfo({
|
|
136
|
+
isValid: false,
|
|
137
|
+
discountAmount: BigInt(0),
|
|
138
|
+
isLoading: false,
|
|
139
|
+
error: "Failed to validate discount code",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
validateDiscount();
|
|
144
|
+
return () => {
|
|
145
|
+
cancelled = true;
|
|
146
|
+
};
|
|
147
|
+
}, [discountCode, ccShopAddress]);
|
|
148
|
+
// Calculate effective dstAmount after discount
|
|
149
|
+
const effectiveDstAmount = (0, react_1.useMemo)(() => {
|
|
150
|
+
if (!discountCode || !discountInfo.isValid || discountInfo.discountAmount === BigInt(0)) {
|
|
151
|
+
return totalAmount;
|
|
152
|
+
}
|
|
153
|
+
const total = BigInt(totalAmount);
|
|
154
|
+
const discount = discountInfo.discountAmount;
|
|
155
|
+
if (discount >= total) {
|
|
156
|
+
console.error("Discount exceeds total price", { totalAmount, discountAmount: discount.toString() });
|
|
157
|
+
return "0";
|
|
158
|
+
}
|
|
159
|
+
return (total - discount).toString();
|
|
160
|
+
}, [totalAmount, discountCode, discountInfo.isValid, discountInfo.discountAmount]);
|
|
161
|
+
// Calculate fiat amount (effectiveDstAmount in USD, assuming USDC with 6 decimals)
|
|
65
162
|
const srcFiatAmount = (0, react_1.useMemo)(() => {
|
|
66
|
-
if (!
|
|
163
|
+
if (!effectiveDstAmount || effectiveDstAmount === "0")
|
|
67
164
|
return "0";
|
|
68
|
-
return (0, number_1.formatUnits)(
|
|
69
|
-
}, [
|
|
70
|
-
// Encode the
|
|
165
|
+
return (0, number_1.formatUnits)(effectiveDstAmount, constants_1.USDC_BASE.decimals);
|
|
166
|
+
}, [effectiveDstAmount]);
|
|
167
|
+
// Encode the contract function call (with or without discount)
|
|
71
168
|
const encodedData = (0, react_1.useMemo)(() => {
|
|
72
169
|
try {
|
|
170
|
+
if (discountCode && discountInfo.isValid) {
|
|
171
|
+
return (0, viem_1.encodeFunctionData)({
|
|
172
|
+
abi: [BUY_PACKS_FOR_WITH_DISCOUNT_ABI],
|
|
173
|
+
functionName: "buyPacksForWithDiscount",
|
|
174
|
+
args: [recipientAddress, BigInt(packId), BigInt(packAmount), discountCode],
|
|
175
|
+
});
|
|
176
|
+
}
|
|
73
177
|
return (0, viem_1.encodeFunctionData)({
|
|
74
178
|
abi: [BUY_PACKS_FOR_ABI],
|
|
75
179
|
functionName: "buyPacksFor",
|
|
@@ -77,17 +181,30 @@ function AnySpendCollectorClubPurchase({ loadOrder, mode = "modal", activeTab =
|
|
|
77
181
|
});
|
|
78
182
|
}
|
|
79
183
|
catch (error) {
|
|
80
|
-
console.error("Failed to encode function data", { recipientAddress, packId, packAmount, error });
|
|
184
|
+
console.error("Failed to encode function data", { recipientAddress, packId, packAmount, discountCode, error });
|
|
81
185
|
return "0x";
|
|
82
186
|
}
|
|
83
|
-
}, [recipientAddress, packId, packAmount]);
|
|
187
|
+
}, [recipientAddress, packId, packAmount, discountCode, discountInfo.isValid]);
|
|
84
188
|
// Default header if not provided
|
|
85
189
|
const defaultHeader = () => ((0, jsx_runtime_1.jsx)("div", { className: "mb-4 flex flex-col items-center gap-3 text-center", children: (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h1", { className: "text-as-primary text-xl font-bold", children: "Buy Collector Club Packs" }), (0, jsx_runtime_1.jsxs)("p", { className: "text-as-secondary text-sm", children: ["Purchase ", packAmount, " pack", packAmount !== 1 ? "s" : "", " using any token"] })] }) }));
|
|
86
|
-
|
|
190
|
+
// Don't render AnySpendCustom while discount is being validated (avoids showing wrong price)
|
|
191
|
+
if (discountCode && discountInfo.isLoading) {
|
|
192
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: "mb-4 flex flex-col items-center gap-3 text-center", children: (0, jsx_runtime_1.jsx)("p", { className: "text-as-secondary text-sm", children: "Validating discount code..." }) }));
|
|
193
|
+
}
|
|
194
|
+
if (discountCode && discountInfo.error) {
|
|
195
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: "mb-4 flex flex-col items-center gap-3 text-center", children: (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-red-500", children: discountInfo.error }) }));
|
|
196
|
+
}
|
|
197
|
+
if (discountCode && discountInfo.isValid && effectiveDstAmount === "0") {
|
|
198
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: "mb-4 flex flex-col items-center gap-3 text-center", children: (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-red-500", children: "Discount exceeds total price" }) }));
|
|
199
|
+
}
|
|
200
|
+
return ((0, jsx_runtime_1.jsx)(AnySpendCustom_1.AnySpendCustom, { loadOrder: loadOrder, mode: mode, activeTab: activeTab, recipientAddress: recipientAddress, spenderAddress: spenderAddress ?? ccShopAddress, orderType: "custom", dstChainId: BASE_CHAIN_ID, dstToken: paymentToken, dstAmount: effectiveDstAmount, contractAddress: ccShopAddress, encodedData: encodedData, metadata: {
|
|
87
201
|
packId,
|
|
88
202
|
packAmount,
|
|
89
203
|
pricePerPack,
|
|
90
204
|
vendingMachineId,
|
|
91
205
|
packType,
|
|
206
|
+
...(discountCode && discountInfo.isValid
|
|
207
|
+
? { discountCode, discountAmount: discountInfo.discountAmount.toString() }
|
|
208
|
+
: {}),
|
|
92
209
|
}, header: header || defaultHeader, onSuccess: onSuccess, showRecipient: showRecipient, srcFiatAmount: srcFiatAmount, forceFiatPayment: forceFiatPayment }));
|
|
93
210
|
}
|
|
@@ -470,6 +470,8 @@ export interface AnySpendCollectorClubPurchaseProps extends BaseModalProps {
|
|
|
470
470
|
forceFiatPayment?: boolean;
|
|
471
471
|
/** Staging environment support */
|
|
472
472
|
isStaging?: boolean;
|
|
473
|
+
/** Optional discount code to apply to the purchase */
|
|
474
|
+
discountCode?: string;
|
|
473
475
|
}
|
|
474
476
|
/**
|
|
475
477
|
* Props for the AnySpend Deposit modal
|
|
@@ -69,5 +69,10 @@ export interface AnySpendCollectorClubPurchaseProps {
|
|
|
69
69
|
* Force fiat payment
|
|
70
70
|
*/
|
|
71
71
|
forceFiatPayment?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Optional discount code to apply to the purchase.
|
|
74
|
+
* When provided, validates on-chain and adjusts the price accordingly.
|
|
75
|
+
*/
|
|
76
|
+
discountCode?: string;
|
|
72
77
|
}
|
|
73
|
-
export declare function AnySpendCollectorClubPurchase({ loadOrder, mode, activeTab, packId, packAmount, pricePerPack, paymentToken, recipientAddress, spenderAddress, isStaging, onSuccess, header, showRecipient, vendingMachineId, packType, forceFiatPayment, }: AnySpendCollectorClubPurchaseProps): import("react/jsx-runtime").JSX.Element;
|
|
78
|
+
export declare function AnySpendCollectorClubPurchase({ loadOrder, mode, activeTab, packId, packAmount, pricePerPack, paymentToken, recipientAddress, spenderAddress, isStaging, onSuccess, header, showRecipient, vendingMachineId, packType, forceFiatPayment, discountCode, }: AnySpendCollectorClubPurchaseProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -26,9 +26,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
28
|
import { USDC_BASE } from "../../../anyspend/constants/index.js";
|
|
29
|
+
import { PUBLIC_BASE_RPC_URL } from "../../../shared/constants/index.js";
|
|
29
30
|
import { formatUnits } from "../../../shared/utils/number.js";
|
|
30
|
-
import { useMemo } from "react";
|
|
31
|
-
import { encodeFunctionData } from "viem";
|
|
31
|
+
import { useEffect, useMemo, useState } from "react";
|
|
32
|
+
import { createPublicClient, encodeFunctionData, http } from "viem";
|
|
33
|
+
import { base } from "viem/chains";
|
|
32
34
|
import { AnySpendCustom } from "./AnySpendCustom.js";
|
|
33
35
|
// Collector Club Shop contract addresses on Base
|
|
34
36
|
const CC_SHOP_ADDRESS = "0x47366E64E4917dd4DdC04Fb9DC507c1dD2b87294";
|
|
@@ -46,7 +48,35 @@ const BUY_PACKS_FOR_ABI = {
|
|
|
46
48
|
stateMutability: "nonpayable",
|
|
47
49
|
type: "function",
|
|
48
50
|
};
|
|
49
|
-
|
|
51
|
+
// ABI for buyPacksForWithDiscount function (with discount code)
|
|
52
|
+
const BUY_PACKS_FOR_WITH_DISCOUNT_ABI = {
|
|
53
|
+
inputs: [
|
|
54
|
+
{ internalType: "address", name: "user", type: "address" },
|
|
55
|
+
{ internalType: "uint256", name: "packId", type: "uint256" },
|
|
56
|
+
{ internalType: "uint256", name: "amount", type: "uint256" },
|
|
57
|
+
{ internalType: "string", name: "discountCode", type: "string" },
|
|
58
|
+
],
|
|
59
|
+
name: "buyPacksForWithDiscount",
|
|
60
|
+
outputs: [],
|
|
61
|
+
stateMutability: "nonpayable",
|
|
62
|
+
type: "function",
|
|
63
|
+
};
|
|
64
|
+
// ABI for isDiscountCodeValid view function
|
|
65
|
+
const IS_DISCOUNT_CODE_VALID_ABI = {
|
|
66
|
+
inputs: [{ internalType: "string", name: "code", type: "string" }],
|
|
67
|
+
name: "isDiscountCodeValid",
|
|
68
|
+
outputs: [
|
|
69
|
+
{ internalType: "bool", name: "isValid", type: "bool" },
|
|
70
|
+
{ internalType: "uint256", name: "discountAmount", type: "uint256" },
|
|
71
|
+
],
|
|
72
|
+
stateMutability: "view",
|
|
73
|
+
type: "function",
|
|
74
|
+
};
|
|
75
|
+
const basePublicClient = createPublicClient({
|
|
76
|
+
chain: base,
|
|
77
|
+
transport: http(PUBLIC_BASE_RPC_URL),
|
|
78
|
+
});
|
|
79
|
+
export function AnySpendCollectorClubPurchase({ loadOrder, mode = "modal", activeTab = "crypto", packId, packAmount, pricePerPack, paymentToken = USDC_BASE, recipientAddress, spenderAddress, isStaging = false, onSuccess, header, showRecipient = true, vendingMachineId, packType, forceFiatPayment, discountCode, }) {
|
|
50
80
|
const ccShopAddress = isStaging ? CC_SHOP_ADDRESS_STAGING : CC_SHOP_ADDRESS;
|
|
51
81
|
// Calculate total amount needed (pricePerPack * packAmount)
|
|
52
82
|
const totalAmount = useMemo(() => {
|
|
@@ -58,15 +88,89 @@ export function AnySpendCollectorClubPurchase({ loadOrder, mode = "modal", activ
|
|
|
58
88
|
return "0";
|
|
59
89
|
}
|
|
60
90
|
}, [pricePerPack, packAmount]);
|
|
61
|
-
//
|
|
91
|
+
// Discount code validation state
|
|
92
|
+
const [discountInfo, setDiscountInfo] = useState({
|
|
93
|
+
isValid: false,
|
|
94
|
+
discountAmount: BigInt(0),
|
|
95
|
+
isLoading: false,
|
|
96
|
+
error: null,
|
|
97
|
+
});
|
|
98
|
+
// Validate discount code on-chain when provided
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!discountCode) {
|
|
101
|
+
setDiscountInfo({ isValid: false, discountAmount: BigInt(0), isLoading: false, error: null });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
let cancelled = false;
|
|
105
|
+
const validateDiscount = async () => {
|
|
106
|
+
setDiscountInfo(prev => ({ ...prev, isLoading: true, error: null }));
|
|
107
|
+
try {
|
|
108
|
+
const result = await basePublicClient.readContract({
|
|
109
|
+
address: ccShopAddress,
|
|
110
|
+
abi: [IS_DISCOUNT_CODE_VALID_ABI],
|
|
111
|
+
functionName: "isDiscountCodeValid",
|
|
112
|
+
args: [discountCode],
|
|
113
|
+
});
|
|
114
|
+
if (cancelled)
|
|
115
|
+
return;
|
|
116
|
+
const [isValid, discountAmount] = result;
|
|
117
|
+
if (!isValid) {
|
|
118
|
+
setDiscountInfo({
|
|
119
|
+
isValid: false,
|
|
120
|
+
discountAmount: BigInt(0),
|
|
121
|
+
isLoading: false,
|
|
122
|
+
error: "Invalid or expired discount code",
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
setDiscountInfo({ isValid: true, discountAmount, isLoading: false, error: null });
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (cancelled)
|
|
130
|
+
return;
|
|
131
|
+
console.error("Failed to validate discount code", { discountCode, error });
|
|
132
|
+
setDiscountInfo({
|
|
133
|
+
isValid: false,
|
|
134
|
+
discountAmount: BigInt(0),
|
|
135
|
+
isLoading: false,
|
|
136
|
+
error: "Failed to validate discount code",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
validateDiscount();
|
|
141
|
+
return () => {
|
|
142
|
+
cancelled = true;
|
|
143
|
+
};
|
|
144
|
+
}, [discountCode, ccShopAddress]);
|
|
145
|
+
// Calculate effective dstAmount after discount
|
|
146
|
+
const effectiveDstAmount = useMemo(() => {
|
|
147
|
+
if (!discountCode || !discountInfo.isValid || discountInfo.discountAmount === BigInt(0)) {
|
|
148
|
+
return totalAmount;
|
|
149
|
+
}
|
|
150
|
+
const total = BigInt(totalAmount);
|
|
151
|
+
const discount = discountInfo.discountAmount;
|
|
152
|
+
if (discount >= total) {
|
|
153
|
+
console.error("Discount exceeds total price", { totalAmount, discountAmount: discount.toString() });
|
|
154
|
+
return "0";
|
|
155
|
+
}
|
|
156
|
+
return (total - discount).toString();
|
|
157
|
+
}, [totalAmount, discountCode, discountInfo.isValid, discountInfo.discountAmount]);
|
|
158
|
+
// Calculate fiat amount (effectiveDstAmount in USD, assuming USDC with 6 decimals)
|
|
62
159
|
const srcFiatAmount = useMemo(() => {
|
|
63
|
-
if (!
|
|
160
|
+
if (!effectiveDstAmount || effectiveDstAmount === "0")
|
|
64
161
|
return "0";
|
|
65
|
-
return formatUnits(
|
|
66
|
-
}, [
|
|
67
|
-
// Encode the
|
|
162
|
+
return formatUnits(effectiveDstAmount, USDC_BASE.decimals);
|
|
163
|
+
}, [effectiveDstAmount]);
|
|
164
|
+
// Encode the contract function call (with or without discount)
|
|
68
165
|
const encodedData = useMemo(() => {
|
|
69
166
|
try {
|
|
167
|
+
if (discountCode && discountInfo.isValid) {
|
|
168
|
+
return encodeFunctionData({
|
|
169
|
+
abi: [BUY_PACKS_FOR_WITH_DISCOUNT_ABI],
|
|
170
|
+
functionName: "buyPacksForWithDiscount",
|
|
171
|
+
args: [recipientAddress, BigInt(packId), BigInt(packAmount), discountCode],
|
|
172
|
+
});
|
|
173
|
+
}
|
|
70
174
|
return encodeFunctionData({
|
|
71
175
|
abi: [BUY_PACKS_FOR_ABI],
|
|
72
176
|
functionName: "buyPacksFor",
|
|
@@ -74,17 +178,30 @@ export function AnySpendCollectorClubPurchase({ loadOrder, mode = "modal", activ
|
|
|
74
178
|
});
|
|
75
179
|
}
|
|
76
180
|
catch (error) {
|
|
77
|
-
console.error("Failed to encode function data", { recipientAddress, packId, packAmount, error });
|
|
181
|
+
console.error("Failed to encode function data", { recipientAddress, packId, packAmount, discountCode, error });
|
|
78
182
|
return "0x";
|
|
79
183
|
}
|
|
80
|
-
}, [recipientAddress, packId, packAmount]);
|
|
184
|
+
}, [recipientAddress, packId, packAmount, discountCode, discountInfo.isValid]);
|
|
81
185
|
// Default header if not provided
|
|
82
186
|
const defaultHeader = () => (_jsx("div", { className: "mb-4 flex flex-col items-center gap-3 text-center", children: _jsxs("div", { children: [_jsx("h1", { className: "text-as-primary text-xl font-bold", children: "Buy Collector Club Packs" }), _jsxs("p", { className: "text-as-secondary text-sm", children: ["Purchase ", packAmount, " pack", packAmount !== 1 ? "s" : "", " using any token"] })] }) }));
|
|
83
|
-
|
|
187
|
+
// Don't render AnySpendCustom while discount is being validated (avoids showing wrong price)
|
|
188
|
+
if (discountCode && discountInfo.isLoading) {
|
|
189
|
+
return (_jsx("div", { className: "mb-4 flex flex-col items-center gap-3 text-center", children: _jsx("p", { className: "text-as-secondary text-sm", children: "Validating discount code..." }) }));
|
|
190
|
+
}
|
|
191
|
+
if (discountCode && discountInfo.error) {
|
|
192
|
+
return (_jsx("div", { className: "mb-4 flex flex-col items-center gap-3 text-center", children: _jsx("p", { className: "text-sm text-red-500", children: discountInfo.error }) }));
|
|
193
|
+
}
|
|
194
|
+
if (discountCode && discountInfo.isValid && effectiveDstAmount === "0") {
|
|
195
|
+
return (_jsx("div", { className: "mb-4 flex flex-col items-center gap-3 text-center", children: _jsx("p", { className: "text-sm text-red-500", children: "Discount exceeds total price" }) }));
|
|
196
|
+
}
|
|
197
|
+
return (_jsx(AnySpendCustom, { loadOrder: loadOrder, mode: mode, activeTab: activeTab, recipientAddress: recipientAddress, spenderAddress: spenderAddress ?? ccShopAddress, orderType: "custom", dstChainId: BASE_CHAIN_ID, dstToken: paymentToken, dstAmount: effectiveDstAmount, contractAddress: ccShopAddress, encodedData: encodedData, metadata: {
|
|
84
198
|
packId,
|
|
85
199
|
packAmount,
|
|
86
200
|
pricePerPack,
|
|
87
201
|
vendingMachineId,
|
|
88
202
|
packType,
|
|
203
|
+
...(discountCode && discountInfo.isValid
|
|
204
|
+
? { discountCode, discountAmount: discountInfo.discountAmount.toString() }
|
|
205
|
+
: {}),
|
|
89
206
|
}, header: header || defaultHeader, onSuccess: onSuccess, showRecipient: showRecipient, srcFiatAmount: srcFiatAmount, forceFiatPayment: forceFiatPayment }));
|
|
90
207
|
}
|
|
@@ -470,6 +470,8 @@ export interface AnySpendCollectorClubPurchaseProps extends BaseModalProps {
|
|
|
470
470
|
forceFiatPayment?: boolean;
|
|
471
471
|
/** Staging environment support */
|
|
472
472
|
isStaging?: boolean;
|
|
473
|
+
/** Optional discount code to apply to the purchase */
|
|
474
|
+
discountCode?: string;
|
|
473
475
|
}
|
|
474
476
|
/**
|
|
475
477
|
* Props for the AnySpend Deposit modal
|
|
@@ -69,5 +69,10 @@ export interface AnySpendCollectorClubPurchaseProps {
|
|
|
69
69
|
* Force fiat payment
|
|
70
70
|
*/
|
|
71
71
|
forceFiatPayment?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Optional discount code to apply to the purchase.
|
|
74
|
+
* When provided, validates on-chain and adjusts the price accordingly.
|
|
75
|
+
*/
|
|
76
|
+
discountCode?: string;
|
|
72
77
|
}
|
|
73
|
-
export declare function AnySpendCollectorClubPurchase({ loadOrder, mode, activeTab, packId, packAmount, pricePerPack, paymentToken, recipientAddress, spenderAddress, isStaging, onSuccess, header, showRecipient, vendingMachineId, packType, forceFiatPayment, }: AnySpendCollectorClubPurchaseProps): import("react/jsx-runtime").JSX.Element;
|
|
78
|
+
export declare function AnySpendCollectorClubPurchase({ loadOrder, mode, activeTab, packId, packAmount, pricePerPack, paymentToken, recipientAddress, spenderAddress, isStaging, onSuccess, header, showRecipient, vendingMachineId, packType, forceFiatPayment, discountCode, }: AnySpendCollectorClubPurchaseProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -470,6 +470,8 @@ export interface AnySpendCollectorClubPurchaseProps extends BaseModalProps {
|
|
|
470
470
|
forceFiatPayment?: boolean;
|
|
471
471
|
/** Staging environment support */
|
|
472
472
|
isStaging?: boolean;
|
|
473
|
+
/** Optional discount code to apply to the purchase */
|
|
474
|
+
discountCode?: string;
|
|
473
475
|
}
|
|
474
476
|
/**
|
|
475
477
|
* Props for the AnySpend Deposit modal
|
package/package.json
CHANGED
|
@@ -27,9 +27,11 @@
|
|
|
27
27
|
import { USDC_BASE } from "@b3dotfun/sdk/anyspend/constants";
|
|
28
28
|
import { components } from "@b3dotfun/sdk/anyspend/types/api";
|
|
29
29
|
import { GetQuoteResponse } from "@b3dotfun/sdk/anyspend/types/api_req_res";
|
|
30
|
+
import { PUBLIC_BASE_RPC_URL } from "@b3dotfun/sdk/shared/constants";
|
|
30
31
|
import { formatUnits } from "@b3dotfun/sdk/shared/utils/number";
|
|
31
|
-
import React, { useMemo } from "react";
|
|
32
|
-
import { encodeFunctionData } from "viem";
|
|
32
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
33
|
+
import { createPublicClient, encodeFunctionData, http } from "viem";
|
|
34
|
+
import { base } from "viem/chains";
|
|
33
35
|
import { AnySpendCustom } from "./AnySpendCustom";
|
|
34
36
|
|
|
35
37
|
// Collector Club Shop contract addresses on Base
|
|
@@ -50,6 +52,37 @@ const BUY_PACKS_FOR_ABI = {
|
|
|
50
52
|
type: "function",
|
|
51
53
|
} as const;
|
|
52
54
|
|
|
55
|
+
// ABI for buyPacksForWithDiscount function (with discount code)
|
|
56
|
+
const BUY_PACKS_FOR_WITH_DISCOUNT_ABI = {
|
|
57
|
+
inputs: [
|
|
58
|
+
{ internalType: "address", name: "user", type: "address" },
|
|
59
|
+
{ internalType: "uint256", name: "packId", type: "uint256" },
|
|
60
|
+
{ internalType: "uint256", name: "amount", type: "uint256" },
|
|
61
|
+
{ internalType: "string", name: "discountCode", type: "string" },
|
|
62
|
+
],
|
|
63
|
+
name: "buyPacksForWithDiscount",
|
|
64
|
+
outputs: [],
|
|
65
|
+
stateMutability: "nonpayable",
|
|
66
|
+
type: "function",
|
|
67
|
+
} as const;
|
|
68
|
+
|
|
69
|
+
// ABI for isDiscountCodeValid view function
|
|
70
|
+
const IS_DISCOUNT_CODE_VALID_ABI = {
|
|
71
|
+
inputs: [{ internalType: "string", name: "code", type: "string" }],
|
|
72
|
+
name: "isDiscountCodeValid",
|
|
73
|
+
outputs: [
|
|
74
|
+
{ internalType: "bool", name: "isValid", type: "bool" },
|
|
75
|
+
{ internalType: "uint256", name: "discountAmount", type: "uint256" },
|
|
76
|
+
],
|
|
77
|
+
stateMutability: "view",
|
|
78
|
+
type: "function",
|
|
79
|
+
} as const;
|
|
80
|
+
|
|
81
|
+
const basePublicClient = createPublicClient({
|
|
82
|
+
chain: base,
|
|
83
|
+
transport: http(PUBLIC_BASE_RPC_URL),
|
|
84
|
+
});
|
|
85
|
+
|
|
53
86
|
export interface AnySpendCollectorClubPurchaseProps {
|
|
54
87
|
/**
|
|
55
88
|
* Optional order ID to load existing order
|
|
@@ -118,6 +151,11 @@ export interface AnySpendCollectorClubPurchaseProps {
|
|
|
118
151
|
* Force fiat payment
|
|
119
152
|
*/
|
|
120
153
|
forceFiatPayment?: boolean;
|
|
154
|
+
/**
|
|
155
|
+
* Optional discount code to apply to the purchase.
|
|
156
|
+
* When provided, validates on-chain and adjusts the price accordingly.
|
|
157
|
+
*/
|
|
158
|
+
discountCode?: string;
|
|
121
159
|
}
|
|
122
160
|
|
|
123
161
|
export function AnySpendCollectorClubPurchase({
|
|
@@ -137,6 +175,7 @@ export function AnySpendCollectorClubPurchase({
|
|
|
137
175
|
vendingMachineId,
|
|
138
176
|
packType,
|
|
139
177
|
forceFiatPayment,
|
|
178
|
+
discountCode,
|
|
140
179
|
}: AnySpendCollectorClubPurchaseProps) {
|
|
141
180
|
const ccShopAddress = isStaging ? CC_SHOP_ADDRESS_STAGING : CC_SHOP_ADDRESS;
|
|
142
181
|
|
|
@@ -150,25 +189,117 @@ export function AnySpendCollectorClubPurchase({
|
|
|
150
189
|
}
|
|
151
190
|
}, [pricePerPack, packAmount]);
|
|
152
191
|
|
|
153
|
-
//
|
|
192
|
+
// Discount code validation state
|
|
193
|
+
const [discountInfo, setDiscountInfo] = useState<{
|
|
194
|
+
isValid: boolean;
|
|
195
|
+
discountAmount: bigint;
|
|
196
|
+
isLoading: boolean;
|
|
197
|
+
error: string | null;
|
|
198
|
+
}>({
|
|
199
|
+
isValid: false,
|
|
200
|
+
discountAmount: BigInt(0),
|
|
201
|
+
isLoading: false,
|
|
202
|
+
error: null,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Validate discount code on-chain when provided
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
if (!discountCode) {
|
|
208
|
+
setDiscountInfo({ isValid: false, discountAmount: BigInt(0), isLoading: false, error: null });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let cancelled = false;
|
|
213
|
+
|
|
214
|
+
const validateDiscount = async () => {
|
|
215
|
+
setDiscountInfo(prev => ({ ...prev, isLoading: true, error: null }));
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const result = await basePublicClient.readContract({
|
|
219
|
+
address: ccShopAddress as `0x${string}`,
|
|
220
|
+
abi: [IS_DISCOUNT_CODE_VALID_ABI],
|
|
221
|
+
functionName: "isDiscountCodeValid",
|
|
222
|
+
args: [discountCode],
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (cancelled) return;
|
|
226
|
+
|
|
227
|
+
const [isValid, discountAmount] = result;
|
|
228
|
+
|
|
229
|
+
if (!isValid) {
|
|
230
|
+
setDiscountInfo({
|
|
231
|
+
isValid: false,
|
|
232
|
+
discountAmount: BigInt(0),
|
|
233
|
+
isLoading: false,
|
|
234
|
+
error: "Invalid or expired discount code",
|
|
235
|
+
});
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
setDiscountInfo({ isValid: true, discountAmount, isLoading: false, error: null });
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (cancelled) return;
|
|
242
|
+
console.error("Failed to validate discount code", { discountCode, error });
|
|
243
|
+
setDiscountInfo({
|
|
244
|
+
isValid: false,
|
|
245
|
+
discountAmount: BigInt(0),
|
|
246
|
+
isLoading: false,
|
|
247
|
+
error: "Failed to validate discount code",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
validateDiscount();
|
|
253
|
+
|
|
254
|
+
return () => {
|
|
255
|
+
cancelled = true;
|
|
256
|
+
};
|
|
257
|
+
}, [discountCode, ccShopAddress]);
|
|
258
|
+
|
|
259
|
+
// Calculate effective dstAmount after discount
|
|
260
|
+
const effectiveDstAmount = useMemo(() => {
|
|
261
|
+
if (!discountCode || !discountInfo.isValid || discountInfo.discountAmount === BigInt(0)) {
|
|
262
|
+
return totalAmount;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const total = BigInt(totalAmount);
|
|
266
|
+
const discount = discountInfo.discountAmount;
|
|
267
|
+
|
|
268
|
+
if (discount >= total) {
|
|
269
|
+
console.error("Discount exceeds total price", { totalAmount, discountAmount: discount.toString() });
|
|
270
|
+
return "0";
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return (total - discount).toString();
|
|
274
|
+
}, [totalAmount, discountCode, discountInfo.isValid, discountInfo.discountAmount]);
|
|
275
|
+
|
|
276
|
+
// Calculate fiat amount (effectiveDstAmount in USD, assuming USDC with 6 decimals)
|
|
154
277
|
const srcFiatAmount = useMemo(() => {
|
|
155
|
-
if (!
|
|
156
|
-
return formatUnits(
|
|
157
|
-
}, [
|
|
278
|
+
if (!effectiveDstAmount || effectiveDstAmount === "0") return "0";
|
|
279
|
+
return formatUnits(effectiveDstAmount, USDC_BASE.decimals);
|
|
280
|
+
}, [effectiveDstAmount]);
|
|
158
281
|
|
|
159
|
-
// Encode the
|
|
282
|
+
// Encode the contract function call (with or without discount)
|
|
160
283
|
const encodedData = useMemo(() => {
|
|
161
284
|
try {
|
|
285
|
+
if (discountCode && discountInfo.isValid) {
|
|
286
|
+
return encodeFunctionData({
|
|
287
|
+
abi: [BUY_PACKS_FOR_WITH_DISCOUNT_ABI],
|
|
288
|
+
functionName: "buyPacksForWithDiscount",
|
|
289
|
+
args: [recipientAddress as `0x${string}`, BigInt(packId), BigInt(packAmount), discountCode],
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
162
293
|
return encodeFunctionData({
|
|
163
294
|
abi: [BUY_PACKS_FOR_ABI],
|
|
164
295
|
functionName: "buyPacksFor",
|
|
165
296
|
args: [recipientAddress as `0x${string}`, BigInt(packId), BigInt(packAmount)],
|
|
166
297
|
});
|
|
167
298
|
} catch (error) {
|
|
168
|
-
console.error("Failed to encode function data", { recipientAddress, packId, packAmount, error });
|
|
299
|
+
console.error("Failed to encode function data", { recipientAddress, packId, packAmount, discountCode, error });
|
|
169
300
|
return "0x";
|
|
170
301
|
}
|
|
171
|
-
}, [recipientAddress, packId, packAmount]);
|
|
302
|
+
}, [recipientAddress, packId, packAmount, discountCode, discountInfo.isValid]);
|
|
172
303
|
|
|
173
304
|
// Default header if not provided
|
|
174
305
|
const defaultHeader = () => (
|
|
@@ -182,6 +313,31 @@ export function AnySpendCollectorClubPurchase({
|
|
|
182
313
|
</div>
|
|
183
314
|
);
|
|
184
315
|
|
|
316
|
+
// Don't render AnySpendCustom while discount is being validated (avoids showing wrong price)
|
|
317
|
+
if (discountCode && discountInfo.isLoading) {
|
|
318
|
+
return (
|
|
319
|
+
<div className="mb-4 flex flex-col items-center gap-3 text-center">
|
|
320
|
+
<p className="text-as-secondary text-sm">Validating discount code...</p>
|
|
321
|
+
</div>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (discountCode && discountInfo.error) {
|
|
326
|
+
return (
|
|
327
|
+
<div className="mb-4 flex flex-col items-center gap-3 text-center">
|
|
328
|
+
<p className="text-sm text-red-500">{discountInfo.error}</p>
|
|
329
|
+
</div>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (discountCode && discountInfo.isValid && effectiveDstAmount === "0") {
|
|
334
|
+
return (
|
|
335
|
+
<div className="mb-4 flex flex-col items-center gap-3 text-center">
|
|
336
|
+
<p className="text-sm text-red-500">Discount exceeds total price</p>
|
|
337
|
+
</div>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
185
341
|
return (
|
|
186
342
|
<AnySpendCustom
|
|
187
343
|
loadOrder={loadOrder}
|
|
@@ -192,7 +348,7 @@ export function AnySpendCollectorClubPurchase({
|
|
|
192
348
|
orderType="custom"
|
|
193
349
|
dstChainId={BASE_CHAIN_ID}
|
|
194
350
|
dstToken={paymentToken}
|
|
195
|
-
dstAmount={
|
|
351
|
+
dstAmount={effectiveDstAmount}
|
|
196
352
|
contractAddress={ccShopAddress}
|
|
197
353
|
encodedData={encodedData}
|
|
198
354
|
metadata={{
|
|
@@ -201,6 +357,9 @@ export function AnySpendCollectorClubPurchase({
|
|
|
201
357
|
pricePerPack,
|
|
202
358
|
vendingMachineId,
|
|
203
359
|
packType,
|
|
360
|
+
...(discountCode && discountInfo.isValid
|
|
361
|
+
? { discountCode, discountAmount: discountInfo.discountAmount.toString() }
|
|
362
|
+
: {}),
|
|
204
363
|
}}
|
|
205
364
|
header={header || defaultHeader}
|
|
206
365
|
onSuccess={onSuccess}
|
|
@@ -497,6 +497,8 @@ export interface AnySpendCollectorClubPurchaseProps extends BaseModalProps {
|
|
|
497
497
|
forceFiatPayment?: boolean;
|
|
498
498
|
/** Staging environment support */
|
|
499
499
|
isStaging?: boolean;
|
|
500
|
+
/** Optional discount code to apply to the purchase */
|
|
501
|
+
discountCode?: string;
|
|
500
502
|
}
|
|
501
503
|
|
|
502
504
|
/**
|