@b3dotfun/sdk 0.1.65 → 0.1.66-alpha.0

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 (116) 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/global-account/react/components/B3DynamicModal.js +4 -0
  34. package/dist/cjs/global-account/react/hooks/useUserQuery.js +10 -0
  35. package/dist/cjs/global-account/react/stores/useModalStore.d.ts +37 -1
  36. package/dist/cjs/global-account/react/stores/userStore.js +1 -0
  37. package/dist/esm/anyspend/react/components/AnySpend.d.ts +2 -0
  38. package/dist/esm/anyspend/react/components/AnySpend.js +7 -16
  39. package/dist/esm/anyspend/react/components/AnySpendCollectorClubPurchase.d.ts +6 -1
  40. package/dist/esm/anyspend/react/components/AnySpendCollectorClubPurchase.js +152 -23
  41. package/dist/esm/anyspend/react/components/AnySpendCustom.js +4 -17
  42. package/dist/esm/anyspend/react/components/AnySpendCustomExactIn.d.ts +2 -0
  43. package/dist/esm/anyspend/react/components/AnySpendCustomExactIn.js +4 -2
  44. package/dist/esm/anyspend/react/components/AnySpendDeposit.d.ts +3 -1
  45. package/dist/esm/anyspend/react/components/AnySpendDeposit.js +2 -2
  46. package/dist/esm/anyspend/react/components/AnySpendWorkflowTrigger.d.ts +31 -0
  47. package/dist/esm/anyspend/react/components/AnySpendWorkflowTrigger.js +11 -0
  48. package/dist/esm/anyspend/react/components/QRDeposit.js +6 -14
  49. package/dist/esm/anyspend/react/components/ccShopAbi.d.ts +113 -0
  50. package/dist/esm/anyspend/react/components/ccShopAbi.js +60 -0
  51. package/dist/esm/anyspend/react/components/common/CryptoPaySection.d.ts +1 -3
  52. package/dist/esm/anyspend/react/components/common/CryptoPaySection.js +3 -3
  53. package/dist/esm/anyspend/react/components/common/OrderTokenAmount.d.ts +1 -4
  54. package/dist/esm/anyspend/react/components/common/OrderTokenAmount.js +2 -56
  55. package/dist/esm/anyspend/react/components/common/PaySection.js +1 -1
  56. package/dist/esm/anyspend/react/components/index.d.ts +2 -0
  57. package/dist/esm/anyspend/react/components/index.js +1 -0
  58. package/dist/esm/anyspend/react/hooks/index.d.ts +1 -0
  59. package/dist/esm/anyspend/react/hooks/index.js +1 -0
  60. package/dist/esm/anyspend/react/hooks/useAnyspendCreateOnrampOrder.js +1 -0
  61. package/dist/esm/anyspend/react/hooks/useAnyspendCreateOrder.d.ts +1 -0
  62. package/dist/esm/anyspend/react/hooks/useAnyspendCreateOrder.js +1 -0
  63. package/dist/esm/anyspend/react/hooks/useOnOrderSuccess.d.ts +10 -0
  64. package/dist/esm/anyspend/react/hooks/useOnOrderSuccess.js +24 -0
  65. package/dist/esm/anyspend/services/anyspend.d.ts +2 -1
  66. package/dist/esm/anyspend/services/anyspend.js +2 -1
  67. package/dist/esm/anyspend/utils/chain.d.ts +1 -1
  68. package/dist/esm/anyspend/utils/chain.js +72 -62
  69. package/dist/esm/global-account/react/components/B3DynamicModal.js +4 -0
  70. package/dist/esm/global-account/react/hooks/useUserQuery.js +11 -1
  71. package/dist/esm/global-account/react/stores/useModalStore.d.ts +37 -1
  72. package/dist/esm/global-account/react/stores/userStore.js +1 -0
  73. package/dist/types/anyspend/react/components/AnySpend.d.ts +2 -0
  74. package/dist/types/anyspend/react/components/AnySpendCollectorClubPurchase.d.ts +6 -1
  75. package/dist/types/anyspend/react/components/AnySpendCustomExactIn.d.ts +2 -0
  76. package/dist/types/anyspend/react/components/AnySpendDeposit.d.ts +3 -1
  77. package/dist/types/anyspend/react/components/AnySpendWorkflowTrigger.d.ts +31 -0
  78. package/dist/types/anyspend/react/components/ccShopAbi.d.ts +113 -0
  79. package/dist/types/anyspend/react/components/common/CryptoPaySection.d.ts +1 -3
  80. package/dist/types/anyspend/react/components/common/OrderTokenAmount.d.ts +1 -4
  81. package/dist/types/anyspend/react/components/index.d.ts +2 -0
  82. package/dist/types/anyspend/react/hooks/index.d.ts +1 -0
  83. package/dist/types/anyspend/react/hooks/useAnyspendCreateOrder.d.ts +1 -0
  84. package/dist/types/anyspend/react/hooks/useOnOrderSuccess.d.ts +10 -0
  85. package/dist/types/anyspend/services/anyspend.d.ts +2 -1
  86. package/dist/types/anyspend/utils/chain.d.ts +1 -1
  87. package/dist/types/global-account/react/stores/useModalStore.d.ts +37 -1
  88. package/package.json +1 -1
  89. package/src/anyspend/README.md +14 -0
  90. package/src/anyspend/docs/checkout-sessions.md +228 -0
  91. package/src/anyspend/docs/components.md +26 -0
  92. package/src/anyspend/docs/examples.md +58 -0
  93. package/src/anyspend/docs/hooks.md +32 -0
  94. package/src/anyspend/llms.txt +185 -0
  95. package/src/anyspend/react/components/AnySpend.tsx +9 -17
  96. package/src/anyspend/react/components/AnySpendCollectorClubPurchase.tsx +206 -22
  97. package/src/anyspend/react/components/AnySpendCustom.tsx +3 -18
  98. package/src/anyspend/react/components/AnySpendCustomExactIn.tsx +5 -1
  99. package/src/anyspend/react/components/AnySpendDeposit.tsx +5 -0
  100. package/src/anyspend/react/components/AnySpendWorkflowTrigger.tsx +73 -0
  101. package/src/anyspend/react/components/QRDeposit.tsx +19 -15
  102. package/src/anyspend/react/components/ccShopAbi.ts +64 -0
  103. package/src/anyspend/react/components/common/CryptoPaySection.tsx +0 -5
  104. package/src/anyspend/react/components/common/OrderTokenAmount.tsx +1 -70
  105. package/src/anyspend/react/components/common/PaySection.tsx +0 -1
  106. package/src/anyspend/react/components/index.ts +2 -0
  107. package/src/anyspend/react/hooks/index.ts +1 -0
  108. package/src/anyspend/react/hooks/useAnyspendCreateOnrampOrder.ts +1 -0
  109. package/src/anyspend/react/hooks/useAnyspendCreateOrder.ts +2 -0
  110. package/src/anyspend/react/hooks/useOnOrderSuccess.ts +36 -0
  111. package/src/anyspend/services/anyspend.ts +3 -0
  112. package/src/anyspend/utils/chain.ts +81 -65
  113. package/src/global-account/react/components/B3DynamicModal.tsx +4 -0
  114. package/src/global-account/react/hooks/useUserQuery.ts +12 -1
  115. package/src/global-account/react/stores/useModalStore.ts +39 -2
  116. package/src/global-account/react/stores/userStore.ts +1 -0
@@ -0,0 +1,185 @@
1
+ # AnySpend SDK
2
+
3
+ > Accept crypto payments and onboard users with zero friction. Users pay with any token on any chain in one seamless experience.
4
+
5
+ ## Overview
6
+
7
+ AnySpend is a payment SDK (`@b3dotfun/sdk`) for crypto payments and fiat onramps. It provides React components, hooks, and service methods for token swaps, cross-chain transactions, NFT purchases, and merchant checkout flows.
8
+
9
+ Supported networks: Ethereum, Base, B3.
10
+ Supported platforms: React Web (full), React Native (hooks + services only), Node.js (services only).
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @b3dotfun/sdk
16
+ ```
17
+
18
+ Wrap your app with the provider:
19
+
20
+ ```tsx
21
+ import { AnySpendProvider } from "@b3dotfun/sdk/anyspend/react";
22
+ import "@b3dotfun/sdk/index.css";
23
+
24
+ function App() {
25
+ return <AnySpendProvider>{/* app */}</AnySpendProvider>;
26
+ }
27
+ ```
28
+
29
+ Requires Node.js v20.15.0+, React 18/19.
30
+
31
+ ## Components
32
+
33
+ ### AnySpend
34
+
35
+ Primary swap/onramp interface. Supports modal or page mode.
36
+
37
+ Props: `mode` ("modal"|"page"), `defaultActiveTab` ("crypto"|"fiat"), `destinationTokenAddress`, `destinationTokenChainId`, `recipientAddress`, `hideTransactionHistoryButton`, `loadOrder`, `onSuccess`, `checkoutSession`.
38
+
39
+ ```tsx
40
+ <AnySpend
41
+ mode="page"
42
+ defaultActiveTab="crypto"
43
+ recipientAddress="0x..."
44
+ onSuccess={(txHash) => console.log(txHash)}
45
+ />
46
+ ```
47
+
48
+ ### AnySpendNFTButton
49
+
50
+ One-click NFT purchase button.
51
+
52
+ Props: `nftContract` (NFTContract), `recipientAddress`, `onSuccess`.
53
+
54
+ NFTContract shape: `{ chainId, contractAddress, price (wei), priceFormatted, currency: { chainId, address, name, symbol, decimals }, name, description, imageUrl }`.
55
+
56
+ ### AnySpendCustom
57
+
58
+ Flexible component for custom smart contract interactions (staking, gaming, DeFi).
59
+
60
+ Props: `orderType` ("custom"), `dstChainId`, `dstToken`, `dstAmount`, `contractAddress`, `encodedData`, `spenderAddress`, `metadata`, `header`, `onSuccess`, `checkoutSession`.
61
+
62
+ ### AnySpendCustomExactIn
63
+
64
+ Like AnySpendCustom but for exact-input flows. Also supports `checkoutSession` prop.
65
+
66
+ ### Specialized Components
67
+
68
+ - `AnySpendNFT` — Enhanced NFT with marketplace features
69
+ - `AnySpendStakeB3` — B3 token staking
70
+ - `AnySpendBuySpin` — Gaming spin wheel purchases
71
+ - `AnySpendTournament` — Tournament entry payments
72
+
73
+ ## Hooks
74
+
75
+ ### useAnyspendQuote(quoteRequest)
76
+
77
+ Get real-time pricing. QuoteRequest: `{ srcChain, dstChain, srcTokenAddress, dstTokenAddress, type ("swap"|"custom"), tradeType ("EXACT_INPUT"|"EXACT_OUTPUT"), amount }`.
78
+
79
+ Returns: `{ anyspendQuote, isLoadingAnyspendQuote, getAnyspendQuoteError, refetchAnyspendQuote }`.
80
+
81
+ ### useAnyspendCreateOrder(options)
82
+
83
+ Create orders. Options: `{ onSuccess, onError, onSettled }`.
84
+
85
+ Returns: `{ createOrder(request), isCreatingOrder, createOrderError }`.
86
+
87
+ ### useAnyspendOrderAndTransactions(orderId)
88
+
89
+ Monitor order status and transactions in real-time.
90
+
91
+ Returns: `{ orderAndTransactions: { order, depositTxs, relayTx, executeTx, refundTxs }, isLoadingOrderAndTransactions, getOrderAndTransactionsError }`.
92
+
93
+ ### useAnyspendOrderHistory(creatorAddress, limit, offset)
94
+
95
+ Paginated order history for a user.
96
+
97
+ ### useAnyspendTokens(chainId, search)
98
+
99
+ Get available tokens for a chain.
100
+
101
+ ### useCoinbaseOnrampOptions()
102
+
103
+ Get Coinbase onramp config for fiat payments.
104
+
105
+ ### useStripeClientSecret(orderData)
106
+
107
+ Get Stripe payment intent for card payments.
108
+
109
+ ### useCreateCheckoutSession()
110
+
111
+ Mutation hook for creating checkout sessions.
112
+
113
+ Returns: `{ mutate: createSession, data, isPending }`.
114
+
115
+ ### useCheckoutSession(sessionId)
116
+
117
+ Query hook that auto-polls a checkout session. Stops on `complete` or `expired`.
118
+
119
+ Returns: `{ data: session, isLoading }`.
120
+
121
+ ## Checkout Sessions
122
+
123
+ Stripe-like checkout sessions decoupled from orders. Merchants create a session first, then create an order when the user picks a payment method.
124
+
125
+ ### Flow
126
+
127
+ 1. `POST /checkout-sessions` → Creates session (DB only, instant). Returns `{ id, status: "open" }`.
128
+ 2. `POST /orders` with `checkoutSessionId` → Creates order linked to session. Returns order with `globalAddress` or `oneClickBuyUrl`.
129
+ 3. User pays (crypto to globalAddress, or onramp redirect).
130
+ 4. `GET /checkout-sessions/:id` → Poll status. Returns `{ status: "complete", order_id }`.
131
+
132
+ ### Session Statuses
133
+
134
+ `open` → `processing` → `complete`, or `open` → `expired`.
135
+
136
+ ### API Endpoints
137
+
138
+ **POST /checkout-sessions** — Create session. All fields optional: `success_url`, `cancel_url`, `metadata`, `client_reference_id`, `expires_in`.
139
+
140
+ **POST /orders** — Standard order creation. Add `checkoutSessionId` to link. Validates: session exists (400), session is open (400), session has no order yet (409).
141
+
142
+ **GET /checkout-sessions/:id** — Retrieve session. Query `?include=order` to embed full order with transactions.
143
+
144
+ **POST /checkout-sessions/:id/expire** — Manually expire an open session.
145
+
146
+ ### Redirect URL Templates
147
+
148
+ `success_url` and `cancel_url` support `{SESSION_ID}` and `{ORDER_ID}` template variables. If no variable present, `?sessionId=<uuid>` is appended.
149
+
150
+ ### Component Integration
151
+
152
+ Pass `checkoutSession` prop to `AnySpend`, `AnySpendCustom`, or `AnySpendCustomExactIn`:
153
+
154
+ ```tsx
155
+ <AnySpend
156
+ checkoutSession={{
157
+ success_url: "https://myshop.com/success?session={SESSION_ID}",
158
+ cancel_url: "https://myshop.com/cancel",
159
+ metadata: { sku: "widget-1" },
160
+ }}
161
+ />
162
+ ```
163
+
164
+ Without the prop, all existing flows are unchanged.
165
+
166
+ ## Order Status Lifecycle
167
+
168
+ `scanning_deposit_transaction` → `obtain_token` → `sending_token_from_vault` → `relay` → `executed`
169
+
170
+ Failure states: `obtain_failed`, `expired`, `refunding`, `refunded`, `failure`.
171
+
172
+ For Stripe: `waiting_stripe_payment` precedes processing.
173
+
174
+ ## Error Codes
175
+
176
+ Payment: `INSUFFICIENT_BALANCE`, `INVALID_TOKEN_ADDRESS`, `MINIMUM_AMOUNT_NOT_MET`, `MAXIMUM_AMOUNT_EXCEEDED`.
177
+ Network: `SLIPPAGE`, `NETWORK_ERROR`, `QUOTE_EXPIRED`, `CHAIN_NOT_SUPPORTED`.
178
+ Contract: `CONTRACT_CALL_FAILED`, `INSUFFICIENT_GAS`, `NONCE_TOO_LOW`, `TRANSACTION_REVERTED`.
179
+
180
+ ## Links
181
+
182
+ - Docs: docs/installation.md, docs/components.md, docs/hooks.md, docs/examples.md, docs/checkout-sessions.md, docs/error-handling.md
183
+ - Live demo: https://anyspend.com
184
+ - Discord: https://discord.gg/b3dotfun
185
+ - Issues: https://github.com/b3-fun/b3/issues
@@ -52,6 +52,7 @@ import { useAutoSelectCryptoPaymentMethod } from "../hooks/useAutoSelectCryptoPa
52
52
  import { useConnectedWalletDisplay } from "../hooks/useConnectedWalletDisplay";
53
53
  import { useCryptoPaymentMethodState } from "../hooks/useCryptoPaymentMethodState";
54
54
  import { useDirectTransfer } from "../hooks/useDirectTransfer";
55
+ import { useOnOrderSuccess } from "../hooks/useOnOrderSuccess";
55
56
  import { useRecipientAddressState } from "../hooks/useRecipientAddressState";
56
57
  import { AnySpendFingerprintWrapper, getFingerprintConfig } from "./AnySpendFingerprintWrapper";
57
58
  import { CryptoPaymentMethod, CryptoPaymentMethodType } from "./common/CryptoPaymentMethod";
@@ -126,6 +127,8 @@ export function AnySpend(props: {
126
127
  allowDirectTransfer?: boolean;
127
128
  /** Fixed destination token amount (in wei/smallest unit). When provided, user cannot change the amount. */
128
129
  destinationTokenAmount?: string;
130
+ /** Opaque metadata passed to the order for callbacks (e.g., workflow form data) */
131
+ callbackMetadata?: Record<string, unknown>;
129
132
  }) {
130
133
  const fingerprintConfig = getFingerprintConfig();
131
134
 
@@ -158,6 +161,7 @@ function AnySpendInner({
158
161
  classes,
159
162
  allowDirectTransfer = false,
160
163
  destinationTokenAmount,
164
+ callbackMetadata,
161
165
  }: {
162
166
  sourceChainId?: number;
163
167
  destinationTokenAddress?: string;
@@ -179,6 +183,7 @@ function AnySpendInner({
179
183
  classes?: AnySpendClasses;
180
184
  allowDirectTransfer?: boolean;
181
185
  destinationTokenAmount?: string;
186
+ callbackMetadata?: Record<string, unknown>;
182
187
  }) {
183
188
  const searchParams = useSearchParamsSSR();
184
189
  const router = useRouter();
@@ -208,9 +213,6 @@ function AnySpendInner({
208
213
  toAmount?: string;
209
214
  } | null>(null);
210
215
 
211
- // Track if onSuccess has been called for the current order
212
- const onSuccessCalled = useRef(false);
213
-
214
216
  // Track animation direction for TransitionPanel
215
217
  const animationDirection = useRef<"forward" | "back" | null>(null);
216
218
  // Track previous panel for proper back navigation
@@ -704,19 +706,8 @@ function AnySpendInner({
704
706
  }
705
707
  }, [anyspendQuote, isSrcInputDirty, destinationTokenAmount]);
706
708
 
707
- useEffect(() => {
708
- if (oat?.data?.order.status === "executed" && !onSuccessCalled.current) {
709
- console.log("Calling onSuccess");
710
- const txHash = oat?.data?.executeTx?.txHash;
711
- onSuccess?.(txHash);
712
- onSuccessCalled.current = true;
713
- }
714
- }, [oat?.data?.order.status, oat?.data?.executeTx?.txHash, onSuccess]);
715
-
716
- // Reset flag when orderId changes
717
- useEffect(() => {
718
- onSuccessCalled.current = false;
719
- }, [orderId]);
709
+ // Call onSuccess when order is executed
710
+ useOnOrderSuccess({ orderData: oat, orderId, onSuccess });
720
711
 
721
712
  const { createOrder, isCreatingOrder } = useAnyspendCreateOrder({
722
713
  onSuccess: data => {
@@ -979,6 +970,7 @@ function AnySpendInner({
979
970
  srcAmount: srcAmountBigInt.toString(),
980
971
  expectedDstAmount: anyspendQuote?.data?.currencyOut?.amount || "0",
981
972
  creatorAddress: globalAddress,
973
+ callbackMetadata,
982
974
  });
983
975
  } catch (err: any) {
984
976
  console.error(err);
@@ -1056,6 +1048,7 @@ function AnySpendInner({
1056
1048
  },
1057
1049
  expectedDstAmount: anyspendQuote?.data?.currencyOut?.amount?.toString() || "0",
1058
1050
  creatorAddress: globalAddress,
1051
+ callbackMetadata,
1059
1052
  });
1060
1053
  } catch (err: any) {
1061
1054
  console.error(err);
@@ -1227,7 +1220,6 @@ function AnySpendInner({
1227
1220
  anyspendQuote={anyspendQuote}
1228
1221
  onTokenSelect={onTokenSelect}
1229
1222
  onShowFeeDetail={() => navigateToPanel(PanelView.FEE_DETAIL, "forward")}
1230
- skipAutoMaxOnTokenChange={!!destinationTokenAmount}
1231
1223
  />
1232
1224
  ) : (
1233
1225
  <motion.div
@@ -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>