@b3dotfun/sdk 0.1.65 → 0.1.66-alpha.1
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/AnySpend.d.ts +2 -0
- package/dist/cjs/anyspend/react/components/AnySpend.js +7 -16
- package/dist/cjs/anyspend/react/components/AnySpendCollectorClubPurchase.d.ts +6 -1
- package/dist/cjs/anyspend/react/components/AnySpendCollectorClubPurchase.js +151 -22
- package/dist/cjs/anyspend/react/components/AnySpendCustom.js +4 -50
- package/dist/cjs/anyspend/react/components/AnySpendCustomExactIn.d.ts +2 -0
- package/dist/cjs/anyspend/react/components/AnySpendCustomExactIn.js +4 -2
- package/dist/cjs/anyspend/react/components/AnySpendDeposit.d.ts +3 -1
- package/dist/cjs/anyspend/react/components/AnySpendDeposit.js +2 -2
- package/dist/cjs/anyspend/react/components/AnySpendWorkflowTrigger.d.ts +31 -0
- package/dist/cjs/anyspend/react/components/AnySpendWorkflowTrigger.js +14 -0
- package/dist/cjs/anyspend/react/components/QRDeposit.js +5 -13
- package/dist/cjs/anyspend/react/components/ccShopAbi.d.ts +113 -0
- package/dist/cjs/anyspend/react/components/ccShopAbi.js +63 -0
- package/dist/cjs/anyspend/react/components/common/CryptoPaySection.d.ts +1 -3
- package/dist/cjs/anyspend/react/components/common/CryptoPaySection.js +3 -3
- package/dist/cjs/anyspend/react/components/common/OrderTokenAmount.d.ts +1 -4
- package/dist/cjs/anyspend/react/components/common/OrderTokenAmount.js +3 -57
- package/dist/cjs/anyspend/react/components/common/PaySection.js +1 -1
- package/dist/cjs/anyspend/react/components/index.d.ts +2 -0
- package/dist/cjs/anyspend/react/components/index.js +3 -1
- package/dist/cjs/anyspend/react/hooks/index.d.ts +1 -0
- package/dist/cjs/anyspend/react/hooks/index.js +1 -0
- package/dist/cjs/anyspend/react/hooks/useAnyspendCreateOnrampOrder.js +1 -0
- package/dist/cjs/anyspend/react/hooks/useAnyspendCreateOrder.d.ts +1 -0
- package/dist/cjs/anyspend/react/hooks/useAnyspendCreateOrder.js +1 -0
- package/dist/cjs/anyspend/react/hooks/useOnOrderSuccess.d.ts +10 -0
- package/dist/cjs/anyspend/react/hooks/useOnOrderSuccess.js +27 -0
- package/dist/cjs/anyspend/services/anyspend.d.ts +2 -1
- package/dist/cjs/anyspend/services/anyspend.js +2 -1
- package/dist/cjs/anyspend/utils/chain.d.ts +1 -1
- package/dist/cjs/anyspend/utils/chain.js +72 -62
- package/dist/cjs/app.shared.js +8 -0
- package/dist/cjs/global-account/react/components/B3DynamicModal.js +4 -0
- package/dist/cjs/global-account/react/hooks/useFirstEOA.d.ts +4 -4
- package/dist/cjs/global-account/react/hooks/useUserQuery.js +10 -0
- package/dist/cjs/global-account/react/stores/useModalStore.d.ts +37 -1
- package/dist/cjs/global-account/react/stores/userStore.js +1 -0
- package/dist/esm/anyspend/react/components/AnySpend.d.ts +2 -0
- package/dist/esm/anyspend/react/components/AnySpend.js +7 -16
- package/dist/esm/anyspend/react/components/AnySpendCollectorClubPurchase.d.ts +6 -1
- package/dist/esm/anyspend/react/components/AnySpendCollectorClubPurchase.js +152 -23
- package/dist/esm/anyspend/react/components/AnySpendCustom.js +4 -17
- package/dist/esm/anyspend/react/components/AnySpendCustomExactIn.d.ts +2 -0
- package/dist/esm/anyspend/react/components/AnySpendCustomExactIn.js +4 -2
- package/dist/esm/anyspend/react/components/AnySpendDeposit.d.ts +3 -1
- package/dist/esm/anyspend/react/components/AnySpendDeposit.js +2 -2
- package/dist/esm/anyspend/react/components/AnySpendWorkflowTrigger.d.ts +31 -0
- package/dist/esm/anyspend/react/components/AnySpendWorkflowTrigger.js +11 -0
- package/dist/esm/anyspend/react/components/QRDeposit.js +6 -14
- package/dist/esm/anyspend/react/components/ccShopAbi.d.ts +113 -0
- package/dist/esm/anyspend/react/components/ccShopAbi.js +60 -0
- package/dist/esm/anyspend/react/components/common/CryptoPaySection.d.ts +1 -3
- package/dist/esm/anyspend/react/components/common/CryptoPaySection.js +3 -3
- package/dist/esm/anyspend/react/components/common/OrderTokenAmount.d.ts +1 -4
- package/dist/esm/anyspend/react/components/common/OrderTokenAmount.js +2 -56
- package/dist/esm/anyspend/react/components/common/PaySection.js +1 -1
- package/dist/esm/anyspend/react/components/index.d.ts +2 -0
- package/dist/esm/anyspend/react/components/index.js +1 -0
- package/dist/esm/anyspend/react/hooks/index.d.ts +1 -0
- package/dist/esm/anyspend/react/hooks/index.js +1 -0
- package/dist/esm/anyspend/react/hooks/useAnyspendCreateOnrampOrder.js +1 -0
- package/dist/esm/anyspend/react/hooks/useAnyspendCreateOrder.d.ts +1 -0
- package/dist/esm/anyspend/react/hooks/useAnyspendCreateOrder.js +1 -0
- package/dist/esm/anyspend/react/hooks/useOnOrderSuccess.d.ts +10 -0
- package/dist/esm/anyspend/react/hooks/useOnOrderSuccess.js +24 -0
- package/dist/esm/anyspend/services/anyspend.d.ts +2 -1
- package/dist/esm/anyspend/services/anyspend.js +2 -1
- package/dist/esm/anyspend/utils/chain.d.ts +1 -1
- package/dist/esm/anyspend/utils/chain.js +72 -62
- package/dist/esm/app.shared.js +8 -0
- package/dist/esm/global-account/react/components/B3DynamicModal.js +4 -0
- package/dist/esm/global-account/react/hooks/useFirstEOA.d.ts +4 -4
- package/dist/esm/global-account/react/hooks/useUserQuery.js +11 -1
- package/dist/esm/global-account/react/stores/useModalStore.d.ts +37 -1
- package/dist/esm/global-account/react/stores/userStore.js +1 -0
- package/dist/types/anyspend/react/components/AnySpend.d.ts +2 -0
- package/dist/types/anyspend/react/components/AnySpendCollectorClubPurchase.d.ts +6 -1
- package/dist/types/anyspend/react/components/AnySpendCustomExactIn.d.ts +2 -0
- package/dist/types/anyspend/react/components/AnySpendDeposit.d.ts +3 -1
- package/dist/types/anyspend/react/components/AnySpendWorkflowTrigger.d.ts +31 -0
- package/dist/types/anyspend/react/components/ccShopAbi.d.ts +113 -0
- package/dist/types/anyspend/react/components/common/CryptoPaySection.d.ts +1 -3
- package/dist/types/anyspend/react/components/common/OrderTokenAmount.d.ts +1 -4
- package/dist/types/anyspend/react/components/index.d.ts +2 -0
- package/dist/types/anyspend/react/hooks/index.d.ts +1 -0
- package/dist/types/anyspend/react/hooks/useAnyspendCreateOrder.d.ts +1 -0
- package/dist/types/anyspend/react/hooks/useOnOrderSuccess.d.ts +10 -0
- package/dist/types/anyspend/services/anyspend.d.ts +2 -1
- package/dist/types/anyspend/utils/chain.d.ts +1 -1
- package/dist/types/global-account/react/hooks/useFirstEOA.d.ts +4 -4
- package/dist/types/global-account/react/stores/useModalStore.d.ts +37 -1
- package/package.json +1 -1
- package/src/anyspend/README.md +14 -0
- package/src/anyspend/docs/checkout-sessions.md +228 -0
- package/src/anyspend/docs/components.md +26 -0
- package/src/anyspend/docs/examples.md +58 -0
- package/src/anyspend/docs/hooks.md +32 -0
- package/src/anyspend/llms.txt +185 -0
- package/src/anyspend/react/components/AnySpend.tsx +9 -17
- package/src/anyspend/react/components/AnySpendCollectorClubPurchase.tsx +206 -22
- package/src/anyspend/react/components/AnySpendCustom.tsx +3 -18
- package/src/anyspend/react/components/AnySpendCustomExactIn.tsx +5 -1
- package/src/anyspend/react/components/AnySpendDeposit.tsx +5 -0
- package/src/anyspend/react/components/AnySpendWorkflowTrigger.tsx +73 -0
- package/src/anyspend/react/components/QRDeposit.tsx +19 -15
- package/src/anyspend/react/components/ccShopAbi.ts +64 -0
- package/src/anyspend/react/components/common/CryptoPaySection.tsx +0 -5
- package/src/anyspend/react/components/common/OrderTokenAmount.tsx +1 -70
- package/src/anyspend/react/components/common/PaySection.tsx +0 -1
- package/src/anyspend/react/components/index.ts +2 -0
- package/src/anyspend/react/hooks/index.ts +1 -0
- package/src/anyspend/react/hooks/useAnyspendCreateOnrampOrder.ts +1 -0
- package/src/anyspend/react/hooks/useAnyspendCreateOrder.ts +2 -0
- package/src/anyspend/react/hooks/useOnOrderSuccess.ts +36 -0
- package/src/anyspend/services/anyspend.ts +3 -0
- package/src/anyspend/utils/chain.ts +81 -65
- package/src/app.shared.ts +11 -0
- package/src/global-account/react/components/B3DynamicModal.tsx +4 -0
- package/src/global-account/react/hooks/useUserQuery.ts +12 -1
- package/src/global-account/react/stores/useModalStore.ts +39 -2
- package/src/global-account/react/stores/userStore.ts +1 -0
|
@@ -27,28 +27,28 @@
|
|
|
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";
|
|
36
|
+
import {
|
|
37
|
+
BUY_PACKS_FOR_ABI,
|
|
38
|
+
BUY_PACKS_FOR_WITH_DISCOUNT_ABI,
|
|
39
|
+
GET_DISCOUNT_CODE_ABI,
|
|
40
|
+
IS_DISCOUNT_CODE_VALID_FOR_PACK_ABI,
|
|
41
|
+
} from "./ccShopAbi";
|
|
34
42
|
|
|
35
43
|
// Collector Club Shop contract addresses on Base
|
|
36
44
|
const CC_SHOP_ADDRESS = "0x47366E64E4917dd4DdC04Fb9DC507c1dD2b87294";
|
|
37
45
|
const CC_SHOP_ADDRESS_STAGING = "0x8b751143342ac41eB965E55430e3F7Adf6BE01fA";
|
|
38
46
|
const BASE_CHAIN_ID = 8453;
|
|
39
47
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
{ internalType: "uint256", name: "packId", type: "uint256" },
|
|
45
|
-
{ internalType: "uint256", name: "amount", type: "uint256" },
|
|
46
|
-
],
|
|
47
|
-
name: "buyPacksFor",
|
|
48
|
-
outputs: [],
|
|
49
|
-
stateMutability: "nonpayable",
|
|
50
|
-
type: "function",
|
|
51
|
-
} as const;
|
|
48
|
+
const basePublicClient = createPublicClient({
|
|
49
|
+
chain: base,
|
|
50
|
+
transport: http(PUBLIC_BASE_RPC_URL),
|
|
51
|
+
});
|
|
52
52
|
|
|
53
53
|
export interface AnySpendCollectorClubPurchaseProps {
|
|
54
54
|
/**
|
|
@@ -118,6 +118,11 @@ export interface AnySpendCollectorClubPurchaseProps {
|
|
|
118
118
|
* Force fiat payment
|
|
119
119
|
*/
|
|
120
120
|
forceFiatPayment?: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Optional discount code to apply to the purchase.
|
|
123
|
+
* When provided, validates on-chain and adjusts the price accordingly.
|
|
124
|
+
*/
|
|
125
|
+
discountCode?: string;
|
|
121
126
|
}
|
|
122
127
|
|
|
123
128
|
export function AnySpendCollectorClubPurchase({
|
|
@@ -137,6 +142,7 @@ export function AnySpendCollectorClubPurchase({
|
|
|
137
142
|
vendingMachineId,
|
|
138
143
|
packType,
|
|
139
144
|
forceFiatPayment,
|
|
145
|
+
discountCode,
|
|
140
146
|
}: AnySpendCollectorClubPurchaseProps) {
|
|
141
147
|
const ccShopAddress = isStaging ? CC_SHOP_ADDRESS_STAGING : CC_SHOP_ADDRESS;
|
|
142
148
|
|
|
@@ -150,25 +156,159 @@ export function AnySpendCollectorClubPurchase({
|
|
|
150
156
|
}
|
|
151
157
|
}, [pricePerPack, packAmount]);
|
|
152
158
|
|
|
153
|
-
//
|
|
159
|
+
// Discount code validation state
|
|
160
|
+
const [discountInfo, setDiscountInfo] = useState<{
|
|
161
|
+
isValid: boolean;
|
|
162
|
+
discountAmount: bigint;
|
|
163
|
+
minPurchaseAmount: bigint;
|
|
164
|
+
isLoading: boolean;
|
|
165
|
+
error: string | null;
|
|
166
|
+
}>({
|
|
167
|
+
isValid: false,
|
|
168
|
+
discountAmount: BigInt(0),
|
|
169
|
+
minPurchaseAmount: BigInt(0),
|
|
170
|
+
isLoading: false,
|
|
171
|
+
error: null,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Validate discount code on-chain when provided
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (!discountCode) {
|
|
177
|
+
setDiscountInfo({
|
|
178
|
+
isValid: false,
|
|
179
|
+
discountAmount: BigInt(0),
|
|
180
|
+
minPurchaseAmount: BigInt(0),
|
|
181
|
+
isLoading: false,
|
|
182
|
+
error: null,
|
|
183
|
+
});
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let cancelled = false;
|
|
188
|
+
|
|
189
|
+
const validateDiscount = async () => {
|
|
190
|
+
setDiscountInfo(prev => ({ ...prev, isLoading: true, error: null }));
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
// Validate against specific pack and fetch full details in parallel
|
|
194
|
+
const [validForPack, codeDetails] = await Promise.all([
|
|
195
|
+
basePublicClient.readContract({
|
|
196
|
+
address: ccShopAddress as `0x${string}`,
|
|
197
|
+
abi: [IS_DISCOUNT_CODE_VALID_FOR_PACK_ABI],
|
|
198
|
+
functionName: "isDiscountCodeValidForPack",
|
|
199
|
+
args: [discountCode, BigInt(packId)],
|
|
200
|
+
}),
|
|
201
|
+
basePublicClient.readContract({
|
|
202
|
+
address: ccShopAddress as `0x${string}`,
|
|
203
|
+
abi: [GET_DISCOUNT_CODE_ABI],
|
|
204
|
+
functionName: "getDiscountCode",
|
|
205
|
+
args: [discountCode],
|
|
206
|
+
}),
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
if (cancelled) return;
|
|
210
|
+
|
|
211
|
+
const [isValid, discountAmount] = validForPack;
|
|
212
|
+
const { minPurchaseAmount, packId: restrictedPackId, exists } = codeDetails;
|
|
213
|
+
|
|
214
|
+
if (!exists) {
|
|
215
|
+
setDiscountInfo({
|
|
216
|
+
isValid: false,
|
|
217
|
+
discountAmount: BigInt(0),
|
|
218
|
+
minPurchaseAmount: BigInt(0),
|
|
219
|
+
isLoading: false,
|
|
220
|
+
error: "Discount code does not exist",
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!isValid) {
|
|
226
|
+
// Provide specific error based on code details
|
|
227
|
+
if (restrictedPackId !== BigInt(0) && restrictedPackId !== BigInt(packId)) {
|
|
228
|
+
setDiscountInfo({
|
|
229
|
+
isValid: false,
|
|
230
|
+
discountAmount: BigInt(0),
|
|
231
|
+
minPurchaseAmount: BigInt(0),
|
|
232
|
+
isLoading: false,
|
|
233
|
+
error: "Discount code is not valid for this pack",
|
|
234
|
+
});
|
|
235
|
+
} else {
|
|
236
|
+
setDiscountInfo({
|
|
237
|
+
isValid: false,
|
|
238
|
+
discountAmount: BigInt(0),
|
|
239
|
+
minPurchaseAmount: BigInt(0),
|
|
240
|
+
isLoading: false,
|
|
241
|
+
error: "Invalid or expired discount code",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
setDiscountInfo({ isValid: true, discountAmount, minPurchaseAmount, isLoading: false, error: null });
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (cancelled) return;
|
|
250
|
+
console.error("Failed to validate discount code", { discountCode, error });
|
|
251
|
+
setDiscountInfo({
|
|
252
|
+
isValid: false,
|
|
253
|
+
discountAmount: BigInt(0),
|
|
254
|
+
minPurchaseAmount: BigInt(0),
|
|
255
|
+
isLoading: false,
|
|
256
|
+
error: "Failed to validate discount code",
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
validateDiscount();
|
|
262
|
+
|
|
263
|
+
return () => {
|
|
264
|
+
cancelled = true;
|
|
265
|
+
};
|
|
266
|
+
}, [discountCode, ccShopAddress, packId]);
|
|
267
|
+
|
|
268
|
+
// Calculate effective dstAmount after discount
|
|
269
|
+
const effectiveDstAmount = useMemo(() => {
|
|
270
|
+
if (!discountCode || !discountInfo.isValid || discountInfo.discountAmount === BigInt(0)) {
|
|
271
|
+
return totalAmount;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const total = BigInt(totalAmount);
|
|
275
|
+
const discount = discountInfo.discountAmount;
|
|
276
|
+
|
|
277
|
+
if (discount >= total) {
|
|
278
|
+
console.error("Discount exceeds total price", { totalAmount, discountAmount: discount.toString() });
|
|
279
|
+
return "0";
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return (total - discount).toString();
|
|
283
|
+
}, [totalAmount, discountCode, discountInfo.isValid, discountInfo.discountAmount]);
|
|
284
|
+
|
|
285
|
+
// Calculate fiat amount (effectiveDstAmount in USD, assuming USDC with 6 decimals)
|
|
154
286
|
const srcFiatAmount = useMemo(() => {
|
|
155
|
-
if (!
|
|
156
|
-
return formatUnits(
|
|
157
|
-
}, [
|
|
287
|
+
if (!effectiveDstAmount || effectiveDstAmount === "0") return "0";
|
|
288
|
+
return formatUnits(effectiveDstAmount, USDC_BASE.decimals);
|
|
289
|
+
}, [effectiveDstAmount]);
|
|
158
290
|
|
|
159
|
-
// Encode the
|
|
291
|
+
// Encode the contract function call (with or without discount)
|
|
160
292
|
const encodedData = useMemo(() => {
|
|
161
293
|
try {
|
|
294
|
+
if (discountCode && discountInfo.isValid) {
|
|
295
|
+
return encodeFunctionData({
|
|
296
|
+
abi: [BUY_PACKS_FOR_WITH_DISCOUNT_ABI],
|
|
297
|
+
functionName: "buyPacksForWithDiscount",
|
|
298
|
+
args: [recipientAddress as `0x${string}`, BigInt(packId), BigInt(packAmount), discountCode],
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
162
302
|
return encodeFunctionData({
|
|
163
303
|
abi: [BUY_PACKS_FOR_ABI],
|
|
164
304
|
functionName: "buyPacksFor",
|
|
165
305
|
args: [recipientAddress as `0x${string}`, BigInt(packId), BigInt(packAmount)],
|
|
166
306
|
});
|
|
167
307
|
} catch (error) {
|
|
168
|
-
console.error("Failed to encode function data", { recipientAddress, packId, packAmount, error });
|
|
308
|
+
console.error("Failed to encode function data", { recipientAddress, packId, packAmount, discountCode, error });
|
|
169
309
|
return "0x";
|
|
170
310
|
}
|
|
171
|
-
}, [recipientAddress, packId, packAmount]);
|
|
311
|
+
}, [recipientAddress, packId, packAmount, discountCode, discountInfo.isValid]);
|
|
172
312
|
|
|
173
313
|
// Default header if not provided
|
|
174
314
|
const defaultHeader = () => (
|
|
@@ -182,6 +322,47 @@ export function AnySpendCollectorClubPurchase({
|
|
|
182
322
|
</div>
|
|
183
323
|
);
|
|
184
324
|
|
|
325
|
+
// Don't render AnySpendCustom while discount is being validated (avoids showing wrong price)
|
|
326
|
+
if (discountCode && discountInfo.isLoading) {
|
|
327
|
+
return (
|
|
328
|
+
<div className="mb-4 flex flex-col items-center gap-3 text-center">
|
|
329
|
+
<p className="text-as-secondary text-sm">Validating discount code...</p>
|
|
330
|
+
</div>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (discountCode && discountInfo.error) {
|
|
335
|
+
return (
|
|
336
|
+
<div className="mb-4 flex flex-col items-center gap-3 text-center">
|
|
337
|
+
<p className="text-sm text-red-500">{discountInfo.error}</p>
|
|
338
|
+
</div>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (
|
|
343
|
+
discountCode &&
|
|
344
|
+
discountInfo.isValid &&
|
|
345
|
+
discountInfo.minPurchaseAmount > BigInt(0) &&
|
|
346
|
+
BigInt(packAmount) < discountInfo.minPurchaseAmount
|
|
347
|
+
) {
|
|
348
|
+
return (
|
|
349
|
+
<div className="mb-4 flex flex-col items-center gap-3 text-center">
|
|
350
|
+
<p className="text-sm text-red-500">
|
|
351
|
+
Minimum purchase of {discountInfo.minPurchaseAmount.toString()} pack
|
|
352
|
+
{discountInfo.minPurchaseAmount > BigInt(1) ? "s" : ""} required for this discount code
|
|
353
|
+
</p>
|
|
354
|
+
</div>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (discountCode && discountInfo.isValid && effectiveDstAmount === "0") {
|
|
359
|
+
return (
|
|
360
|
+
<div className="mb-4 flex flex-col items-center gap-3 text-center">
|
|
361
|
+
<p className="text-sm text-red-500">Discount exceeds total price</p>
|
|
362
|
+
</div>
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
185
366
|
return (
|
|
186
367
|
<AnySpendCustom
|
|
187
368
|
loadOrder={loadOrder}
|
|
@@ -192,7 +373,7 @@ export function AnySpendCollectorClubPurchase({
|
|
|
192
373
|
orderType="custom"
|
|
193
374
|
dstChainId={BASE_CHAIN_ID}
|
|
194
375
|
dstToken={paymentToken}
|
|
195
|
-
dstAmount={
|
|
376
|
+
dstAmount={effectiveDstAmount}
|
|
196
377
|
contractAddress={ccShopAddress}
|
|
197
378
|
encodedData={encodedData}
|
|
198
379
|
metadata={{
|
|
@@ -201,6 +382,9 @@ export function AnySpendCollectorClubPurchase({
|
|
|
201
382
|
pricePerPack,
|
|
202
383
|
vendingMachineId,
|
|
203
384
|
packType,
|
|
385
|
+
...(discountCode && discountInfo.isValid
|
|
386
|
+
? { discountCode, discountAmount: discountInfo.discountAmount.toString() }
|
|
387
|
+
: {}),
|
|
204
388
|
}}
|
|
205
389
|
header={header || defaultHeader}
|
|
206
390
|
onSuccess={onSuccess}
|
|
@@ -45,6 +45,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
|
45
45
|
|
|
46
46
|
import { base } from "viem/chains";
|
|
47
47
|
import { useCryptoPaymentMethodState } from "../hooks/useCryptoPaymentMethodState";
|
|
48
|
+
import { useOnOrderSuccess } from "../hooks/useOnOrderSuccess";
|
|
48
49
|
import { useRecipientAddressState } from "../hooks/useRecipientAddressState";
|
|
49
50
|
import { AnySpendFingerprintWrapper, getFingerprintConfig } from "./AnySpendFingerprintWrapper";
|
|
50
51
|
import { CryptoPaymentMethod, CryptoPaymentMethodType } from "./common/CryptoPaymentMethod";
|
|
@@ -274,9 +275,6 @@ function AnySpendCustomInner({
|
|
|
274
275
|
|
|
275
276
|
const [orderId, setOrderId] = useState<string | undefined>(loadOrder);
|
|
276
277
|
|
|
277
|
-
// Track if onSuccess has been called for the current order
|
|
278
|
-
const onSuccessCalled = React.useRef(false);
|
|
279
|
-
|
|
280
278
|
const [srcChainId, setSrcChainId] = useState<number>(base.id);
|
|
281
279
|
|
|
282
280
|
// Get token list for token balance check
|
|
@@ -433,21 +431,8 @@ function AnySpendCustomInner({
|
|
|
433
431
|
const { geoData, isOnrampSupported, coinbaseAvailablePaymentMethods, stripeOnrampSupport, stripeWeb2Support } =
|
|
434
432
|
useGeoOnrampOptions(srcFiatAmountForGeoCheck);
|
|
435
433
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
console.log("Calling onSuccess");
|
|
439
|
-
const relayTxs = oat?.data?.relayTxs;
|
|
440
|
-
const lastRelayTxHash = relayTxs?.[relayTxs.length - 1]?.txHash;
|
|
441
|
-
const txHash = oat?.data?.executeTx?.txHash || lastRelayTxHash;
|
|
442
|
-
onSuccess?.(txHash);
|
|
443
|
-
onSuccessCalled.current = true;
|
|
444
|
-
}
|
|
445
|
-
}, [oat?.data?.order.status, oat?.data?.executeTx?.txHash, oat?.data?.relayTxs, onSuccess]);
|
|
446
|
-
|
|
447
|
-
// Reset flag when orderId changes
|
|
448
|
-
useEffect(() => {
|
|
449
|
-
onSuccessCalled.current = false;
|
|
450
|
-
}, [orderId]);
|
|
434
|
+
// Call onSuccess when order is executed
|
|
435
|
+
useOnOrderSuccess({ orderData: oat, orderId, onSuccess });
|
|
451
436
|
|
|
452
437
|
const { createOrder: createRegularOrder, isCreatingOrder: isCreatingRegularOrder } = useAnyspendCreateOrder({
|
|
453
438
|
onSuccess: data => {
|
|
@@ -83,6 +83,8 @@ export interface AnySpendCustomExactInProps {
|
|
|
83
83
|
classes?: AnySpendCustomExactInClasses;
|
|
84
84
|
/** When true, allows direct transfer without swap if source and destination token/chain are the same */
|
|
85
85
|
allowDirectTransfer?: boolean;
|
|
86
|
+
/** Opaque metadata passed to the order for callbacks (e.g., workflow form data) */
|
|
87
|
+
callbackMetadata?: Record<string, unknown>;
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
export function AnySpendCustomExactIn(props: AnySpendCustomExactInProps) {
|
|
@@ -120,6 +122,7 @@ function AnySpendCustomExactInInner({
|
|
|
120
122
|
returnHomeLabel,
|
|
121
123
|
classes,
|
|
122
124
|
allowDirectTransfer = false,
|
|
125
|
+
callbackMetadata,
|
|
123
126
|
}: AnySpendCustomExactInProps) {
|
|
124
127
|
const actionLabel = customExactInConfig?.action ?? "Custom Execution";
|
|
125
128
|
const setB3ModalOpen = useModalStore(state => state.setB3ModalOpen);
|
|
@@ -418,7 +421,6 @@ function AnySpendCustomExactInInner({
|
|
|
418
421
|
onSelectCryptoPaymentMethod={() => setActivePanel(PanelView.CRYPTO_PAYMENT_METHOD)}
|
|
419
422
|
anyspendQuote={anyspendQuote}
|
|
420
423
|
onTokenSelect={onTokenSelect}
|
|
421
|
-
skipAutoMaxOnTokenChange={!!destinationTokenAmount}
|
|
422
424
|
/>
|
|
423
425
|
) : (
|
|
424
426
|
<motion.div
|
|
@@ -587,6 +589,7 @@ function AnySpendCustomExactInInner({
|
|
|
587
589
|
? normalizeAddress(customExactInConfig.spenderAddress)
|
|
588
590
|
: undefined,
|
|
589
591
|
},
|
|
592
|
+
callbackMetadata,
|
|
590
593
|
});
|
|
591
594
|
} else {
|
|
592
595
|
// EXACT_INPUT mode: create custom_exact_in order (original behavior)
|
|
@@ -604,6 +607,7 @@ function AnySpendCustomExactInInner({
|
|
|
604
607
|
expectedDstAmount: expectedDstAmountRaw,
|
|
605
608
|
creatorAddress: globalAddress,
|
|
606
609
|
payload,
|
|
610
|
+
callbackMetadata,
|
|
607
611
|
});
|
|
608
612
|
}
|
|
609
613
|
} catch (err: any) {
|
|
@@ -123,6 +123,8 @@ export interface AnySpendDepositProps {
|
|
|
123
123
|
allowDirectTransfer?: boolean;
|
|
124
124
|
/** Fixed destination token amount (in wei/smallest unit). When provided, user cannot change the amount. */
|
|
125
125
|
destinationTokenAmount?: string;
|
|
126
|
+
/** Opaque metadata passed to the order for callbacks (e.g., workflow form data) */
|
|
127
|
+
callbackMetadata?: Record<string, unknown>;
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
// Default supported chains
|
|
@@ -248,6 +250,7 @@ export function AnySpendDeposit({
|
|
|
248
250
|
classes,
|
|
249
251
|
allowDirectTransfer = false,
|
|
250
252
|
destinationTokenAmount,
|
|
253
|
+
callbackMetadata,
|
|
251
254
|
}: AnySpendDepositProps) {
|
|
252
255
|
// Extract deposit-specific classes for convenience
|
|
253
256
|
const depositClasses = classes?.deposit;
|
|
@@ -697,6 +700,7 @@ export function AnySpendDeposit({
|
|
|
697
700
|
classes={classes?.customExactIn}
|
|
698
701
|
allowDirectTransfer={allowDirectTransfer}
|
|
699
702
|
destinationTokenAmount={destinationTokenAmount}
|
|
703
|
+
callbackMetadata={callbackMetadata}
|
|
700
704
|
/>
|
|
701
705
|
) : (
|
|
702
706
|
<AnySpend
|
|
@@ -720,6 +724,7 @@ export function AnySpendDeposit({
|
|
|
720
724
|
classes={classes?.anySpend}
|
|
721
725
|
allowDirectTransfer={allowDirectTransfer}
|
|
722
726
|
destinationTokenAmount={destinationTokenAmount}
|
|
727
|
+
callbackMetadata={callbackMetadata}
|
|
723
728
|
/>
|
|
724
729
|
)}
|
|
725
730
|
</div>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { AnySpendAllClasses } from "./types/classes";
|
|
3
|
+
import { AnySpendDeposit } from "./AnySpendDeposit";
|
|
4
|
+
|
|
5
|
+
export interface AnySpendWorkflowTriggerProps {
|
|
6
|
+
/** Payment recipient address (hex) */
|
|
7
|
+
recipientAddress: string;
|
|
8
|
+
/** Destination chain ID */
|
|
9
|
+
chainId: number;
|
|
10
|
+
/** Destination token address */
|
|
11
|
+
tokenAddress: string;
|
|
12
|
+
/** Required payment amount in token base units (wei) */
|
|
13
|
+
amount: string;
|
|
14
|
+
/** Workflow ID to trigger */
|
|
15
|
+
workflowId: string;
|
|
16
|
+
/** Organization ID that owns the workflow */
|
|
17
|
+
orgId: string;
|
|
18
|
+
/** Optional callback metadata merged into the order */
|
|
19
|
+
callbackMetadata?: {
|
|
20
|
+
/** Passed as trigger result inputs — accessible via {{root.result.inputs.*}} */
|
|
21
|
+
inputs?: Record<string, unknown>;
|
|
22
|
+
} & Record<string, unknown>;
|
|
23
|
+
/** Callback when payment succeeds */
|
|
24
|
+
onSuccess?: (amount: string) => void;
|
|
25
|
+
/** Callback when modal is closed */
|
|
26
|
+
onClose?: () => void;
|
|
27
|
+
/** Display mode */
|
|
28
|
+
mode?: "modal" | "page";
|
|
29
|
+
/** Custom action label */
|
|
30
|
+
actionLabel?: string;
|
|
31
|
+
/** Custom class names */
|
|
32
|
+
classes?: AnySpendAllClasses;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function AnySpendWorkflowTrigger({
|
|
36
|
+
recipientAddress,
|
|
37
|
+
chainId,
|
|
38
|
+
tokenAddress,
|
|
39
|
+
amount,
|
|
40
|
+
workflowId,
|
|
41
|
+
orgId,
|
|
42
|
+
callbackMetadata,
|
|
43
|
+
onSuccess,
|
|
44
|
+
onClose,
|
|
45
|
+
mode,
|
|
46
|
+
actionLabel,
|
|
47
|
+
classes,
|
|
48
|
+
}: AnySpendWorkflowTriggerProps) {
|
|
49
|
+
const metadata = useMemo(
|
|
50
|
+
() => ({
|
|
51
|
+
workflowId,
|
|
52
|
+
orgId,
|
|
53
|
+
...callbackMetadata,
|
|
54
|
+
}),
|
|
55
|
+
[workflowId, orgId, callbackMetadata],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<AnySpendDeposit
|
|
60
|
+
recipientAddress={recipientAddress}
|
|
61
|
+
destinationTokenAddress={tokenAddress}
|
|
62
|
+
destinationTokenChainId={chainId}
|
|
63
|
+
destinationTokenAmount={amount}
|
|
64
|
+
callbackMetadata={metadata}
|
|
65
|
+
onSuccess={onSuccess}
|
|
66
|
+
onClose={onClose}
|
|
67
|
+
mode={mode}
|
|
68
|
+
actionLabel={actionLabel}
|
|
69
|
+
classes={classes}
|
|
70
|
+
allowDirectTransfer
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ALL_CHAINS,
|
|
3
|
+
getAvailableChainIds,
|
|
4
|
+
getPaymentUrl,
|
|
5
|
+
isSameChainAndToken,
|
|
6
|
+
ZERO_ADDRESS,
|
|
7
|
+
} from "@b3dotfun/sdk/anyspend";
|
|
2
8
|
import { components } from "@b3dotfun/sdk/anyspend/types/api";
|
|
3
9
|
import { Button, toast } from "@b3dotfun/sdk/global-account/react";
|
|
4
10
|
import { cn } from "@b3dotfun/sdk/shared/utils/cn";
|
|
@@ -8,6 +14,7 @@ import { QRCodeSVG } from "qrcode.react";
|
|
|
8
14
|
import { useEffect, useRef, useState } from "react";
|
|
9
15
|
import { useAnyspendOrderAndTransactions } from "../hooks/useAnyspendOrderAndTransactions";
|
|
10
16
|
import { useCreateDepositFirstOrder } from "../hooks/useCreateDepositFirstOrder";
|
|
17
|
+
import { useOnOrderSuccess } from "../hooks/useOnOrderSuccess";
|
|
11
18
|
import { TransferResult, useWatchTransfer } from "../hooks/useWatchTransfer";
|
|
12
19
|
import { DepositContractConfig } from "./AnySpendDeposit";
|
|
13
20
|
import { ChainTokenIcon } from "./common/ChainTokenIcon";
|
|
@@ -90,7 +97,6 @@ export function QRDeposit({
|
|
|
90
97
|
const [orderId, setOrderId] = useState<string | undefined>();
|
|
91
98
|
const [globalAddress, setGlobalAddress] = useState<string | undefined>();
|
|
92
99
|
const orderCreatedRef = useRef(false);
|
|
93
|
-
const onSuccessCalled = useRef(false);
|
|
94
100
|
const [transferResult, setTransferResult] = useState<TransferResult | null>(null);
|
|
95
101
|
|
|
96
102
|
// Source token/chain as state (can be changed by user)
|
|
@@ -189,22 +195,20 @@ export function QRDeposit({
|
|
|
189
195
|
]);
|
|
190
196
|
|
|
191
197
|
// Call onSuccess when order is executed
|
|
192
|
-
|
|
193
|
-
if (oat?.data?.order.status === "executed" && !onSuccessCalled.current) {
|
|
194
|
-
const txHash = oat?.data?.executeTx?.txHash;
|
|
195
|
-
onSuccess?.(txHash);
|
|
196
|
-
onSuccessCalled.current = true;
|
|
197
|
-
}
|
|
198
|
-
}, [oat?.data?.order.status, oat?.data?.executeTx?.txHash, onSuccess]);
|
|
199
|
-
|
|
200
|
-
// Reset onSuccess flag when orderId changes
|
|
201
|
-
useEffect(() => {
|
|
202
|
-
onSuccessCalled.current = false;
|
|
203
|
-
}, [orderId]);
|
|
198
|
+
useOnOrderSuccess({ orderData: oat, orderId, onSuccess });
|
|
204
199
|
|
|
205
200
|
// For pure transfers, always use recipient address; for orders, use global address
|
|
206
201
|
const displayAddress = isPureTransfer ? recipientAddress : globalAddress || recipientAddress;
|
|
207
202
|
|
|
203
|
+
// Generate EIP-681 payment URI for the QR code so wallets know which chain/token to use
|
|
204
|
+
const qrValue = getPaymentUrl(
|
|
205
|
+
displayAddress,
|
|
206
|
+
undefined,
|
|
207
|
+
sourceToken.address === ZERO_ADDRESS ? "ETH" : sourceToken.address,
|
|
208
|
+
sourceChainId,
|
|
209
|
+
sourceToken.decimals,
|
|
210
|
+
);
|
|
211
|
+
|
|
208
212
|
const handleCopyAddress = async () => {
|
|
209
213
|
if (displayAddress) {
|
|
210
214
|
await navigator.clipboard.writeText(displayAddress);
|
|
@@ -387,7 +391,7 @@ export function QRDeposit({
|
|
|
387
391
|
{/* QR Code */}
|
|
388
392
|
<div className={classes?.qrCodeContainer || "anyspend-qr-code-container flex flex-col items-center gap-2"}>
|
|
389
393
|
<div className={classes?.qrCode || "anyspend-qr-code rounded-lg bg-white p-2"}>
|
|
390
|
-
<QRCodeSVG value={
|
|
394
|
+
<QRCodeSVG value={qrValue} size={120} level="M" marginSize={0} />
|
|
391
395
|
</div>
|
|
392
396
|
<span className={classes?.qrScanHint || "anyspend-qr-scan-hint text-as-secondary text-xs"}>
|
|
393
397
|
SCAN WITH <span className="inline-block">🦊</span>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// CCShop contract ABI fragments used by AnySpendCollectorClubPurchase
|
|
2
|
+
|
|
3
|
+
export const BUY_PACKS_FOR_ABI = {
|
|
4
|
+
inputs: [
|
|
5
|
+
{ internalType: "address", name: "user", type: "address" },
|
|
6
|
+
{ internalType: "uint256", name: "packId", type: "uint256" },
|
|
7
|
+
{ internalType: "uint256", name: "amount", type: "uint256" },
|
|
8
|
+
],
|
|
9
|
+
name: "buyPacksFor",
|
|
10
|
+
outputs: [],
|
|
11
|
+
stateMutability: "nonpayable",
|
|
12
|
+
type: "function",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export const BUY_PACKS_FOR_WITH_DISCOUNT_ABI = {
|
|
16
|
+
inputs: [
|
|
17
|
+
{ internalType: "address", name: "user", type: "address" },
|
|
18
|
+
{ internalType: "uint256", name: "packId", type: "uint256" },
|
|
19
|
+
{ internalType: "uint256", name: "amount", type: "uint256" },
|
|
20
|
+
{ internalType: "string", name: "discountCode", type: "string" },
|
|
21
|
+
],
|
|
22
|
+
name: "buyPacksForWithDiscount",
|
|
23
|
+
outputs: [],
|
|
24
|
+
stateMutability: "nonpayable",
|
|
25
|
+
type: "function",
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
export const IS_DISCOUNT_CODE_VALID_FOR_PACK_ABI = {
|
|
29
|
+
inputs: [
|
|
30
|
+
{ internalType: "string", name: "code", type: "string" },
|
|
31
|
+
{ internalType: "uint256", name: "packId", type: "uint256" },
|
|
32
|
+
],
|
|
33
|
+
name: "isDiscountCodeValidForPack",
|
|
34
|
+
outputs: [
|
|
35
|
+
{ internalType: "bool", name: "isValid", type: "bool" },
|
|
36
|
+
{ internalType: "uint256", name: "discountAmount", type: "uint256" },
|
|
37
|
+
],
|
|
38
|
+
stateMutability: "view",
|
|
39
|
+
type: "function",
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
export const GET_DISCOUNT_CODE_ABI = {
|
|
43
|
+
inputs: [{ internalType: "string", name: "code", type: "string" }],
|
|
44
|
+
name: "getDiscountCode",
|
|
45
|
+
outputs: [
|
|
46
|
+
{
|
|
47
|
+
components: [
|
|
48
|
+
{ internalType: "uint256", name: "discountAmount", type: "uint256" },
|
|
49
|
+
{ internalType: "uint256", name: "expiresAt", type: "uint256" },
|
|
50
|
+
{ internalType: "bool", name: "used", type: "bool" },
|
|
51
|
+
{ internalType: "bool", name: "exists", type: "bool" },
|
|
52
|
+
{ internalType: "uint256", name: "maxUses", type: "uint256" },
|
|
53
|
+
{ internalType: "uint256", name: "usedCount", type: "uint256" },
|
|
54
|
+
{ internalType: "uint256", name: "packId", type: "uint256" },
|
|
55
|
+
{ internalType: "uint256", name: "minPurchaseAmount", type: "uint256" },
|
|
56
|
+
],
|
|
57
|
+
internalType: "struct CCShop.DiscountCode",
|
|
58
|
+
name: "",
|
|
59
|
+
type: "tuple",
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
stateMutability: "view",
|
|
63
|
+
type: "function",
|
|
64
|
+
} as const;
|
|
@@ -32,8 +32,6 @@ interface CryptoPaySectionProps {
|
|
|
32
32
|
onShowFeeDetail?: () => void;
|
|
33
33
|
// Custom classes for styling
|
|
34
34
|
classes?: CryptoPaySectionClasses;
|
|
35
|
-
/** When true, skip auto-setting max balance when token changes (used for fixed destination amount mode) */
|
|
36
|
-
skipAutoMaxOnTokenChange?: boolean;
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
export function CryptoPaySection({
|
|
@@ -51,7 +49,6 @@ export function CryptoPaySection({
|
|
|
51
49
|
onTokenSelect,
|
|
52
50
|
onShowFeeDetail,
|
|
53
51
|
classes,
|
|
54
|
-
skipAutoMaxOnTokenChange = false,
|
|
55
52
|
}: CryptoPaySectionProps) {
|
|
56
53
|
const { data: srcTokenMetadata } = useTokenData(selectedSrcToken?.chainId, selectedSrcToken?.address);
|
|
57
54
|
|
|
@@ -125,7 +122,6 @@ export function CryptoPaySection({
|
|
|
125
122
|
<div className={classes?.inputContainer}>
|
|
126
123
|
<OrderTokenAmount
|
|
127
124
|
address={walletAddress}
|
|
128
|
-
walletAddress={walletAddress}
|
|
129
125
|
context="from"
|
|
130
126
|
inputValue={srcAmount}
|
|
131
127
|
onChangeInput={value => {
|
|
@@ -137,7 +133,6 @@ export function CryptoPaySection({
|
|
|
137
133
|
token={selectedSrcToken}
|
|
138
134
|
setToken={setSelectedSrcToken}
|
|
139
135
|
onTokenSelect={onTokenSelect}
|
|
140
|
-
skipAutoMaxOnTokenChange={skipAutoMaxOnTokenChange}
|
|
141
136
|
/>
|
|
142
137
|
</div>
|
|
143
138
|
<div className={classes?.balanceRow || "flex items-center justify-between"}>
|