@b3dotfun/sdk 0.1.70-alpha.11 → 0.1.70-alpha.13

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 (41) hide show
  1. package/dist/cjs/anyspend/react/components/AnySpend.d.ts +7 -0
  2. package/dist/cjs/anyspend/react/components/AnySpend.js +30 -17
  3. package/dist/cjs/anyspend/react/components/AnySpendDeposit.d.ts +15 -1
  4. package/dist/cjs/anyspend/react/components/AnySpendDeposit.js +50 -13
  5. package/dist/cjs/anyspend/react/components/QRDeposit.d.ts +14 -3
  6. package/dist/cjs/anyspend/react/components/QRDeposit.js +24 -15
  7. package/dist/cjs/global-account/react/components/SignInWithB3/BetterAuthSignIn.js +3 -1
  8. package/dist/cjs/global-account/react/components/SignInWithB3/components/AuthButton.js +2 -1
  9. package/dist/cjs/global-account/react/components/SignInWithB3/steps/LoginStepBetterAuth.js +1 -0
  10. package/dist/cjs/global-account/react/components/SignInWithB3/utils/signInUtils.js +2 -0
  11. package/dist/cjs/global-account/react/hooks/useBetterAuth.d.ts +1 -1
  12. package/dist/cjs/global-account/react/stores/useModalStore.d.ts +14 -0
  13. package/dist/esm/anyspend/react/components/AnySpend.d.ts +7 -0
  14. package/dist/esm/anyspend/react/components/AnySpend.js +30 -17
  15. package/dist/esm/anyspend/react/components/AnySpendDeposit.d.ts +15 -1
  16. package/dist/esm/anyspend/react/components/AnySpendDeposit.js +51 -14
  17. package/dist/esm/anyspend/react/components/QRDeposit.d.ts +14 -3
  18. package/dist/esm/anyspend/react/components/QRDeposit.js +25 -16
  19. package/dist/esm/global-account/react/components/SignInWithB3/BetterAuthSignIn.js +3 -1
  20. package/dist/esm/global-account/react/components/SignInWithB3/components/AuthButton.js +2 -1
  21. package/dist/esm/global-account/react/components/SignInWithB3/steps/LoginStepBetterAuth.js +1 -0
  22. package/dist/esm/global-account/react/components/SignInWithB3/utils/signInUtils.js +2 -0
  23. package/dist/esm/global-account/react/hooks/useBetterAuth.d.ts +1 -1
  24. package/dist/esm/global-account/react/stores/useModalStore.d.ts +14 -0
  25. package/dist/styles/index.css +1 -1
  26. package/dist/types/anyspend/react/components/AnySpend.d.ts +7 -0
  27. package/dist/types/anyspend/react/components/AnySpendDeposit.d.ts +15 -1
  28. package/dist/types/anyspend/react/components/QRDeposit.d.ts +14 -3
  29. package/dist/types/global-account/react/hooks/useBetterAuth.d.ts +1 -1
  30. package/dist/types/global-account/react/stores/useModalStore.d.ts +14 -0
  31. package/package.json +1 -1
  32. package/src/anyspend/react/components/AnySpend.tsx +42 -16
  33. package/src/anyspend/react/components/AnySpendDeposit.tsx +87 -12
  34. package/src/anyspend/react/components/QRDeposit.tsx +46 -18
  35. package/src/anyspend/react/components/__tests__/QRDeposit.test.tsx +256 -0
  36. package/src/global-account/react/components/SignInWithB3/BetterAuthSignIn.tsx +12 -1
  37. package/src/global-account/react/components/SignInWithB3/components/AuthButton.tsx +9 -1
  38. package/src/global-account/react/components/SignInWithB3/steps/LoginStepBetterAuth.tsx +1 -0
  39. package/src/global-account/react/components/SignInWithB3/utils/signInUtils.ts +2 -0
  40. package/src/global-account/react/hooks/useBetterAuth.ts +1 -1
  41. package/src/global-account/react/stores/useModalStore.ts +14 -0
@@ -0,0 +1,256 @@
1
+ import { USDC_BASE } from "@b3dotfun/sdk/anyspend";
2
+ import { fireEvent, render, screen } from "@testing-library/react";
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ /**
6
+ * Deterministic component test for the `pureTransferOnly` "Fund Wallet" behavior.
7
+ *
8
+ * When `pureTransferOnly` is true, the destination token MIRRORS the user-selected
9
+ * source token, so funding is always a same-chain/same-token PURE TRANSFER:
10
+ * - NO `deposit_first` swap order is created (the order-creation effect early-returns
11
+ * on `isPureTransfer`), so `createOrder` is never called.
12
+ * - The QR/deposit address is the recipient's OWN wallet address (not a global/order
13
+ * address minted by AnySpend).
14
+ * - `useWatchTransfer` watches the user's wallet for the EXACT token they selected.
15
+ *
16
+ * This guards the bug where "Fund Wallet" with USDC delivered ETH instead.
17
+ */
18
+
19
+ // --- Spies / captured args observable across the test file ---
20
+
21
+ // createOrder spy returned by useCreateDepositFirstOrder.
22
+ const createOrderSpy = vi.fn();
23
+
24
+ // reset spy returned by useWatchTransfer — QRDeposit calls reset() on token switch to
25
+ // clear the stale balance baseline (see Test B).
26
+ const resetSpy = vi.fn();
27
+
28
+ // Captures the most recent props passed to useWatchTransfer so we can assert which
29
+ // token/decimals/address the pure-transfer path is watching.
30
+ let watchTransferArgs: any;
31
+
32
+ // Holds the `setToken` callback handed to the mocked Relay TokenSelector so a test
33
+ // can simulate the user switching the source token.
34
+ let capturedSetToken: ((token: any) => void) | undefined;
35
+
36
+ // Allows Test D to flip useTokenData's return (null = API miss) for the
37
+ // AnySpendDeposit-level decimals-invariant assertion.
38
+ const tokenDataState: { data: any; isLoading: boolean } = { data: null, isLoading: false };
39
+
40
+ // --- Mocks ---
41
+
42
+ // Mock the four hooks QRDeposit imports (relative paths from the component file).
43
+ vi.mock("../../hooks/useCreateDepositFirstOrder", () => ({
44
+ useCreateDepositFirstOrder: () => ({ createOrder: createOrderSpy, isCreatingOrder: false }),
45
+ }));
46
+
47
+ vi.mock("../../hooks/useAnyspendOrderAndTransactions", () => ({
48
+ useAnyspendOrderAndTransactions: () => ({ orderAndTransactions: undefined }),
49
+ }));
50
+
51
+ vi.mock("../../hooks/useWatchTransfer", () => ({
52
+ useWatchTransfer: (props: any) => {
53
+ watchTransferArgs = props;
54
+ return { isWatching: true, reset: resetSpy };
55
+ },
56
+ }));
57
+
58
+ vi.mock("../../hooks/useOnOrderSuccess", () => ({
59
+ useOnOrderSuccess: () => undefined,
60
+ }));
61
+
62
+ // Mock the Relay TokenSelector: render a button that, when clicked, invokes the passed
63
+ // `setToken` with a chosen token, simulating the user switching the source token. Also
64
+ // render the provided `trigger` so the rest of the component tree still mounts.
65
+ vi.mock("@relayprotocol/relay-kit-ui", () => ({
66
+ TokenSelector: ({ setToken, trigger }: any) => {
67
+ capturedSetToken = setToken;
68
+ return (
69
+ <div data-testid="token-selector">
70
+ {trigger}
71
+ <button data-testid="switch-token" onClick={() => setToken(SWITCHED_ETH_ON_BASE)} />
72
+ </div>
73
+ );
74
+ },
75
+ }));
76
+
77
+ // Spread the real global-account/react (Button, Badge, etc. are needed by transitively
78
+ // imported sub-components and render fine under happy-dom). Override only `toast` so the
79
+ // error path doesn't depend on a toast provider.
80
+ vi.mock("@b3dotfun/sdk/global-account/react", async importOriginal => {
81
+ const actual = await importOriginal<any>();
82
+ return { ...actual, toast: { error: vi.fn(), success: vi.fn() } };
83
+ });
84
+
85
+ // Mock the wallet/balance/token hooks at their SOURCE modules (the barrel re-exports
86
+ // these, so the override binds reliably). This lets AnySpendDeposit (Test D) render
87
+ // without a ThirdwebProvider / wallet context and keeps the token-data fetch deterministic.
88
+ vi.mock("@b3dotfun/sdk/global-account/react/hooks/useAccountWallet", () => ({
89
+ useAccountWallet: () => ({ connectedEOAWallet: undefined }),
90
+ }));
91
+
92
+ vi.mock("@b3dotfun/sdk/global-account/react/hooks/useSimBalance", () => ({
93
+ useSimBalance: () => ({ data: undefined, isLoading: false }),
94
+ useSimSvmBalance: () => ({ data: undefined, isLoading: false }),
95
+ useSimTokenBalance: () => ({ data: undefined, isLoading: false }),
96
+ }));
97
+
98
+ vi.mock("@b3dotfun/sdk/global-account/react/hooks/useTokenData", () => ({
99
+ useTokenData: () => tokenDataState,
100
+ }));
101
+
102
+ // --- Fixtures ---
103
+
104
+ const RECIPIENT = "0x1111111111111111111111111111111111111111";
105
+
106
+ // A DIFFERENT token than USDC for the "switch source token" test: native ETH on Base.
107
+ const SWITCHED_ETH_ON_BASE = {
108
+ chainId: 8453,
109
+ address: "0x0000000000000000000000000000000000000000",
110
+ symbol: "ETH",
111
+ name: "Ethereum",
112
+ decimals: 18,
113
+ logoURI: "https://assets.relay.link/icons/1/light.png",
114
+ };
115
+
116
+ // Imported lazily so module-level mocks are registered before the component graph loads.
117
+ async function loadQRDeposit() {
118
+ const mod = await import("../QRDeposit");
119
+ return mod.QRDeposit;
120
+ }
121
+
122
+ async function loadAnySpendDeposit() {
123
+ const mod = await import("../AnySpendDeposit");
124
+ return mod.AnySpendDeposit;
125
+ }
126
+
127
+ beforeEach(() => {
128
+ watchTransferArgs = undefined;
129
+ capturedSetToken = undefined;
130
+ tokenDataState.data = null;
131
+ tokenDataState.isLoading = false;
132
+ });
133
+
134
+ afterEach(() => {
135
+ vi.clearAllMocks();
136
+ });
137
+
138
+ describe("QRDeposit pureTransferOnly (Fund Wallet)", () => {
139
+ it("Test A — pure transfer creates NO order and uses the wallet's own address", async () => {
140
+ const QRDeposit = await loadQRDeposit();
141
+
142
+ render(
143
+ <QRDeposit
144
+ pureTransferOnly
145
+ recipientAddress={RECIPIENT}
146
+ destinationToken={USDC_BASE}
147
+ destinationChainId={8453}
148
+ />,
149
+ );
150
+
151
+ // The whole point: no swap/bridge order is created in pure-transfer mode.
152
+ expect(createOrderSpy).not.toHaveBeenCalled();
153
+
154
+ // It watches the recipient's OWN wallet for a direct incoming transfer, and is enabled.
155
+ expect(watchTransferArgs).toBeDefined();
156
+ expect(watchTransferArgs.address).toBe(RECIPIENT);
157
+ expect(watchTransferArgs.enabled).toBe(true);
158
+
159
+ // The mirror followed the initial selection: it watches for the EXACT funding token
160
+ // (Base USDC, 6 decimals) — NOT a generic 18-decimal fallback.
161
+ expect(watchTransferArgs.chainId).toBe(USDC_BASE.chainId);
162
+ expect(watchTransferArgs.tokenAddress).toBe(USDC_BASE.address);
163
+ expect(watchTransferArgs.tokenDecimals).toBe(6);
164
+
165
+ // The displayed deposit address is the recipient's own address, NOT a global/order address.
166
+ expect(screen.getByText(RECIPIENT)).toBeTruthy();
167
+ });
168
+
169
+ it("Test B — switching the source token stays a pure transfer (no order; mirror follows selection)", async () => {
170
+ const QRDeposit = await loadQRDeposit();
171
+
172
+ render(
173
+ <QRDeposit
174
+ pureTransferOnly
175
+ recipientAddress={RECIPIENT}
176
+ destinationToken={USDC_BASE}
177
+ destinationChainId={8453}
178
+ />,
179
+ );
180
+
181
+ // Sanity: still USDC before the switch.
182
+ expect(watchTransferArgs.tokenAddress).toBe(USDC_BASE.address);
183
+ expect(watchTransferArgs.tokenDecimals).toBe(6);
184
+
185
+ // User switches the source token to native ETH on Base via the (mocked) TokenSelector.
186
+ expect(capturedSetToken).toBeDefined();
187
+ fireEvent.click(screen.getByTestId("switch-token"));
188
+
189
+ // The balance watcher baseline is reset on token switch, so the new token isn't
190
+ // compared against the old token's (differently-scaled) balance.
191
+ expect(resetSpy).toHaveBeenCalled();
192
+
193
+ // Still a pure transfer: no order is ever created, even after switching tokens.
194
+ expect(createOrderSpy).not.toHaveBeenCalled();
195
+
196
+ // The mirror followed the new selection: now watching native ETH (18 decimals) on Base.
197
+ expect(watchTransferArgs.address).toBe(RECIPIENT);
198
+ expect(watchTransferArgs.enabled).toBe(true);
199
+ expect(watchTransferArgs.chainId).toBe(SWITCHED_ETH_ON_BASE.chainId);
200
+ expect(watchTransferArgs.tokenAddress).toBe(SWITCHED_ETH_ON_BASE.address);
201
+ expect(watchTransferArgs.tokenDecimals).toBe(18);
202
+ });
203
+
204
+ it("Test C — control: pureTransferOnly=false with source != destination DOES create an order", async () => {
205
+ const QRDeposit = await loadQRDeposit();
206
+
207
+ // No pureTransferOnly: source defaults to ETH-on-Base, which differs from the USDC dest,
208
+ // so this is a real swap and the normal deposit_first order path must run.
209
+ render(<QRDeposit recipientAddress={RECIPIENT} destinationToken={USDC_BASE} destinationChainId={8453} />);
210
+
211
+ // The flag is what gates the pure-transfer behavior: without it, an order IS created once.
212
+ expect(createOrderSpy).toHaveBeenCalledTimes(1);
213
+
214
+ // And it's created with the USDC destination token (the swap path is intact).
215
+ const orderArgs = createOrderSpy.mock.calls[0][0];
216
+ expect(orderArgs.recipientAddress).toBe(RECIPIENT);
217
+ expect(orderArgs.dstToken.address).toBe(USDC_BASE.address);
218
+ expect(orderArgs.dstToken.decimals).toBe(6);
219
+ // Source defaulted to native ETH on Base (the differing token that forces a swap).
220
+ expect(orderArgs.srcToken.address).toBe("0x0000000000000000000000000000000000000000");
221
+ });
222
+ });
223
+
224
+ describe("AnySpendDeposit decimals invariant (KNOWN_FUNDING_TOKENS fallback)", () => {
225
+ it("Test D — Base USDC keeps decimals: 6 even when useTokenData returns null", async () => {
226
+ const AnySpendDeposit = await loadAnySpendDeposit();
227
+
228
+ // Simulate the API miss: useTokenData returns null. The KNOWN_FUNDING_TOKENS fallback
229
+ // in AnySpendDeposit must still resolve Base USDC to decimals: 6 (never the generic 18),
230
+ // and that fully-resolved token is forwarded to QRDeposit as the pure-transfer source.
231
+ tokenDataState.data = null;
232
+ tokenDataState.isLoading = false;
233
+
234
+ render(
235
+ <AnySpendDeposit
236
+ pureTransferOnly
237
+ recipientAddress={RECIPIENT}
238
+ destinationTokenAddress={USDC_BASE.address}
239
+ destinationTokenChainId={USDC_BASE.chainId}
240
+ />,
241
+ );
242
+
243
+ // pureTransferOnly no longer auto-opens QR — AnySpendDeposit lands on the chooser
244
+ // (select-chain) screen first. Navigate into QR exactly like the user would, by
245
+ // clicking the "Deposit with QR Code" option.
246
+ fireEvent.click(screen.getByText("Deposit with QR Code"));
247
+
248
+ // Verified end-to-end through the REAL AnySpendDeposit -> REAL QRDeposit: the pure-transfer
249
+ // source mirrors the destination, so useWatchTransfer is called with USDC's 6 decimals.
250
+ expect(watchTransferArgs).toBeDefined();
251
+ expect(watchTransferArgs.tokenAddress).toBe(USDC_BASE.address);
252
+ expect(watchTransferArgs.tokenDecimals).toBe(6);
253
+ // No swap order in pure-transfer mode.
254
+ expect(createOrderSpy).not.toHaveBeenCalled();
255
+ });
256
+ });
@@ -1,4 +1,5 @@
1
1
  import { Button, Input, Loading, useAuthStore } from "@b3dotfun/sdk/global-account/react";
2
+ import { cn } from "@b3dotfun/sdk/shared/utils";
2
3
  import { debugB3React } from "@b3dotfun/sdk/shared/utils/debug";
3
4
  import { useState } from "react";
4
5
  import {
@@ -20,6 +21,7 @@ const DEFAULT_SOCIAL_PROVIDERS: SocialProviderConfig[] = [
20
21
  { id: "discord", label: "Discord" },
21
22
  { id: "microsoft", label: "Microsoft" },
22
23
  { id: "slack", label: "Slack" },
24
+ { id: "twitter", label: "X" },
23
25
  ];
24
26
 
25
27
  export interface BetterAuthSignInProps {
@@ -415,7 +417,16 @@ export function BetterAuthSignIn({
415
417
  />
416
418
  </svg>
417
419
  ) : (
418
- icon && <img src={icon} alt="" className="h-5 w-5" />
420
+ icon && (
421
+ <img
422
+ src={icon}
423
+ alt=""
424
+ className={cn(
425
+ "h-5 w-5",
426
+ (provider.id === "github" || provider.id === "twitter") && "dark:invert",
427
+ )}
428
+ />
429
+ )
419
430
  )}
420
431
  <span>{isProviderLoading ? "Redirecting..." : label}</span>
421
432
  </button>
@@ -1,4 +1,5 @@
1
1
  import { Button } from "../../custom/Button";
2
+ import { cn } from "@b3dotfun/sdk/shared/utils";
2
3
  import { Github, Mail } from "lucide-react";
3
4
  import { strategyIcons, strategyLabels } from "../utils/signInUtils";
4
5
 
@@ -31,7 +32,14 @@ export function AuthButton({
31
32
  className="flex w-full items-center justify-center bg-gray-100 px-2 py-3 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700"
32
33
  >
33
34
  {strategyIcon ? (
34
- <img src={strategyIcon} alt={`${strategyLabel} icon`} className="h-9 w-9" />
35
+ <img
36
+ src={strategyIcon}
37
+ alt={`${strategyLabel} icon`}
38
+ className={cn(
39
+ "h-9 w-9",
40
+ (strategy === "github" || strategy === "twitter" || strategy === "x") && "dark:invert",
41
+ )}
42
+ />
35
43
  ) : FallbackIcon ? (
36
44
  <FallbackIcon className="h-9 w-9 text-gray-900 dark:text-gray-100" />
37
45
  ) : (
@@ -18,6 +18,7 @@ const SOCIAL_PROVIDERS: { id: BetterAuthSocialProvider; label: string }[] = [
18
18
  { id: "discord", label: "Discord" },
19
19
  { id: "microsoft", label: "Microsoft" },
20
20
  { id: "slack", label: "Slack" },
21
+ { id: "twitter", label: "X" },
21
22
  ];
22
23
 
23
24
  interface LoginStepBetterAuthProps {
@@ -65,6 +65,7 @@ export const strategyIcons: Record<string, string> = {
65
65
  google: "https://cdn.b3.fun/google.svg",
66
66
  github: "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/github/github-original.svg",
67
67
  x: "https://cdn.b3.fun/x.svg?1",
68
+ twitter: "https://cdn.b3.fun/x.svg?1",
68
69
  discord: "https://cdn.b3.fun/discord.svg",
69
70
  apple: "https://cdn.b3.fun/apple.svg",
70
71
  guest: "https://cdn.b3.fun/incognito.svg",
@@ -75,6 +76,7 @@ export const strategyIcons: Record<string, string> = {
75
76
  export const strategyLabels: Record<string, string> = {
76
77
  google: "Google",
77
78
  x: "X",
79
+ twitter: "X",
78
80
  discord: "Discord",
79
81
  apple: "Apple",
80
82
  guest: "Guest",
@@ -7,7 +7,7 @@ import { useUserQuery } from "./useUserQuery";
7
7
 
8
8
  const debug = debugB3React("useBetterAuth");
9
9
 
10
- export type BetterAuthSocialProvider = "google" | "discord" | "apple" | "github" | "slack" | "microsoft";
10
+ export type BetterAuthSocialProvider = "google" | "discord" | "apple" | "github" | "slack" | "microsoft" | "twitter";
11
11
 
12
12
  /** Thrown when email verification is required before the user can sign in. */
13
13
  export class EmailVerificationRequiredError extends Error {
@@ -645,8 +645,16 @@ export interface AnySpendDepositModalProps extends BaseModalProps {
645
645
  destinationTokenAddress: string;
646
646
  /** The destination chain ID */
647
647
  destinationTokenChainId: number;
648
+ /**
649
+ * When false, the destination token is user-selectable even though a default destination token is
650
+ * provided (the provided destination is used only as the initial/default value). Used by the
651
+ * wallet-funding deposit flow so the user can change the receive token away from the default
652
+ * (Base USDC). Defaults to true (locked destination). Does not affect the QR/pure-transfer path. */
653
+ lockDestinationToken?: boolean;
648
654
  /** Callback when deposit succeeds */
649
655
  onSuccess?: (amount: string) => void;
656
+ /** Callback when the modal's close control is pressed. */
657
+ onClose?: () => void;
650
658
  /** Callback for opening a custom modal (e.g., for special token handling) */
651
659
  onOpenCustomModal?: () => void;
652
660
  /** Custom footer content */
@@ -687,6 +695,12 @@ export interface AnySpendDepositModalProps extends BaseModalProps {
687
695
  classes?: AnySpendAllClasses;
688
696
  /** Whether to allow direct transfer without swap */
689
697
  allowDirectTransfer?: boolean;
698
+ /**
699
+ * When true, the QR-deposit path is a PURE TRANSFER: the destination token/chain
700
+ * mirror the user's selected source token, so the exact token they send lands in
701
+ * their wallet on the same chain (no swap/bridge).
702
+ */
703
+ pureTransferOnly?: boolean;
690
704
  /** Opaque metadata passed to the order for callbacks (e.g., workflow form data) */
691
705
  callbackMetadata?: Record<string, unknown>;
692
706
  }