@b3dotfun/sdk 0.1.70-alpha.15 → 0.1.70-alpha.16

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.
@@ -0,0 +1,61 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { getChainName, getPaymentUrl, ZERO_ADDRESS } from "../../../../anyspend/index.js";
4
+ import { CopyToClipboard } from "../../../../global-account/react/index.js";
5
+ import { cn } from "../../../../shared/utils/cn.js";
6
+ import { ChevronLeft, Loader2, X } from "lucide-react";
7
+ import { QRCodeSVG } from "qrcode.react";
8
+ import { useCallback, useEffect, useMemo, useState } from "react";
9
+ import { useWatchTransfer } from "../../hooks/useWatchTransfer.js";
10
+ import { ChainWarningText, WarningText } from "./WarningText.js";
11
+ import { TransferResultScreen } from "./TransferResultScreen.js";
12
+ /**
13
+ * A self-contained view that shows the recipient address as a QR code + copy button and
14
+ * watches for an incoming same-chain/same-token transfer via `useWatchTransfer`. When a
15
+ * transfer is detected it renders `TransferResultScreen`.
16
+ *
17
+ * This is the "TRANSFER_CRYPTO" path for same-token AnySpend deposits — no relayer order,
18
+ * no fee; the user simply sends the token directly to their own address.
19
+ */
20
+ export function PureTransferView({ mode = "modal", recipientAddress, chainId, token, displayAmount, isActive, onBack, onClose, onTransferDetected, }) {
21
+ const [transferResult, setTransferResult] = useState(null);
22
+ // Fix 4: reset transferResult when the panel becomes active again
23
+ useEffect(() => {
24
+ if (isActive)
25
+ setTransferResult(null);
26
+ }, [isActive]);
27
+ // Fix 3: stabilize the callback so it doesn't re-run the watcher effect every render
28
+ const handleTransferDetected = useCallback((result) => {
29
+ setTransferResult(result);
30
+ onTransferDetected?.(result);
31
+ }, [onTransferDetected]);
32
+ const { isWatching } = useWatchTransfer({
33
+ address: recipientAddress,
34
+ chainId,
35
+ tokenAddress: token.address,
36
+ tokenDecimals: token.decimals,
37
+ // Fix 1: also require a non-empty recipient before enabling the watcher
38
+ enabled: !!recipientAddress && !transferResult && isActive,
39
+ onTransferDetected: handleTransferDetected,
40
+ });
41
+ // Fix 2: guard qrValue when recipientAddress is empty
42
+ const qrValue = useMemo(() => recipientAddress
43
+ ? getPaymentUrl(recipientAddress, undefined, token.address === ZERO_ADDRESS ? "ETH" : token.address, chainId, token.decimals)
44
+ : "", [recipientAddress, token.address, token.decimals, chainId]);
45
+ const chainName = useMemo(() => {
46
+ try {
47
+ return getChainName(chainId);
48
+ }
49
+ catch {
50
+ return "the specified chain";
51
+ }
52
+ }, [chainId]);
53
+ // Guard: if no address yet, render nothing
54
+ if (!recipientAddress)
55
+ return null;
56
+ // Once a transfer is detected, hand off to the shared success screen
57
+ if (transferResult) {
58
+ return (_jsx(TransferResultScreen, { mode: mode, transferResult: transferResult, token: token, chainId: chainId, recipientAddress: recipientAddress, onBack: onBack, onClose: onClose }));
59
+ }
60
+ return (_jsx("div", { className: cn("anyspend-container anyspend-pure-transfer font-inter bg-as-surface-primary mx-auto w-full max-w-[460px] p-6"), children: _jsxs("div", { className: "anyspend-pure-transfer-content flex flex-col gap-4", children: [_jsxs("div", { className: "anyspend-pure-transfer-header flex items-center justify-between", children: [onBack ? (_jsx("button", { onClick: onBack, className: "anyspend-pure-transfer-back text-as-secondary hover:text-as-primary", "aria-label": "Go back", children: _jsx(ChevronLeft, { className: "h-5 w-5" }) })) : (_jsx("div", { className: "w-5" })), _jsxs("h2", { className: "anyspend-pure-transfer-title text-as-primary text-base font-semibold", children: ["Send ", token.symbol] }), onClose ? (_jsx("button", { onClick: onClose, className: "anyspend-pure-transfer-close text-as-secondary hover:text-as-primary", "aria-label": "Close", children: _jsx(X, { className: "h-5 w-5" }) })) : (_jsx("div", { className: "w-5" }))] }), displayAmount && (_jsx("div", { className: "anyspend-pure-transfer-amount-badge border-as-border-secondary bg-as-surface-secondary flex items-center justify-center rounded-xl border px-4 py-2", children: _jsx("span", { className: "text-as-primary text-sm font-medium", children: displayAmount }) })), _jsxs("div", { className: "anyspend-pure-transfer-qr-content border-as-stroke flex items-start gap-4 rounded-xl border p-4", children: [_jsxs("div", { className: "anyspend-pure-transfer-qr-code-container flex flex-col items-center gap-2", children: [_jsx("div", { className: "anyspend-pure-transfer-qr-code rounded-lg bg-white p-2", children: _jsx(QRCodeSVG, { value: qrValue, size: 120, level: "M", marginSize: 0 }) }), _jsxs("span", { className: "anyspend-pure-transfer-qr-scan-hint text-as-secondary text-xs", children: ["SCAN WITH ", _jsx("span", { className: "inline-block", children: "\uD83E\uDD8A" })] })] }), _jsxs("div", { className: "anyspend-pure-transfer-address-container flex flex-1 flex-col gap-1", children: [_jsx("span", { className: "anyspend-pure-transfer-address-label text-as-secondary text-sm", children: "Deposit address:" }), _jsxs("div", { className: "anyspend-pure-transfer-address-row flex items-start gap-1", children: [_jsx("span", { className: "anyspend-pure-transfer-address text-as-primary break-all font-mono text-sm leading-relaxed", children: recipientAddress }), _jsx(CopyToClipboard, { text: recipientAddress, className: "anyspend-pure-transfer-copy-icon text-as-secondary hover:text-as-primary mt-0.5 shrink-0" })] })] })] }), _jsx(ChainWarningText, { chainId: chainId }), _jsxs(WarningText, { children: ["Only send ", token.symbol, " on ", chainName, ". Other tokens will not be converted."] }), isWatching && (_jsxs("div", { className: "anyspend-pure-transfer-watching bg-as-brand/10 flex items-center justify-center gap-2 rounded-lg p-3", children: [_jsx(Loader2, { className: "text-as-brand h-4 w-4 animate-spin" }), _jsx("span", { className: "text-as-brand text-sm", children: "Watching for incoming transfer..." })] })), _jsx(CopyToClipboard, { text: recipientAddress, children: _jsx("button", { className: "anyspend-pure-transfer-copy-button bg-as-brand hover:bg-as-brand/90 flex w-full items-center justify-center gap-2 rounded-xl py-3.5 font-medium text-white transition-all", children: "Copy deposit address" }) })] }) }));
61
+ }
@@ -13,6 +13,36 @@ let socketClientUrl = null;
13
13
  // REST client instance
14
14
  let restClient = null;
15
15
  let restClientUrl = null;
16
+ // The pinned @b3dotfun/b3-api client's generated usersMethods predates `setBio`,
17
+ // so `users.setBio(...)` throws "setBio is not a function". Re-register the users
18
+ // service with the full list (mirrors users.shared.ts). Remove once the
19
+ // @b3dotfun/b3-api pin includes setBio.
20
+ const USERS_METHODS = [
21
+ "find",
22
+ "get",
23
+ "create",
24
+ "patch",
25
+ "remove",
26
+ "setReferralCode",
27
+ "syncTwProfiles",
28
+ "setAvatar",
29
+ "setBio",
30
+ "registerUsername",
31
+ "getMe",
32
+ ];
33
+ // Re-register on every client the manager builds — the active transport
34
+ // (REST/socket) switches at runtime.
35
+ function ensureUsersCustomMethods(client) {
36
+ try {
37
+ const connection = client.get("connection");
38
+ if (connection?.service) {
39
+ client.use("users", connection.service("users"), { methods: USERS_METHODS });
40
+ }
41
+ }
42
+ catch {
43
+ // Non-fatal: keep whatever methods the generated client already exposes.
44
+ }
45
+ }
16
46
  /**
17
47
  * Creates a socket client
18
48
  */
@@ -21,6 +51,7 @@ function createSocketClient() {
21
51
  if (!socketClient || socketClientUrl !== url) {
22
52
  socketInstance = io(url, { transports: ["websocket"] });
23
53
  socketClient = createClient(socketio(socketInstance), clientOptions);
54
+ ensureUsersCustomMethods(socketClient);
24
55
  socketClientUrl = url;
25
56
  }
26
57
  return socketClient;
@@ -41,6 +72,7 @@ function createRestClient() {
41
72
  if (!restClient || restClientUrl !== url) {
42
73
  const connection = rest(url).fetch(resolveFetch());
43
74
  restClient = createClient(connection, clientOptions);
75
+ ensureUsersCustomMethods(restClient);
44
76
  restClientUrl = url;
45
77
  }
46
78
  return restClient;
@@ -20,7 +20,8 @@ export declare enum PanelView {
20
20
  FEE_DETAIL = 9,
21
21
  DIRECT_TRANSFER_SUCCESS = 10,
22
22
  FIAT_KYC = 11,
23
- FIAT_AUTH = 12
23
+ FIAT_AUTH = 12,
24
+ PURE_TRANSFER_ADDRESS = 13
24
25
  }
25
26
  export declare function AnySpend(props: {
26
27
  mode?: "page" | "modal";
@@ -0,0 +1,30 @@
1
+ import type { components } from "@b3dotfun/sdk/anyspend/types/api";
2
+ import type { TransferResult } from "../../hooks/useWatchTransfer";
3
+ export interface PureTransferViewProps {
4
+ mode?: "modal" | "page";
5
+ /** The recipient address where funds will land (the user's own wallet) */
6
+ recipientAddress: string;
7
+ /** Chain ID to watch on */
8
+ chainId: number;
9
+ /** Token the user sends — drives the QR code and balance watcher */
10
+ token: components["schemas"]["Token"];
11
+ /** Human-readable amount to display (display-only; skip row if empty or "0") */
12
+ displayAmount?: string;
13
+ /** Whether this panel is the currently active panel; gates the balance watcher */
14
+ isActive?: boolean;
15
+ /** Callback when back chevron is clicked */
16
+ onBack?: () => void;
17
+ /** Callback when X close button is clicked */
18
+ onClose?: () => void;
19
+ /** Callback when an incoming transfer is detected */
20
+ onTransferDetected?: (result: TransferResult) => void;
21
+ }
22
+ /**
23
+ * A self-contained view that shows the recipient address as a QR code + copy button and
24
+ * watches for an incoming same-chain/same-token transfer via `useWatchTransfer`. When a
25
+ * transfer is detected it renders `TransferResultScreen`.
26
+ *
27
+ * This is the "TRANSFER_CRYPTO" path for same-token AnySpend deposits — no relayer order,
28
+ * no fee; the user simply sends the token directly to their own address.
29
+ */
30
+ export declare function PureTransferView({ mode, recipientAddress, chainId, token, displayAmount, isActive, onBack, onClose, onTransferDetected, }: PureTransferViewProps): import("react/jsx-runtime").JSX.Element | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b3dotfun/sdk",
3
- "version": "0.1.70-alpha.15",
3
+ "version": "0.1.70-alpha.16",
4
4
  "source": "src/index.ts",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "react-native": "./dist/cjs/index.native.js",
@@ -71,6 +71,7 @@ import { LoginStep } from "@b3dotfun/sdk/global-account/react/components/SignInW
71
71
  import { PanelOnramp } from "./common/PanelOnramp";
72
72
  import { PanelOnrampPayment } from "./common/PanelOnrampPayment";
73
73
  import { PointsDetailPanel } from "./common/PointsDetailPanel";
74
+ import { PureTransferView } from "./common/PureTransferView";
74
75
  import { RecipientSelection } from "./common/RecipientSelection";
75
76
  import { TabSection } from "./common/TabSection";
76
77
  import { AnySpendCustomizationProvider, useAnySpendCustomization } from "./context/AnySpendCustomizationContext";
@@ -100,6 +101,7 @@ export enum PanelView {
100
101
  DIRECT_TRANSFER_SUCCESS,
101
102
  FIAT_KYC,
102
103
  FIAT_AUTH,
104
+ PURE_TRANSFER_ADDRESS,
103
105
  }
104
106
 
105
107
  const ANYSPEND_RECIPIENTS_KEY = "anyspend_recipients";
@@ -687,7 +689,10 @@ function AnySpendInner({
687
689
  globalAddress,
688
690
  });
689
691
 
690
- const recipientProfile = useProfile({ address: effectiveRecipientAddress, fresh: true });
692
+ const recipientProfile = useProfile({
693
+ address: effectiveRecipientAddress,
694
+ fresh: true,
695
+ });
691
696
  const recipientName = recipientProfile.data?.name;
692
697
 
693
698
  // Check token balance for crypto payments
@@ -906,15 +911,36 @@ function AnySpendInner({
906
911
  const isDirectTransfer = isSameChainSameToken && allowDirectTransfer;
907
912
 
908
913
  // Determine button state and text
909
- const btnInfo: { text: string; disable: boolean; error: boolean; loading: boolean } = useMemo(() => {
914
+ const btnInfo: {
915
+ text: string;
916
+ disable: boolean;
917
+ error: boolean;
918
+ loading: boolean;
919
+ } = useMemo(() => {
910
920
  // For fiat tab, check srcAmountOnRamp; for crypto tab, check activeInputAmountInWei
911
921
  const hasAmount =
912
922
  activeTab === "fiat" ? srcAmountOnRamp && parseFloat(srcAmountOnRamp) > 0 : activeInputAmountInWei !== "0";
913
- if (!hasAmount) return { text: "Enter an amount", disable: true, error: false, loading: false };
923
+ if (!hasAmount)
924
+ return {
925
+ text: "Enter an amount",
926
+ disable: true,
927
+ error: false,
928
+ loading: false,
929
+ };
914
930
  if (isSameChainSameToken && !allowDirectTransfer)
915
- return { text: "Select a different token or chain", disable: true, error: false, loading: false };
931
+ return {
932
+ text: "Select a different token or chain",
933
+ disable: true,
934
+ error: false,
935
+ loading: false,
936
+ };
916
937
  if (isLoadingAnyspendQuote && !isSameChainSameToken)
917
- return { text: "Loading quote...", disable: true, error: false, loading: true };
938
+ return {
939
+ text: "Loading quote...",
940
+ disable: true,
941
+ error: false,
942
+ loading: true,
943
+ };
918
944
  if (isCreatingOrder || isCreatingOnrampOrder || isSwitchingOrExecuting)
919
945
  return {
920
946
  text: isSwitchingOrExecuting ? "Transferring..." : "Creating order...",
@@ -923,15 +949,31 @@ function AnySpendInner({
923
949
  loading: true,
924
950
  };
925
951
  if ((!anyspendQuote || !anyspendQuote.success) && !(isSameChainSameToken && allowDirectTransfer))
926
- return { text: "No quote found", disable: true, error: false, loading: false };
952
+ return {
953
+ text: "No quote found",
954
+ disable: true,
955
+ error: false,
956
+ loading: false,
957
+ };
927
958
 
928
959
  if (activeTab === "fiat") {
929
960
  // For fiat: check recipient first, then payment method
930
- if (!effectiveRecipientAddress) return { text: "Select recipient", disable: false, error: false, loading: false };
961
+ if (!effectiveRecipientAddress)
962
+ return {
963
+ text: "Select recipient",
964
+ disable: false,
965
+ error: false,
966
+ loading: false,
967
+ };
931
968
 
932
969
  // If no fiat payment method selected, show "Select payment method"
933
970
  if (selectedFiatPaymentMethod === FiatPaymentMethod.NONE) {
934
- return { text: "Select payment method", disable: false, error: false, loading: false };
971
+ return {
972
+ text: "Select payment method",
973
+ disable: false,
974
+ error: false,
975
+ loading: false,
976
+ };
935
977
  }
936
978
  // If payment method is selected, show "Continue"
937
979
  return { text: "Continue", disable: false, error: false, loading: false };
@@ -942,11 +984,22 @@ function AnySpendInner({
942
984
 
943
985
  // If no payment method selected, show "Choose payment method"
944
986
  if (effectiveCryptoPaymentMethod === CryptoPaymentMethodType.NONE) {
945
- return { text: "Choose payment method", disable: false, error: false, loading: false };
987
+ return {
988
+ text: "Choose payment method",
989
+ disable: false,
990
+ error: false,
991
+ loading: false,
992
+ };
946
993
  }
947
994
 
948
995
  // Check recipient after payment method
949
- if (!effectiveRecipientAddress) return { text: "Select recipient", disable: false, error: false, loading: false };
996
+ if (!effectiveRecipientAddress)
997
+ return {
998
+ text: "Select recipient",
999
+ disable: false,
1000
+ error: false,
1001
+ loading: false,
1002
+ };
950
1003
 
951
1004
  // If payment method selected, show appropriate action
952
1005
  if (
@@ -954,10 +1007,22 @@ function AnySpendInner({
954
1007
  effectiveCryptoPaymentMethod === CryptoPaymentMethodType.GLOBAL_WALLET
955
1008
  ) {
956
1009
  const buttonText = isSameChainSameToken && allowDirectTransfer ? "Transfer" : "Swap";
957
- return { text: buttonText, disable: false, error: false, loading: false };
1010
+ return {
1011
+ text: buttonText,
1012
+ disable: false,
1013
+ error: false,
1014
+ loading: false,
1015
+ };
958
1016
  }
959
1017
  if (effectiveCryptoPaymentMethod === CryptoPaymentMethodType.TRANSFER_CRYPTO) {
960
- return { text: "Continue to payment", disable: false, error: false, loading: false };
1018
+ const transferText =
1019
+ isSameChainSameToken && allowDirectTransfer ? "Show deposit address" : "Continue to payment";
1020
+ return {
1021
+ text: transferText,
1022
+ disable: false,
1023
+ error: false,
1024
+ loading: false,
1025
+ };
961
1026
  }
962
1027
  }
963
1028
 
@@ -1063,10 +1128,18 @@ function AnySpendInner({
1063
1128
  // Handle crypto swap creation
1064
1129
  const handleCryptoSwap = async (method: CryptoPaymentMethodType) => {
1065
1130
  try {
1066
- const isDirectTransfer = isSameChainSameToken && allowDirectTransfer;
1131
+ // TRANSFER_CRYPTO is the manual deposit-address flow — no connected wallet to send from.
1132
+ const isDirectTransfer =
1133
+ isSameChainSameToken && allowDirectTransfer && method !== CryptoPaymentMethodType.TRANSFER_CRYPTO;
1067
1134
 
1068
1135
  invariant(effectiveRecipientAddress, "Recipient address is not found");
1069
1136
 
1137
+ // Same-token manual deposit → show recipient address + watch, no relayer order, no fee
1138
+ if (isSameChainSameToken && allowDirectTransfer && method === CryptoPaymentMethodType.TRANSFER_CRYPTO) {
1139
+ navigateToPanel(PanelView.PURE_TRANSFER_ADDRESS, "forward");
1140
+ return;
1141
+ }
1142
+
1070
1143
  const srcAmountBigInt = parseUnits(srcAmount.replace(/,/g, ""), selectedSrcToken.decimals);
1071
1144
 
1072
1145
  // Handle direct transfer (same chain/token) - bypass backend, transfer directly
@@ -1089,10 +1162,6 @@ function AnySpendInner({
1089
1162
  // Regular swap flow - use backend
1090
1163
  invariant(anyspendQuote, "Relay price is not found");
1091
1164
 
1092
- // Debug: Check payment method values
1093
- console.log("handleCryptoSwap - method parameter:", method);
1094
- console.log("handleCryptoSwap - selectedCryptoPaymentMethod state:", selectedCryptoPaymentMethod);
1095
-
1096
1165
  createOrder({
1097
1166
  recipientAddress: effectiveRecipientAddress,
1098
1167
  orderType: "swap",
@@ -1100,7 +1169,11 @@ function AnySpendInner({
1100
1169
  dstChain: isBuyMode ? destinationTokenChainId : selectedDstChainId,
1101
1170
  srcToken: selectedSrcToken,
1102
1171
  dstToken: isBuyMode
1103
- ? { ...selectedDstToken, chainId: destinationTokenChainId, address: destinationTokenAddress }
1172
+ ? {
1173
+ ...selectedDstToken,
1174
+ chainId: destinationTokenChainId,
1175
+ address: destinationTokenAddress,
1176
+ }
1104
1177
  : selectedDstToken,
1105
1178
  srcAmount: srcAmountBigInt.toString(),
1106
1179
  expectedDstAmount: anyspendQuote?.data?.currencyOut?.amount || "0",
@@ -1759,6 +1832,20 @@ function AnySpendInner({
1759
1832
  </div>
1760
1833
  );
1761
1834
 
1835
+ const pureTransferAddressView = (
1836
+ <PureTransferView
1837
+ mode={mode}
1838
+ recipientAddress={effectiveRecipientAddress ?? ""}
1839
+ chainId={selectedSrcChainId}
1840
+ token={selectedSrcToken}
1841
+ displayAmount={srcAmount && srcAmount !== "0" ? `${srcAmount} ${selectedSrcToken.symbol}` : undefined}
1842
+ isActive={activePanel === PanelView.PURE_TRANSFER_ADDRESS}
1843
+ onBack={navigateBack}
1844
+ onClose={() => setB3ModalOpen(false)}
1845
+ onTransferDetected={() => onSuccess?.()}
1846
+ />
1847
+ );
1848
+
1762
1849
  // Add tabs to the main component when no order is loaded
1763
1850
  return (
1764
1851
  <StyleRoot>
@@ -1839,6 +1926,9 @@ function AnySpendInner({
1839
1926
  <div key="fiat-auth-view" className={cn(mode === "page" && "p-6")}>
1840
1927
  {authView}
1841
1928
  </div>,
1929
+ <div key="pure-transfer-address-view" className={cn(mode === "page" && "p-6")}>
1930
+ {pureTransferAddressView}
1931
+ </div>,
1842
1932
  ]}
1843
1933
  </TransitionPanel>
1844
1934
  </div>
@@ -574,7 +574,11 @@ function AnySpendCustomExactInInner({
574
574
 
575
575
  const handleCryptoOrder = async () => {
576
576
  try {
577
- const isDirectTransfer = isSameChainSameToken && allowDirectTransfer;
577
+ // TRANSFER_CRYPTO is the manual deposit-address flow — no connected wallet to send from.
578
+ const isDirectTransfer =
579
+ isSameChainSameToken &&
580
+ allowDirectTransfer &&
581
+ effectiveCryptoPaymentMethod !== CryptoPaymentMethodType.TRANSFER_CRYPTO;
578
582
 
579
583
  if (!isDirectTransfer) {
580
584
  invariant(anyspendQuote, "Relay price is not found");
@@ -0,0 +1,212 @@
1
+ "use client";
2
+
3
+ import { getChainName, getPaymentUrl, ZERO_ADDRESS } from "@b3dotfun/sdk/anyspend";
4
+ import type { components } from "@b3dotfun/sdk/anyspend/types/api";
5
+ import { CopyToClipboard } from "@b3dotfun/sdk/global-account/react";
6
+ import { cn } from "@b3dotfun/sdk/shared/utils/cn";
7
+ import { ChevronLeft, Loader2, X } from "lucide-react";
8
+ import { QRCodeSVG } from "qrcode.react";
9
+ import { useCallback, useEffect, useMemo, useState } from "react";
10
+ import type { TransferResult } from "../../hooks/useWatchTransfer";
11
+ import { useWatchTransfer } from "../../hooks/useWatchTransfer";
12
+ import { ChainWarningText, WarningText } from "./WarningText";
13
+ import { TransferResultScreen } from "./TransferResultScreen";
14
+
15
+ export interface PureTransferViewProps {
16
+ mode?: "modal" | "page";
17
+ /** The recipient address where funds will land (the user's own wallet) */
18
+ recipientAddress: string;
19
+ /** Chain ID to watch on */
20
+ chainId: number;
21
+ /** Token the user sends — drives the QR code and balance watcher */
22
+ token: components["schemas"]["Token"];
23
+ /** Human-readable amount to display (display-only; skip row if empty or "0") */
24
+ displayAmount?: string;
25
+ /** Whether this panel is the currently active panel; gates the balance watcher */
26
+ isActive?: boolean;
27
+ /** Callback when back chevron is clicked */
28
+ onBack?: () => void;
29
+ /** Callback when X close button is clicked */
30
+ onClose?: () => void;
31
+ /** Callback when an incoming transfer is detected */
32
+ onTransferDetected?: (result: TransferResult) => void;
33
+ }
34
+
35
+ /**
36
+ * A self-contained view that shows the recipient address as a QR code + copy button and
37
+ * watches for an incoming same-chain/same-token transfer via `useWatchTransfer`. When a
38
+ * transfer is detected it renders `TransferResultScreen`.
39
+ *
40
+ * This is the "TRANSFER_CRYPTO" path for same-token AnySpend deposits — no relayer order,
41
+ * no fee; the user simply sends the token directly to their own address.
42
+ */
43
+ export function PureTransferView({
44
+ mode = "modal",
45
+ recipientAddress,
46
+ chainId,
47
+ token,
48
+ displayAmount,
49
+ isActive,
50
+ onBack,
51
+ onClose,
52
+ onTransferDetected,
53
+ }: PureTransferViewProps) {
54
+ const [transferResult, setTransferResult] = useState<TransferResult | null>(null);
55
+
56
+ // Fix 4: reset transferResult when the panel becomes active again
57
+ useEffect(() => {
58
+ if (isActive) setTransferResult(null);
59
+ }, [isActive]);
60
+
61
+ // Fix 3: stabilize the callback so it doesn't re-run the watcher effect every render
62
+ const handleTransferDetected = useCallback(
63
+ (result: TransferResult) => {
64
+ setTransferResult(result);
65
+ onTransferDetected?.(result);
66
+ },
67
+ [onTransferDetected],
68
+ );
69
+
70
+ const { isWatching } = useWatchTransfer({
71
+ address: recipientAddress,
72
+ chainId,
73
+ tokenAddress: token.address,
74
+ tokenDecimals: token.decimals,
75
+ // Fix 1: also require a non-empty recipient before enabling the watcher
76
+ enabled: !!recipientAddress && !transferResult && isActive,
77
+ onTransferDetected: handleTransferDetected,
78
+ });
79
+
80
+ // Fix 2: guard qrValue when recipientAddress is empty
81
+ const qrValue = useMemo(
82
+ () =>
83
+ recipientAddress
84
+ ? getPaymentUrl(
85
+ recipientAddress,
86
+ undefined,
87
+ token.address === ZERO_ADDRESS ? "ETH" : token.address,
88
+ chainId,
89
+ token.decimals,
90
+ )
91
+ : "",
92
+ [recipientAddress, token.address, token.decimals, chainId],
93
+ );
94
+
95
+ const chainName = useMemo(() => {
96
+ try {
97
+ return getChainName(chainId);
98
+ } catch {
99
+ return "the specified chain";
100
+ }
101
+ }, [chainId]);
102
+
103
+ // Guard: if no address yet, render nothing
104
+ if (!recipientAddress) return null;
105
+
106
+ // Once a transfer is detected, hand off to the shared success screen
107
+ if (transferResult) {
108
+ return (
109
+ <TransferResultScreen
110
+ mode={mode}
111
+ transferResult={transferResult}
112
+ token={token}
113
+ chainId={chainId}
114
+ recipientAddress={recipientAddress}
115
+ onBack={onBack}
116
+ onClose={onClose}
117
+ />
118
+ );
119
+ }
120
+
121
+ return (
122
+ <div
123
+ className={cn(
124
+ "anyspend-container anyspend-pure-transfer font-inter bg-as-surface-primary mx-auto w-full max-w-[460px] p-6",
125
+ )}
126
+ >
127
+ <div className="anyspend-pure-transfer-content flex flex-col gap-4">
128
+ {/* Header */}
129
+ <div className="anyspend-pure-transfer-header flex items-center justify-between">
130
+ {onBack ? (
131
+ <button
132
+ onClick={onBack}
133
+ className="anyspend-pure-transfer-back text-as-secondary hover:text-as-primary"
134
+ aria-label="Go back"
135
+ >
136
+ <ChevronLeft className="h-5 w-5" />
137
+ </button>
138
+ ) : (
139
+ <div className="w-5" />
140
+ )}
141
+ <h2 className="anyspend-pure-transfer-title text-as-primary text-base font-semibold">Send {token.symbol}</h2>
142
+ {onClose ? (
143
+ <button
144
+ onClick={onClose}
145
+ className="anyspend-pure-transfer-close text-as-secondary hover:text-as-primary"
146
+ aria-label="Close"
147
+ >
148
+ <X className="h-5 w-5" />
149
+ </button>
150
+ ) : (
151
+ <div className="w-5" />
152
+ )}
153
+ </div>
154
+
155
+ {/* Optional amount badge */}
156
+ {displayAmount && (
157
+ <div className="anyspend-pure-transfer-amount-badge border-as-border-secondary bg-as-surface-secondary flex items-center justify-center rounded-xl border px-4 py-2">
158
+ <span className="text-as-primary text-sm font-medium">{displayAmount}</span>
159
+ </div>
160
+ )}
161
+
162
+ {/* QR Code and address block — mirrors QRDeposit ~lines 413-453 */}
163
+ <div className="anyspend-pure-transfer-qr-content border-as-stroke flex items-start gap-4 rounded-xl border p-4">
164
+ {/* QR Code */}
165
+ <div className="anyspend-pure-transfer-qr-code-container flex flex-col items-center gap-2">
166
+ <div className="anyspend-pure-transfer-qr-code rounded-lg bg-white p-2">
167
+ <QRCodeSVG value={qrValue} size={120} level="M" marginSize={0} />
168
+ </div>
169
+ <span className="anyspend-pure-transfer-qr-scan-hint text-as-secondary text-xs">
170
+ SCAN WITH <span className="inline-block">🦊</span>
171
+ </span>
172
+ </div>
173
+
174
+ {/* Address info */}
175
+ <div className="anyspend-pure-transfer-address-container flex flex-1 flex-col gap-1">
176
+ <span className="anyspend-pure-transfer-address-label text-as-secondary text-sm">Deposit address:</span>
177
+ <div className="anyspend-pure-transfer-address-row flex items-start gap-1">
178
+ <span className="anyspend-pure-transfer-address text-as-primary break-all font-mono text-sm leading-relaxed">
179
+ {recipientAddress}
180
+ </span>
181
+ <CopyToClipboard
182
+ text={recipientAddress}
183
+ className="anyspend-pure-transfer-copy-icon text-as-secondary hover:text-as-primary mt-0.5 shrink-0"
184
+ />
185
+ </div>
186
+ </div>
187
+ </div>
188
+
189
+ {/* Warnings — mirrors QRDeposit ~lines 456-460 */}
190
+ <ChainWarningText chainId={chainId} />
191
+ <WarningText>
192
+ Only send {token.symbol} on {chainName}. Other tokens will not be converted.
193
+ </WarningText>
194
+
195
+ {/* Watching indicator — mirrors QRDeposit ~lines 462-473 */}
196
+ {isWatching && (
197
+ <div className="anyspend-pure-transfer-watching bg-as-brand/10 flex items-center justify-center gap-2 rounded-lg p-3">
198
+ <Loader2 className="text-as-brand h-4 w-4 animate-spin" />
199
+ <span className="text-as-brand text-sm">Watching for incoming transfer...</span>
200
+ </div>
201
+ )}
202
+
203
+ {/* Full-width copy button — mirrors QRDeposit ~lines 476-484 */}
204
+ <CopyToClipboard text={recipientAddress}>
205
+ <button className="anyspend-pure-transfer-copy-button bg-as-brand hover:bg-as-brand/90 flex w-full items-center justify-center gap-2 rounded-xl py-3.5 font-medium text-white transition-all">
206
+ Copy deposit address
207
+ </button>
208
+ </CopyToClipboard>
209
+ </div>
210
+ </div>
211
+ );
212
+ }
@@ -19,6 +19,37 @@ let socketClientUrl: string | null = null;
19
19
  let restClient: ClientApplication | null = null;
20
20
  let restClientUrl: string | null = null;
21
21
 
22
+ // The pinned @b3dotfun/b3-api client's generated usersMethods predates `setBio`,
23
+ // so `users.setBio(...)` throws "setBio is not a function". Re-register the users
24
+ // service with the full list (mirrors users.shared.ts). Remove once the
25
+ // @b3dotfun/b3-api pin includes setBio.
26
+ const USERS_METHODS = [
27
+ "find",
28
+ "get",
29
+ "create",
30
+ "patch",
31
+ "remove",
32
+ "setReferralCode",
33
+ "syncTwProfiles",
34
+ "setAvatar",
35
+ "setBio",
36
+ "registerUsername",
37
+ "getMe",
38
+ ];
39
+
40
+ // Re-register on every client the manager builds — the active transport
41
+ // (REST/socket) switches at runtime.
42
+ function ensureUsersCustomMethods(client: ClientApplication): void {
43
+ try {
44
+ const connection = (client as any).get("connection");
45
+ if (connection?.service) {
46
+ (client as any).use("users", connection.service("users"), { methods: USERS_METHODS });
47
+ }
48
+ } catch {
49
+ // Non-fatal: keep whatever methods the generated client already exposes.
50
+ }
51
+ }
52
+
22
53
  /**
23
54
  * Creates a socket client
24
55
  */
@@ -27,6 +58,7 @@ function createSocketClient(): ClientApplication {
27
58
  if (!socketClient || socketClientUrl !== url) {
28
59
  socketInstance = io(url, { transports: ["websocket"] });
29
60
  socketClient = createClient(socketio(socketInstance), clientOptions);
61
+ ensureUsersCustomMethods(socketClient);
30
62
  socketClientUrl = url;
31
63
  }
32
64
  return socketClient;
@@ -48,6 +80,7 @@ function createRestClient(): ClientApplication {
48
80
  if (!restClient || restClientUrl !== url) {
49
81
  const connection = rest(url).fetch(resolveFetch());
50
82
  restClient = createClient(connection, clientOptions);
83
+ ensureUsersCustomMethods(restClient);
51
84
  restClientUrl = url;
52
85
  }
53
86
  return restClient;