@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.
@@ -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
- 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, }) {
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
- // Calculate fiat amount (totalAmount in USD, assuming USDC with 6 decimals)
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 (!totalAmount || totalAmount === "0")
163
+ if (!effectiveDstAmount || effectiveDstAmount === "0")
67
164
  return "0";
68
- return (0, number_1.formatUnits)(totalAmount, constants_1.USDC_BASE.decimals);
69
- }, [totalAmount]);
70
- // Encode the buyPacksFor function call
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
- 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: totalAmount, contractAddress: ccShopAddress, encodedData: encodedData, metadata: {
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
- 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, }) {
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
- // Calculate fiat amount (totalAmount in USD, assuming USDC with 6 decimals)
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 (!totalAmount || totalAmount === "0")
160
+ if (!effectiveDstAmount || effectiveDstAmount === "0")
64
161
  return "0";
65
- return formatUnits(totalAmount, USDC_BASE.decimals);
66
- }, [totalAmount]);
67
- // Encode the buyPacksFor function call
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
- return (_jsx(AnySpendCustom, { loadOrder: loadOrder, mode: mode, activeTab: activeTab, recipientAddress: recipientAddress, spenderAddress: spenderAddress ?? ccShopAddress, orderType: "custom", dstChainId: BASE_CHAIN_ID, dstToken: paymentToken, dstAmount: totalAmount, contractAddress: ccShopAddress, encodedData: encodedData, metadata: {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b3dotfun/sdk",
3
- "version": "0.1.65-alpha.3",
3
+ "version": "0.1.65-alpha.4",
4
4
  "source": "src/index.ts",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "react-native": "./dist/cjs/index.native.js",
@@ -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
- // Calculate fiat amount (totalAmount in USD, assuming USDC with 6 decimals)
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 (!totalAmount || totalAmount === "0") return "0";
156
- return formatUnits(totalAmount, USDC_BASE.decimals);
157
- }, [totalAmount]);
278
+ if (!effectiveDstAmount || effectiveDstAmount === "0") return "0";
279
+ return formatUnits(effectiveDstAmount, USDC_BASE.decimals);
280
+ }, [effectiveDstAmount]);
158
281
 
159
- // Encode the buyPacksFor function call
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={totalAmount}
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
  /**