@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.
Files changed (122) hide show
  1. package/dist/cjs/anyspend/react/components/AnySpend.d.ts +2 -0
  2. package/dist/cjs/anyspend/react/components/AnySpend.js +7 -16
  3. package/dist/cjs/anyspend/react/components/AnySpendCollectorClubPurchase.d.ts +6 -1
  4. package/dist/cjs/anyspend/react/components/AnySpendCollectorClubPurchase.js +151 -22
  5. package/dist/cjs/anyspend/react/components/AnySpendCustom.js +4 -50
  6. package/dist/cjs/anyspend/react/components/AnySpendCustomExactIn.d.ts +2 -0
  7. package/dist/cjs/anyspend/react/components/AnySpendCustomExactIn.js +4 -2
  8. package/dist/cjs/anyspend/react/components/AnySpendDeposit.d.ts +3 -1
  9. package/dist/cjs/anyspend/react/components/AnySpendDeposit.js +2 -2
  10. package/dist/cjs/anyspend/react/components/AnySpendWorkflowTrigger.d.ts +31 -0
  11. package/dist/cjs/anyspend/react/components/AnySpendWorkflowTrigger.js +14 -0
  12. package/dist/cjs/anyspend/react/components/QRDeposit.js +5 -13
  13. package/dist/cjs/anyspend/react/components/ccShopAbi.d.ts +113 -0
  14. package/dist/cjs/anyspend/react/components/ccShopAbi.js +63 -0
  15. package/dist/cjs/anyspend/react/components/common/CryptoPaySection.d.ts +1 -3
  16. package/dist/cjs/anyspend/react/components/common/CryptoPaySection.js +3 -3
  17. package/dist/cjs/anyspend/react/components/common/OrderTokenAmount.d.ts +1 -4
  18. package/dist/cjs/anyspend/react/components/common/OrderTokenAmount.js +3 -57
  19. package/dist/cjs/anyspend/react/components/common/PaySection.js +1 -1
  20. package/dist/cjs/anyspend/react/components/index.d.ts +2 -0
  21. package/dist/cjs/anyspend/react/components/index.js +3 -1
  22. package/dist/cjs/anyspend/react/hooks/index.d.ts +1 -0
  23. package/dist/cjs/anyspend/react/hooks/index.js +1 -0
  24. package/dist/cjs/anyspend/react/hooks/useAnyspendCreateOnrampOrder.js +1 -0
  25. package/dist/cjs/anyspend/react/hooks/useAnyspendCreateOrder.d.ts +1 -0
  26. package/dist/cjs/anyspend/react/hooks/useAnyspendCreateOrder.js +1 -0
  27. package/dist/cjs/anyspend/react/hooks/useOnOrderSuccess.d.ts +10 -0
  28. package/dist/cjs/anyspend/react/hooks/useOnOrderSuccess.js +27 -0
  29. package/dist/cjs/anyspend/services/anyspend.d.ts +2 -1
  30. package/dist/cjs/anyspend/services/anyspend.js +2 -1
  31. package/dist/cjs/anyspend/utils/chain.d.ts +1 -1
  32. package/dist/cjs/anyspend/utils/chain.js +72 -62
  33. package/dist/cjs/app.shared.js +8 -0
  34. package/dist/cjs/global-account/react/components/B3DynamicModal.js +4 -0
  35. package/dist/cjs/global-account/react/hooks/useFirstEOA.d.ts +4 -4
  36. package/dist/cjs/global-account/react/hooks/useUserQuery.js +10 -0
  37. package/dist/cjs/global-account/react/stores/useModalStore.d.ts +37 -1
  38. package/dist/cjs/global-account/react/stores/userStore.js +1 -0
  39. package/dist/esm/anyspend/react/components/AnySpend.d.ts +2 -0
  40. package/dist/esm/anyspend/react/components/AnySpend.js +7 -16
  41. package/dist/esm/anyspend/react/components/AnySpendCollectorClubPurchase.d.ts +6 -1
  42. package/dist/esm/anyspend/react/components/AnySpendCollectorClubPurchase.js +152 -23
  43. package/dist/esm/anyspend/react/components/AnySpendCustom.js +4 -17
  44. package/dist/esm/anyspend/react/components/AnySpendCustomExactIn.d.ts +2 -0
  45. package/dist/esm/anyspend/react/components/AnySpendCustomExactIn.js +4 -2
  46. package/dist/esm/anyspend/react/components/AnySpendDeposit.d.ts +3 -1
  47. package/dist/esm/anyspend/react/components/AnySpendDeposit.js +2 -2
  48. package/dist/esm/anyspend/react/components/AnySpendWorkflowTrigger.d.ts +31 -0
  49. package/dist/esm/anyspend/react/components/AnySpendWorkflowTrigger.js +11 -0
  50. package/dist/esm/anyspend/react/components/QRDeposit.js +6 -14
  51. package/dist/esm/anyspend/react/components/ccShopAbi.d.ts +113 -0
  52. package/dist/esm/anyspend/react/components/ccShopAbi.js +60 -0
  53. package/dist/esm/anyspend/react/components/common/CryptoPaySection.d.ts +1 -3
  54. package/dist/esm/anyspend/react/components/common/CryptoPaySection.js +3 -3
  55. package/dist/esm/anyspend/react/components/common/OrderTokenAmount.d.ts +1 -4
  56. package/dist/esm/anyspend/react/components/common/OrderTokenAmount.js +2 -56
  57. package/dist/esm/anyspend/react/components/common/PaySection.js +1 -1
  58. package/dist/esm/anyspend/react/components/index.d.ts +2 -0
  59. package/dist/esm/anyspend/react/components/index.js +1 -0
  60. package/dist/esm/anyspend/react/hooks/index.d.ts +1 -0
  61. package/dist/esm/anyspend/react/hooks/index.js +1 -0
  62. package/dist/esm/anyspend/react/hooks/useAnyspendCreateOnrampOrder.js +1 -0
  63. package/dist/esm/anyspend/react/hooks/useAnyspendCreateOrder.d.ts +1 -0
  64. package/dist/esm/anyspend/react/hooks/useAnyspendCreateOrder.js +1 -0
  65. package/dist/esm/anyspend/react/hooks/useOnOrderSuccess.d.ts +10 -0
  66. package/dist/esm/anyspend/react/hooks/useOnOrderSuccess.js +24 -0
  67. package/dist/esm/anyspend/services/anyspend.d.ts +2 -1
  68. package/dist/esm/anyspend/services/anyspend.js +2 -1
  69. package/dist/esm/anyspend/utils/chain.d.ts +1 -1
  70. package/dist/esm/anyspend/utils/chain.js +72 -62
  71. package/dist/esm/app.shared.js +8 -0
  72. package/dist/esm/global-account/react/components/B3DynamicModal.js +4 -0
  73. package/dist/esm/global-account/react/hooks/useFirstEOA.d.ts +4 -4
  74. package/dist/esm/global-account/react/hooks/useUserQuery.js +11 -1
  75. package/dist/esm/global-account/react/stores/useModalStore.d.ts +37 -1
  76. package/dist/esm/global-account/react/stores/userStore.js +1 -0
  77. package/dist/types/anyspend/react/components/AnySpend.d.ts +2 -0
  78. package/dist/types/anyspend/react/components/AnySpendCollectorClubPurchase.d.ts +6 -1
  79. package/dist/types/anyspend/react/components/AnySpendCustomExactIn.d.ts +2 -0
  80. package/dist/types/anyspend/react/components/AnySpendDeposit.d.ts +3 -1
  81. package/dist/types/anyspend/react/components/AnySpendWorkflowTrigger.d.ts +31 -0
  82. package/dist/types/anyspend/react/components/ccShopAbi.d.ts +113 -0
  83. package/dist/types/anyspend/react/components/common/CryptoPaySection.d.ts +1 -3
  84. package/dist/types/anyspend/react/components/common/OrderTokenAmount.d.ts +1 -4
  85. package/dist/types/anyspend/react/components/index.d.ts +2 -0
  86. package/dist/types/anyspend/react/hooks/index.d.ts +1 -0
  87. package/dist/types/anyspend/react/hooks/useAnyspendCreateOrder.d.ts +1 -0
  88. package/dist/types/anyspend/react/hooks/useOnOrderSuccess.d.ts +10 -0
  89. package/dist/types/anyspend/services/anyspend.d.ts +2 -1
  90. package/dist/types/anyspend/utils/chain.d.ts +1 -1
  91. package/dist/types/global-account/react/hooks/useFirstEOA.d.ts +4 -4
  92. package/dist/types/global-account/react/stores/useModalStore.d.ts +37 -1
  93. package/package.json +1 -1
  94. package/src/anyspend/README.md +14 -0
  95. package/src/anyspend/docs/checkout-sessions.md +228 -0
  96. package/src/anyspend/docs/components.md +26 -0
  97. package/src/anyspend/docs/examples.md +58 -0
  98. package/src/anyspend/docs/hooks.md +32 -0
  99. package/src/anyspend/llms.txt +185 -0
  100. package/src/anyspend/react/components/AnySpend.tsx +9 -17
  101. package/src/anyspend/react/components/AnySpendCollectorClubPurchase.tsx +206 -22
  102. package/src/anyspend/react/components/AnySpendCustom.tsx +3 -18
  103. package/src/anyspend/react/components/AnySpendCustomExactIn.tsx +5 -1
  104. package/src/anyspend/react/components/AnySpendDeposit.tsx +5 -0
  105. package/src/anyspend/react/components/AnySpendWorkflowTrigger.tsx +73 -0
  106. package/src/anyspend/react/components/QRDeposit.tsx +19 -15
  107. package/src/anyspend/react/components/ccShopAbi.ts +64 -0
  108. package/src/anyspend/react/components/common/CryptoPaySection.tsx +0 -5
  109. package/src/anyspend/react/components/common/OrderTokenAmount.tsx +1 -70
  110. package/src/anyspend/react/components/common/PaySection.tsx +0 -1
  111. package/src/anyspend/react/components/index.ts +2 -0
  112. package/src/anyspend/react/hooks/index.ts +1 -0
  113. package/src/anyspend/react/hooks/useAnyspendCreateOnrampOrder.ts +1 -0
  114. package/src/anyspend/react/hooks/useAnyspendCreateOrder.ts +2 -0
  115. package/src/anyspend/react/hooks/useOnOrderSuccess.ts +36 -0
  116. package/src/anyspend/services/anyspend.ts +3 -0
  117. package/src/anyspend/utils/chain.ts +81 -65
  118. package/src/app.shared.ts +11 -0
  119. package/src/global-account/react/components/B3DynamicModal.tsx +4 -0
  120. package/src/global-account/react/hooks/useUserQuery.ts +12 -1
  121. package/src/global-account/react/stores/useModalStore.ts +39 -2
  122. 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
- // ABI for buyPacksFor function only
41
- const BUY_PACKS_FOR_ABI = {
42
- inputs: [
43
- { internalType: "address", name: "user", type: "address" },
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
- // Calculate fiat amount (totalAmount in USD, assuming USDC with 6 decimals)
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 (!totalAmount || totalAmount === "0") return "0";
156
- return formatUnits(totalAmount, USDC_BASE.decimals);
157
- }, [totalAmount]);
287
+ if (!effectiveDstAmount || effectiveDstAmount === "0") return "0";
288
+ return formatUnits(effectiveDstAmount, USDC_BASE.decimals);
289
+ }, [effectiveDstAmount]);
158
290
 
159
- // Encode the buyPacksFor function call
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={totalAmount}
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
- useEffect(() => {
437
- if (oat?.data?.order.status === "executed" && !onSuccessCalled.current) {
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 { ALL_CHAINS, getAvailableChainIds, isSameChainAndToken } from "@b3dotfun/sdk/anyspend";
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
- useEffect(() => {
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={displayAddress} size={120} level="M" marginSize={0} />
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"}>