@b3dotfun/sdk 0.0.30 → 0.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/anyspend/react/components/AnySpend.js +1 -1
- package/dist/cjs/anyspend/react/components/AnySpendBuySpin.js +2 -1
- package/dist/cjs/anyspend/react/components/AnySpendStakeB3.js +2 -1
- package/dist/cjs/anyspend/react/components/AnyspendDepositHype.d.ts +4 -0
- package/dist/cjs/anyspend/react/components/AnyspendDepositHype.js +6 -1
- package/dist/cjs/anyspend/react/components/common/ChainTokenIcon.d.ts +1 -1
- package/dist/cjs/anyspend/react/components/common/ChainTokenIcon.js +2 -1
- package/dist/cjs/anyspend/react/components/common/CryptoPaymentMethod.js +23 -28
- package/dist/cjs/anyspend/react/components/common/CryptoReceiveSection.d.ts +3 -1
- package/dist/cjs/anyspend/react/components/common/CryptoReceiveSection.js +2 -2
- package/dist/cjs/anyspend/react/components/common/OrderDetails.js +5 -5
- package/dist/cjs/anyspend/react/components/common/OrderTokenAmount.js +1 -1
- package/dist/cjs/anyspend/react/components/common/PanelOnramp.d.ts +4 -1
- package/dist/cjs/anyspend/react/components/common/PanelOnramp.js +3 -3
- package/dist/cjs/anyspend/react/components/common/PaySection.js +1 -1
- package/dist/cjs/global-account/react/components/B3DynamicModal.js +2 -5
- package/dist/cjs/global-account/react/components/B3Provider/B3Provider.js +5 -0
- package/dist/cjs/global-account/react/components/LinkAccount/LinkAccount.js +1 -0
- package/dist/cjs/global-account/react/components/ManageAccount/BalanceContent.d.ts +6 -0
- package/dist/cjs/global-account/react/components/ManageAccount/BalanceContent.js +94 -0
- package/dist/cjs/global-account/react/components/ManageAccount/ContentTokens.d.ts +14 -0
- package/dist/cjs/global-account/react/components/ManageAccount/ContentTokens.js +272 -0
- package/dist/cjs/global-account/react/components/ManageAccount/ManageAccount.js +9 -51
- package/dist/cjs/global-account/react/components/ManageAccount/TokenBalanceRow.d.ts +10 -0
- package/dist/cjs/global-account/react/components/ManageAccount/TokenBalanceRow.js +8 -0
- package/dist/cjs/global-account/react/components/TokenIcon.d.ts +11 -0
- package/dist/cjs/global-account/react/components/TokenIcon.js +43 -0
- package/dist/cjs/global-account/react/components/ui/accordion.d.ts +7 -0
- package/dist/cjs/global-account/react/components/ui/accordion.js +53 -0
- package/dist/cjs/global-account/react/components/ui/dialog.js +1 -1
- package/dist/cjs/global-account/react/hooks/index.d.ts +2 -0
- package/dist/cjs/global-account/react/hooks/index.js +5 -1
- package/dist/cjs/global-account/react/hooks/useAnalytics.d.ts +7 -0
- package/dist/cjs/global-account/react/hooks/useAnalytics.js +29 -0
- package/dist/cjs/global-account/react/hooks/useB3BalanceFromAddresses.js +2 -1
- package/dist/cjs/global-account/react/hooks/useNativeBalance.js +2 -1
- package/dist/cjs/global-account/react/hooks/useSimBalance.d.ts +24 -0
- package/dist/cjs/global-account/react/hooks/useSimBalance.js +29 -0
- package/dist/cjs/global-account/react/hooks/useUnifiedChainSwitchAndExecute.js +2 -1
- package/dist/cjs/global-account/react/stores/useModalStore.d.ts +2 -2
- package/dist/cjs/global-account/react/utils/profileDisplay.js +9 -0
- package/dist/cjs/global-account/utils/analytics.d.ts +16 -0
- package/dist/cjs/global-account/utils/analytics.js +55 -0
- package/dist/cjs/shared/constants/index.d.ts +1 -0
- package/dist/cjs/shared/constants/index.js +2 -1
- package/dist/cjs/shared/generated/chain-networks.json +185 -17
- package/dist/esm/anyspend/react/components/AnySpend.js +1 -1
- package/dist/esm/anyspend/react/components/AnySpendBuySpin.js +2 -1
- package/dist/esm/anyspend/react/components/AnySpendStakeB3.js +2 -1
- package/dist/esm/anyspend/react/components/AnyspendDepositHype.d.ts +4 -0
- package/dist/esm/anyspend/react/components/AnyspendDepositHype.js +5 -1
- package/dist/esm/anyspend/react/components/common/ChainTokenIcon.d.ts +1 -1
- package/dist/esm/anyspend/react/components/common/ChainTokenIcon.js +2 -1
- package/dist/esm/anyspend/react/components/common/CryptoPaymentMethod.js +22 -27
- package/dist/esm/anyspend/react/components/common/CryptoReceiveSection.d.ts +3 -1
- package/dist/esm/anyspend/react/components/common/CryptoReceiveSection.js +2 -2
- package/dist/esm/anyspend/react/components/common/OrderDetails.js +5 -5
- package/dist/esm/anyspend/react/components/common/OrderTokenAmount.js +1 -1
- package/dist/esm/anyspend/react/components/common/PanelOnramp.d.ts +4 -1
- package/dist/esm/anyspend/react/components/common/PanelOnramp.js +4 -4
- package/dist/esm/anyspend/react/components/common/PaySection.js +1 -1
- package/dist/esm/global-account/react/components/B3DynamicModal.js +2 -5
- package/dist/esm/global-account/react/components/B3Provider/B3Provider.js +5 -0
- package/dist/esm/global-account/react/components/LinkAccount/LinkAccount.js +1 -0
- package/dist/esm/global-account/react/components/ManageAccount/BalanceContent.d.ts +6 -0
- package/dist/esm/global-account/react/components/ManageAccount/BalanceContent.js +88 -0
- package/dist/esm/global-account/react/components/ManageAccount/ContentTokens.d.ts +14 -0
- package/dist/esm/global-account/react/components/ManageAccount/ContentTokens.js +266 -0
- package/dist/esm/global-account/react/components/ManageAccount/ManageAccount.js +12 -51
- package/dist/esm/global-account/react/components/ManageAccount/TokenBalanceRow.d.ts +10 -0
- package/dist/esm/global-account/react/components/ManageAccount/TokenBalanceRow.js +5 -0
- package/dist/esm/global-account/react/components/TokenIcon.d.ts +11 -0
- package/dist/esm/global-account/react/components/TokenIcon.js +37 -0
- package/dist/esm/global-account/react/components/ui/accordion.d.ts +7 -0
- package/dist/esm/global-account/react/components/ui/accordion.js +14 -0
- package/dist/esm/global-account/react/components/ui/dialog.js +1 -1
- package/dist/esm/global-account/react/hooks/index.d.ts +2 -0
- package/dist/esm/global-account/react/hooks/index.js +2 -0
- package/dist/esm/global-account/react/hooks/useAnalytics.d.ts +7 -0
- package/dist/esm/global-account/react/hooks/useAnalytics.js +26 -0
- package/dist/esm/global-account/react/hooks/useB3BalanceFromAddresses.js +2 -1
- package/dist/esm/global-account/react/hooks/useNativeBalance.js +2 -1
- package/dist/esm/global-account/react/hooks/useSimBalance.d.ts +24 -0
- package/dist/esm/global-account/react/hooks/useSimBalance.js +26 -0
- package/dist/esm/global-account/react/hooks/useUnifiedChainSwitchAndExecute.js +2 -1
- package/dist/esm/global-account/react/stores/useModalStore.d.ts +2 -2
- package/dist/esm/global-account/react/utils/profileDisplay.js +9 -0
- package/dist/esm/global-account/utils/analytics.d.ts +16 -0
- package/dist/esm/global-account/utils/analytics.js +50 -0
- package/dist/esm/shared/constants/index.d.ts +1 -0
- package/dist/esm/shared/constants/index.js +1 -0
- package/dist/esm/shared/generated/chain-networks.json +185 -17
- package/dist/styles/index.css +1 -1
- package/dist/types/anyspend/react/components/AnyspendDepositHype.d.ts +4 -0
- package/dist/types/anyspend/react/components/common/ChainTokenIcon.d.ts +1 -1
- package/dist/types/anyspend/react/components/common/CryptoReceiveSection.d.ts +3 -1
- package/dist/types/anyspend/react/components/common/PanelOnramp.d.ts +4 -1
- package/dist/types/global-account/react/components/ManageAccount/BalanceContent.d.ts +6 -0
- package/dist/types/global-account/react/components/ManageAccount/ContentTokens.d.ts +14 -0
- package/dist/types/global-account/react/components/ManageAccount/TokenBalanceRow.d.ts +10 -0
- package/dist/types/global-account/react/components/TokenIcon.d.ts +11 -0
- package/dist/types/global-account/react/components/ui/accordion.d.ts +7 -0
- package/dist/types/global-account/react/hooks/index.d.ts +2 -0
- package/dist/types/global-account/react/hooks/useAnalytics.d.ts +7 -0
- package/dist/types/global-account/react/hooks/useSimBalance.d.ts +24 -0
- package/dist/types/global-account/react/stores/useModalStore.d.ts +2 -2
- package/dist/types/global-account/utils/analytics.d.ts +16 -0
- package/dist/types/shared/constants/index.d.ts +1 -0
- package/package.json +10 -18
- package/src/anyspend/react/components/AnySpend.tsx +1 -0
- package/src/anyspend/react/components/AnySpendBuySpin.tsx +2 -1
- package/src/anyspend/react/components/AnySpendStakeB3.tsx +3 -2
- package/src/anyspend/react/components/AnyspendDepositHype.tsx +10 -0
- package/src/anyspend/react/components/AnyspendSignatureMint.tsx +4 -4
- package/src/anyspend/react/components/common/ChainTokenIcon.tsx +8 -2
- package/src/anyspend/react/components/common/CryptoPaymentMethod.tsx +56 -107
- package/src/anyspend/react/components/common/CryptoReceiveSection.tsx +12 -3
- package/src/anyspend/react/components/common/OrderDetails.tsx +5 -5
- package/src/anyspend/react/components/common/OrderTokenAmount.tsx +2 -2
- package/src/anyspend/react/components/common/PanelOnramp.tsx +11 -5
- package/src/anyspend/react/components/common/PaySection.tsx +1 -1
- package/src/global-account/react/components/B3DynamicModal.tsx +8 -7
- package/src/global-account/react/components/B3Provider/B3Provider.tsx +6 -0
- package/src/global-account/react/components/LinkAccount/LinkAccount.tsx +2 -1
- package/src/global-account/react/components/ManageAccount/BalanceContent.tsx +228 -0
- package/src/global-account/react/components/ManageAccount/ContentTokens.tsx +568 -0
- package/src/global-account/react/components/ManageAccount/ManageAccount.tsx +86 -341
- package/src/global-account/react/components/ManageAccount/TokenBalanceRow.tsx +46 -0
- package/src/global-account/react/components/TokenIcon.tsx +87 -0
- package/src/global-account/react/components/ui/accordion.tsx +53 -0
- package/src/global-account/react/components/ui/dialog.tsx +1 -1
- package/src/global-account/react/hooks/index.ts +2 -0
- package/src/global-account/react/hooks/useAccountAssets.ts +1 -0
- package/src/global-account/react/hooks/useAnalytics.tsx +30 -0
- package/src/global-account/react/hooks/useB3BalanceFromAddresses.ts +3 -2
- package/src/global-account/react/hooks/useNativeBalance.tsx +2 -1
- package/src/global-account/react/hooks/useSimBalance.ts +56 -0
- package/src/global-account/react/hooks/useUnifiedChainSwitchAndExecute.ts +3 -1
- package/src/global-account/react/stores/useModalStore.ts +2 -2
- package/src/global-account/react/utils/profileDisplay.ts +9 -0
- package/src/global-account/utils/analytics.ts +64 -0
- package/src/shared/constants/index.ts +2 -0
- package/src/shared/generated/chain-networks.json +185 -17
- package/src/{anyspend/types → types}/window.d.ts +5 -1
- package/dist/cjs/index.d.ts +0 -0
- package/dist/cjs/index.js +0 -2
- package/dist/esm/index.d.ts +0 -0
- package/dist/esm/index.js +0 -2
- package/dist/types/index.d.ts +0 -0
- package/src/index.ts +0 -1
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
import { ALL_CHAINS, getExplorerTxUrl } from "@b3dotfun/sdk/anyspend";
|
|
2
|
+
import { ChainTokenIcon } from "@b3dotfun/sdk/anyspend/react/components/common/ChainTokenIcon";
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
TransitionPanel,
|
|
6
|
+
useAnalytics,
|
|
7
|
+
useSimBalance,
|
|
8
|
+
useUnifiedChainSwitchAndExecute,
|
|
9
|
+
} from "@b3dotfun/sdk/global-account/react";
|
|
10
|
+
import { formatDisplayNumber, formatTokenAmount } from "@b3dotfun/sdk/shared/utils/number";
|
|
11
|
+
import { ArrowLeft, CircleHelp, Copy, Loader2, Send } from "lucide-react";
|
|
12
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
13
|
+
import { NumericFormat } from "react-number-format";
|
|
14
|
+
import { toast } from "sonner";
|
|
15
|
+
import { useActiveAccount } from "thirdweb/react";
|
|
16
|
+
import { encodeFunctionData, erc20Abi, isAddress, parseUnits } from "viem";
|
|
17
|
+
import { SimBalanceItem } from "../../hooks/useSimBalance";
|
|
18
|
+
import invariant from "invariant";
|
|
19
|
+
|
|
20
|
+
// Panel view enum for managing navigation between token list and send form
|
|
21
|
+
enum TokenPanelView {
|
|
22
|
+
LIST = 0, // Show list of user's tokens
|
|
23
|
+
SEND = 1, // Show send token form
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ContentTokensProps {
|
|
27
|
+
activeTab: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* ContentTokens Component
|
|
32
|
+
*
|
|
33
|
+
* Displays user's token balances with ability to send tokens. Features:
|
|
34
|
+
* - Animated transitions between token list and send form
|
|
35
|
+
* - Smart filtering (shows all tokens when ≤5 valuable tokens or no $1+ tokens)
|
|
36
|
+
* - NumericFormat inputs for proper number handling
|
|
37
|
+
* - Focus preservation during transitions (see render functions pattern below)
|
|
38
|
+
*/
|
|
39
|
+
export function ContentTokens({ activeTab }: ContentTokensProps) {
|
|
40
|
+
// === TOKEN FILTERING STATE ===
|
|
41
|
+
const [showAllTokens, setShowAllTokens] = useState(false);
|
|
42
|
+
|
|
43
|
+
// === NAVIGATION STATE ===
|
|
44
|
+
const [tokenPanelView, setTokenPanelView] = useState<TokenPanelView>(TokenPanelView.LIST);
|
|
45
|
+
|
|
46
|
+
// === SEND FORM STATE ===
|
|
47
|
+
const [selectedToken, setSelectedToken] = useState<SimBalanceItem | null>(null);
|
|
48
|
+
const [recipientAddress, setRecipientAddress] = useState("");
|
|
49
|
+
const [sendAmount, setSendAmount] = useState("");
|
|
50
|
+
const [isSending, setIsSending] = useState(false);
|
|
51
|
+
const [addressError, setAddressError] = useState("");
|
|
52
|
+
|
|
53
|
+
// === ANIMATION STATE ===
|
|
54
|
+
// CRITICAL: useRef for animation direction prevents component remounting
|
|
55
|
+
// This ensures input focus is preserved during panel transitions
|
|
56
|
+
const animationDirection = useRef<"forward" | "back" | null>(null);
|
|
57
|
+
|
|
58
|
+
// === DATA FETCHING ===
|
|
59
|
+
const account = useActiveAccount();
|
|
60
|
+
const { data: simBalance, refetch: refetchSimBalance, isLoading: isLoadingBalance } = useSimBalance(account?.address);
|
|
61
|
+
|
|
62
|
+
// === BLOCKCHAIN INTERACTION ===
|
|
63
|
+
const { switchChainAndExecute } = useUnifiedChainSwitchAndExecute();
|
|
64
|
+
|
|
65
|
+
// === ANALYTICS ===
|
|
66
|
+
const { sendAnalyticsEvent } = useAnalytics();
|
|
67
|
+
|
|
68
|
+
// === ADDRESS VALIDATION ===
|
|
69
|
+
// Handle recipient address change with real-time validation using viem
|
|
70
|
+
const handleRecipientAddressChange = (value: string) => {
|
|
71
|
+
setRecipientAddress(value);
|
|
72
|
+
// Only show error if user has typed something and it's invalid
|
|
73
|
+
// Using viem's isAddress for robust EVM address validation
|
|
74
|
+
if (value && !isAddress(value)) {
|
|
75
|
+
setAddressError("Please enter a valid EVM address (0x...)");
|
|
76
|
+
} else {
|
|
77
|
+
setAddressError("");
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// === TAB RESET EFFECT ===
|
|
82
|
+
// Reset all state when user switches away from tokens tab
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (activeTab !== "tokens") {
|
|
85
|
+
setTokenPanelView(TokenPanelView.LIST);
|
|
86
|
+
setSelectedToken(null);
|
|
87
|
+
setRecipientAddress("");
|
|
88
|
+
setSendAmount("");
|
|
89
|
+
setIsSending(false);
|
|
90
|
+
setAddressError("");
|
|
91
|
+
animationDirection.current = null;
|
|
92
|
+
}
|
|
93
|
+
}, [activeTab]);
|
|
94
|
+
|
|
95
|
+
// === HELPER FUNCTION ===
|
|
96
|
+
// Get current version of selected token from fresh balance data
|
|
97
|
+
// 🔧 FIX: Prevents auto-navigation back to token list when balance refreshes
|
|
98
|
+
// The useSimBalance hook refreshes data, creating new token object references
|
|
99
|
+
// This helper ensures we always get the fresh token data instead of stale references
|
|
100
|
+
const getCurrentSelectedToken = (): SimBalanceItem | null => {
|
|
101
|
+
if (!selectedToken || !simBalance?.balances) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const found = simBalance.balances.find(
|
|
106
|
+
token => token.chain_id === selectedToken.chain_id && token.address === selectedToken.address,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return found || null;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// ==================================================================================
|
|
113
|
+
// === RENDER FUNCTIONS (NOT COMPONENTS!) ===
|
|
114
|
+
// ==================================================================================
|
|
115
|
+
//
|
|
116
|
+
// 🚨 CRITICAL ARCHITECTURE DECISION:
|
|
117
|
+
// These are render functions, NOT component functions with useCallback!
|
|
118
|
+
//
|
|
119
|
+
// WHY THIS WORKS:
|
|
120
|
+
// ✅ Stable wrapper <div> elements with consistent keys
|
|
121
|
+
// ✅ React never sees these as "new components"
|
|
122
|
+
// ✅ Input focus is preserved during transitions
|
|
123
|
+
// ✅ No component remounting issues
|
|
124
|
+
//
|
|
125
|
+
// WHY useCallback DIDN'T WORK:
|
|
126
|
+
// ❌ useCallback(() => <JSX />, [deps]) still creates new component instances
|
|
127
|
+
// ❌ React treats each render as a different component
|
|
128
|
+
// ❌ Causes remounting and focus loss
|
|
129
|
+
//
|
|
130
|
+
// THE PATTERN:
|
|
131
|
+
// Instead of: {[<ComponentA />, <ComponentB />]}
|
|
132
|
+
// We use: {[<div key="a">{renderA()}</div>, <div key="b">{renderB()}</div>]}
|
|
133
|
+
// ==================================================================================
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Renders the send token form panel
|
|
137
|
+
* Includes recipient input, amount input with NumericFormat, and percentage buttons
|
|
138
|
+
*/
|
|
139
|
+
const renderSendTokenPanel = () => {
|
|
140
|
+
// Get fresh token data to prevent stale references
|
|
141
|
+
const currentToken = getCurrentSelectedToken();
|
|
142
|
+
|
|
143
|
+
// 🔧 SINGLE FALLBACK STRATEGY:
|
|
144
|
+
// Use fresh token data when available, fall back to selectedToken if needed
|
|
145
|
+
// This prevents duplication of "currentToken || selectedToken" throughout the component
|
|
146
|
+
const displayToken = currentToken || selectedToken;
|
|
147
|
+
|
|
148
|
+
// Handle percentage button clicks (25%, 50%, 75%, 100%)
|
|
149
|
+
const handlePercentageClick = (percentage: number) => {
|
|
150
|
+
if (displayToken) {
|
|
151
|
+
const tokenBalance = (BigInt(displayToken.amount) * BigInt(percentage)) / BigInt(100);
|
|
152
|
+
const amount = formatTokenAmount(tokenBalance, displayToken.decimals, 30, false);
|
|
153
|
+
setSendAmount(amount);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Execute token transfer transaction
|
|
158
|
+
const handleSend = async () => {
|
|
159
|
+
if (!displayToken || !recipientAddress || !sendAmount || parseFloat(sendAmount) <= 0) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
setIsSending(true);
|
|
164
|
+
|
|
165
|
+
const amountInWei = parseUnits(sendAmount, displayToken.decimals);
|
|
166
|
+
|
|
167
|
+
// Prepare analytics event data
|
|
168
|
+
const analyticsData = {
|
|
169
|
+
amount: sendAmount,
|
|
170
|
+
symbol: displayToken.symbol,
|
|
171
|
+
chain_id: displayToken.chain_id,
|
|
172
|
+
address: displayToken.address,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
invariant(isAddress(recipientAddress), "Recipient address is not a valid address");
|
|
177
|
+
|
|
178
|
+
const sendTokenData = encodeFunctionData({
|
|
179
|
+
abi: erc20Abi,
|
|
180
|
+
functionName: "transfer",
|
|
181
|
+
args: [recipientAddress, amountInWei],
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const tx = await switchChainAndExecute(displayToken.chain_id, {
|
|
185
|
+
to: displayToken.address === "native" ? recipientAddress : displayToken.address,
|
|
186
|
+
data: sendTokenData,
|
|
187
|
+
value: displayToken.address === "native" ? amountInWei : BigInt(0),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (tx) {
|
|
191
|
+
// Track successful send
|
|
192
|
+
sendAnalyticsEvent("send_token_button_click", {
|
|
193
|
+
...analyticsData,
|
|
194
|
+
success: true,
|
|
195
|
+
tx: getExplorerTxUrl(displayToken.chain_id, tx),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Reset form
|
|
199
|
+
setSendAmount("");
|
|
200
|
+
}
|
|
201
|
+
} catch (error: any) {
|
|
202
|
+
// Track failed send
|
|
203
|
+
sendAnalyticsEvent("send_token_button_click", {
|
|
204
|
+
...analyticsData,
|
|
205
|
+
success: false,
|
|
206
|
+
reason: error.message || "Unknown error",
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Error
|
|
210
|
+
toast.error(`Failed to send ${displayToken.symbol}: ${error.message || "Unknown error"}`);
|
|
211
|
+
} finally {
|
|
212
|
+
// Wait 1 second to make sure the tx is indexed on sim api.
|
|
213
|
+
setTimeout(async () => {
|
|
214
|
+
// Force refetch to bypass cache and get fresh balance data
|
|
215
|
+
await refetchSimBalance();
|
|
216
|
+
}, 1000);
|
|
217
|
+
setIsSending(false);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Show loading state only if no token data is available at all
|
|
222
|
+
if (!displayToken) {
|
|
223
|
+
return (
|
|
224
|
+
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
225
|
+
<div className="bg-b3-line/50 mb-4 rounded-full p-4">
|
|
226
|
+
<Loader2 className="text-b3-foreground-muted h-8 w-8 animate-spin" />
|
|
227
|
+
</div>
|
|
228
|
+
<h3 className="text-b3-grey font-neue-montreal-semibold mb-2">Loading token data...</h3>
|
|
229
|
+
<p className="text-b3-foreground-muted font-neue-montreal-medium text-sm">
|
|
230
|
+
Please wait while we fetch the latest information
|
|
231
|
+
</p>
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<div className="space-y-6">
|
|
238
|
+
{/* Header */}
|
|
239
|
+
<div className="flex items-center gap-3">
|
|
240
|
+
<Button
|
|
241
|
+
variant="ghost"
|
|
242
|
+
size="icon"
|
|
243
|
+
onClick={() => {
|
|
244
|
+
animationDirection.current = "back";
|
|
245
|
+
setTokenPanelView(TokenPanelView.LIST);
|
|
246
|
+
}}
|
|
247
|
+
className="hover:bg-b3-line/60"
|
|
248
|
+
disabled={isSending}
|
|
249
|
+
>
|
|
250
|
+
<ArrowLeft className="h-5 w-5" />
|
|
251
|
+
</Button>
|
|
252
|
+
<div className="flex items-center gap-3">
|
|
253
|
+
<div className="flex h-8 w-8 items-center justify-center">
|
|
254
|
+
{ALL_CHAINS[displayToken.chain_id]?.logoUrl ? (
|
|
255
|
+
<ChainTokenIcon
|
|
256
|
+
chainUrl={ALL_CHAINS[displayToken.chain_id].logoUrl}
|
|
257
|
+
tokenUrl={displayToken.token_metadata?.logo}
|
|
258
|
+
className="size-8"
|
|
259
|
+
/>
|
|
260
|
+
) : (
|
|
261
|
+
<CircleHelp className="text-b3-react-foreground size-8" />
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
<h2 className="text-b3-grey font-neue-montreal-semibold text-lg">Send {displayToken.symbol}</h2>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
{/* Recipient Address */}
|
|
269
|
+
<div className="space-y-2">
|
|
270
|
+
<label className="text-b3-grey font-neue-montreal-medium text-sm">Recipient Address</label>
|
|
271
|
+
<div className="space-y-1">
|
|
272
|
+
<div className="relative">
|
|
273
|
+
<input
|
|
274
|
+
type="text"
|
|
275
|
+
value={recipientAddress}
|
|
276
|
+
onChange={e => handleRecipientAddressChange(e.target.value)}
|
|
277
|
+
placeholder="Enter wallet address (0x...)"
|
|
278
|
+
className={`border-b3-line bg-b3-background text-b3-grey font-neue-montreal-medium placeholder:text-b3-foreground-muted w-full rounded-xl border px-4 py-3 pr-12 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 ${
|
|
279
|
+
addressError ? "border-red-500 focus:border-red-500" : "focus:border-b3-primary-blue"
|
|
280
|
+
}`}
|
|
281
|
+
disabled={isSending}
|
|
282
|
+
/>
|
|
283
|
+
<Button
|
|
284
|
+
variant="ghost"
|
|
285
|
+
size="icon"
|
|
286
|
+
className="hover:bg-b3-line/60 absolute right-2 top-1/2 h-8 w-8 -translate-y-1/2"
|
|
287
|
+
disabled={isSending}
|
|
288
|
+
onClick={() => {
|
|
289
|
+
navigator.clipboard.readText().then(text => {
|
|
290
|
+
handleRecipientAddressChange(text);
|
|
291
|
+
});
|
|
292
|
+
}}
|
|
293
|
+
>
|
|
294
|
+
<Copy className="h-4 w-4" />
|
|
295
|
+
</Button>
|
|
296
|
+
</div>
|
|
297
|
+
{addressError && <p className="font-neue-montreal-medium text-xs text-red-500">{addressError}</p>}
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
{/* Amount */}
|
|
302
|
+
<div className="space-y-2">
|
|
303
|
+
<label className="text-b3-grey font-neue-montreal-medium text-sm">Amount</label>
|
|
304
|
+
<div className="space-y-3">
|
|
305
|
+
<NumericFormat
|
|
306
|
+
decimalSeparator="."
|
|
307
|
+
allowedDecimalSeparators={[","]}
|
|
308
|
+
thousandSeparator
|
|
309
|
+
inputMode="decimal"
|
|
310
|
+
autoComplete="off"
|
|
311
|
+
autoCorrect="off"
|
|
312
|
+
type="text"
|
|
313
|
+
placeholder="0.00"
|
|
314
|
+
minLength={1}
|
|
315
|
+
maxLength={30}
|
|
316
|
+
spellCheck="false"
|
|
317
|
+
className="border-b3-line bg-b3-background text-b3-grey font-neue-montreal-medium placeholder:text-b3-foreground-muted focus:border-b3-primary-blue w-full rounded-xl border px-4 py-3 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
318
|
+
pattern="^[0-9]*[.,]?[0-9]*$"
|
|
319
|
+
disabled={isSending}
|
|
320
|
+
value={sendAmount}
|
|
321
|
+
allowNegative={false}
|
|
322
|
+
onChange={e => setSendAmount(e.currentTarget.value)}
|
|
323
|
+
/>
|
|
324
|
+
|
|
325
|
+
{/* Percentage buttons */}
|
|
326
|
+
<div className="grid grid-cols-4 gap-2">
|
|
327
|
+
{[25, 50, 75, 100].map(percentage => (
|
|
328
|
+
<Button
|
|
329
|
+
key={percentage}
|
|
330
|
+
variant="outline"
|
|
331
|
+
onClick={() => handlePercentageClick(percentage)}
|
|
332
|
+
className="hover:bg-b3-primary-wash border-b3-line text-b3-grey font-neue-montreal-medium text-sm"
|
|
333
|
+
disabled={isSending}
|
|
334
|
+
>
|
|
335
|
+
{percentage}%
|
|
336
|
+
</Button>
|
|
337
|
+
))}
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
{/* Available balance */}
|
|
341
|
+
<div className="text-b3-foreground-muted font-neue-montreal-medium text-sm">
|
|
342
|
+
Available: {formatTokenAmount(BigInt(displayToken.amount), displayToken.decimals)} {displayToken.symbol}
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
{/* Send Button */}
|
|
348
|
+
<Button
|
|
349
|
+
onClick={handleSend}
|
|
350
|
+
disabled={
|
|
351
|
+
!recipientAddress ||
|
|
352
|
+
!sendAmount ||
|
|
353
|
+
parseFloat(sendAmount) <= 0 ||
|
|
354
|
+
isSending ||
|
|
355
|
+
!!addressError ||
|
|
356
|
+
!isAddress(recipientAddress)
|
|
357
|
+
}
|
|
358
|
+
className="bg-b3-primary-blue hover:bg-b3-primary-blue/90 font-neue-montreal-semibold disabled:bg-b3-line disabled:text-b3-foreground-muted w-full rounded-xl py-3 text-white"
|
|
359
|
+
>
|
|
360
|
+
{isSending ? (
|
|
361
|
+
<>
|
|
362
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
363
|
+
Sending...
|
|
364
|
+
</>
|
|
365
|
+
) : (
|
|
366
|
+
<>
|
|
367
|
+
<Send className="mr-2 h-4 w-4" />
|
|
368
|
+
Send {displayToken.symbol}
|
|
369
|
+
</>
|
|
370
|
+
)}
|
|
371
|
+
</Button>
|
|
372
|
+
</div>
|
|
373
|
+
);
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
// Skeleton loading component for token list
|
|
377
|
+
const LoadingIndicator = () => (
|
|
378
|
+
<div className="space-y-4">
|
|
379
|
+
<div className="space-y-1">
|
|
380
|
+
{[...Array(3)].map((_, index) => (
|
|
381
|
+
<div key={index} className="flex items-center justify-between rounded-xl p-3">
|
|
382
|
+
<div className="flex items-center gap-3">
|
|
383
|
+
<div className="bg-b3-line h-10 w-10 animate-pulse rounded-full"></div>
|
|
384
|
+
<div>
|
|
385
|
+
<div className="bg-b3-line mb-1 h-4 w-16 animate-pulse rounded"></div>
|
|
386
|
+
<div className="bg-b3-line h-3 w-24 animate-pulse rounded"></div>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
<div className="text-right">
|
|
390
|
+
<div className="bg-b3-line mb-1 h-4 w-20 animate-pulse rounded"></div>
|
|
391
|
+
<div className="bg-b3-line h-3 w-16 animate-pulse rounded"></div>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
))}
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Renders the token list panel with smart filtering
|
|
401
|
+
* Features intelligent token display logic to reduce noise while ensuring visibility
|
|
402
|
+
*/
|
|
403
|
+
const renderTokenListPanel = () => {
|
|
404
|
+
// Show loading indicator when balance is loading
|
|
405
|
+
if (isLoadingBalance) {
|
|
406
|
+
return <LoadingIndicator />;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Show empty state when no account or no balance data
|
|
410
|
+
if (!account?.address || !simBalance) {
|
|
411
|
+
return (
|
|
412
|
+
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
413
|
+
<div className="bg-b3-line/50 mb-4 rounded-full p-4">
|
|
414
|
+
<Loader2 className="text-b3-foreground-muted h-8 w-8" />
|
|
415
|
+
</div>
|
|
416
|
+
<h3 className="text-b3-grey font-neue-montreal-semibold mb-2">No wallet connected</h3>
|
|
417
|
+
<p className="text-b3-foreground-muted font-neue-montreal-medium text-sm">
|
|
418
|
+
Connect your wallet to view token balances
|
|
419
|
+
</p>
|
|
420
|
+
</div>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// === SMART FILTERING LOGIC ===
|
|
425
|
+
// Filter tokens with value >= $1 to reduce noise from dust tokens
|
|
426
|
+
const filteredTokens =
|
|
427
|
+
simBalance?.balances.filter(token => token.value_usd !== undefined && token.value_usd >= 1) || [];
|
|
428
|
+
|
|
429
|
+
// 🧠 INTELLIGENT DISPLAY LOGIC:
|
|
430
|
+
// Show all tokens automatically when filtering would be unhelpful:
|
|
431
|
+
// 1. User explicitly requested to show all, OR
|
|
432
|
+
// 2. No tokens with value >= $1 (user has only dust), OR
|
|
433
|
+
// 3. 5 or fewer tokens with value >= $1 (not enough to warrant filtering)
|
|
434
|
+
const shouldShowAllTokens = showAllTokens || filteredTokens.length === 0 || filteredTokens.length <= 5;
|
|
435
|
+
const tokensToShow = shouldShowAllTokens ? simBalance?.balances || [] : filteredTokens;
|
|
436
|
+
const hasHiddenTokens = !shouldShowAllTokens && (simBalance?.balances.length || 0) > filteredTokens.length;
|
|
437
|
+
|
|
438
|
+
// Handle token selection and navigate to send form
|
|
439
|
+
const handleTokenClick = (token: SimBalanceItem) => {
|
|
440
|
+
setSelectedToken(token);
|
|
441
|
+
animationDirection.current = "forward"; // Set animation direction BEFORE state change
|
|
442
|
+
setTokenPanelView(TokenPanelView.SEND);
|
|
443
|
+
// Reset form when selecting a new token
|
|
444
|
+
setRecipientAddress("");
|
|
445
|
+
setSendAmount("");
|
|
446
|
+
setIsSending(false);
|
|
447
|
+
setAddressError("");
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// Show empty state when no tokens are available
|
|
451
|
+
if (tokensToShow.length === 0) {
|
|
452
|
+
return (
|
|
453
|
+
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
454
|
+
<div className="bg-b3-line/50 mb-4 rounded-full p-4">
|
|
455
|
+
<CircleHelp className="text-b3-foreground-muted h-8 w-8" />
|
|
456
|
+
</div>
|
|
457
|
+
<h3 className="text-b3-grey font-neue-montreal-semibold mb-2">No tokens found</h3>
|
|
458
|
+
<p className="text-b3-foreground-muted font-neue-montreal-medium text-sm">
|
|
459
|
+
No token balances found in your wallet.
|
|
460
|
+
</p>
|
|
461
|
+
</div>
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return (
|
|
466
|
+
<div className="space-y-4">
|
|
467
|
+
<div className="space-y-1">
|
|
468
|
+
{tokensToShow.map(token => (
|
|
469
|
+
<div
|
|
470
|
+
key={token.chain_id + "_" + token.address}
|
|
471
|
+
className="hover:bg-b3-line/60 dark:hover:bg-b3-primary-wash/40 group flex cursor-pointer items-center justify-between rounded-xl p-3 transition-all duration-200"
|
|
472
|
+
onClick={() => handleTokenClick(token)}
|
|
473
|
+
>
|
|
474
|
+
<div className="flex items-center gap-3">
|
|
475
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-full">
|
|
476
|
+
{ALL_CHAINS[token.chain_id]?.logoUrl ? (
|
|
477
|
+
<ChainTokenIcon
|
|
478
|
+
chainUrl={ALL_CHAINS[token.chain_id].logoUrl}
|
|
479
|
+
tokenUrl={token.token_metadata?.logo}
|
|
480
|
+
className="size-10"
|
|
481
|
+
/>
|
|
482
|
+
) : (
|
|
483
|
+
<CircleHelp className="text-b3-react-foreground size-10" />
|
|
484
|
+
)}
|
|
485
|
+
</div>
|
|
486
|
+
<div>
|
|
487
|
+
<div className="flex items-center gap-2">
|
|
488
|
+
<span className="text-b3-grey font-neue-montreal-semibold transition-colors duration-200 group-hover:font-bold group-hover:text-black">
|
|
489
|
+
{token.symbol}
|
|
490
|
+
</span>
|
|
491
|
+
</div>
|
|
492
|
+
<div className="text-b3-foreground-muted font-neue-montreal-medium text-sm transition-colors duration-200 group-hover:text-gray-700">
|
|
493
|
+
{token.name}
|
|
494
|
+
</div>
|
|
495
|
+
</div>
|
|
496
|
+
</div>
|
|
497
|
+
<div className="text-right">
|
|
498
|
+
<div className="text-b3-grey font-neue-montreal-semibold transition-colors duration-200 group-hover:font-bold group-hover:text-black">
|
|
499
|
+
{formatTokenAmount(BigInt(token.amount), token.decimals)}
|
|
500
|
+
</div>
|
|
501
|
+
<div className="text-b3-foreground-muted font-neue-montreal-medium text-sm transition-colors duration-200 group-hover:text-gray-700">
|
|
502
|
+
{formatDisplayNumber(token.value_usd, { style: "currency", fractionDigits: 2 })}
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
))}
|
|
507
|
+
</div>
|
|
508
|
+
|
|
509
|
+
{hasHiddenTokens && !showAllTokens && (
|
|
510
|
+
<div className="flex justify-center">
|
|
511
|
+
<Button
|
|
512
|
+
variant="ghost"
|
|
513
|
+
className="text-b3-primary-blue hover:text-b3-primary-blue/80 font-neue-montreal-semibold text-sm"
|
|
514
|
+
onClick={() => setShowAllTokens(true)}
|
|
515
|
+
>
|
|
516
|
+
Show more
|
|
517
|
+
</Button>
|
|
518
|
+
</div>
|
|
519
|
+
)}
|
|
520
|
+
</div>
|
|
521
|
+
);
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// === ANIMATION CONFIGURATION ===
|
|
525
|
+
// Memoize variants to prevent re-creation and unwanted re-renders
|
|
526
|
+
const variants = useMemo(
|
|
527
|
+
() => ({
|
|
528
|
+
enter: (direction: "forward" | "back" | null) => ({
|
|
529
|
+
x: direction === "back" ? -300 : 300, // Back: slide from left, Forward: slide from right
|
|
530
|
+
opacity: 0,
|
|
531
|
+
}),
|
|
532
|
+
center: { x: 0, opacity: 1 }, // Final position: centered and visible
|
|
533
|
+
exit: (direction: "forward" | "back" | null) => ({
|
|
534
|
+
x: direction === "back" ? 300 : -300, // Back: slide to right, Forward: slide to left
|
|
535
|
+
opacity: 0,
|
|
536
|
+
}),
|
|
537
|
+
}),
|
|
538
|
+
[],
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
// Memoize transition config for consistent spring animation
|
|
542
|
+
const transition = useMemo(
|
|
543
|
+
() => ({
|
|
544
|
+
type: "spring" as const,
|
|
545
|
+
stiffness: 300, // Spring tension
|
|
546
|
+
damping: 30, // Spring damping
|
|
547
|
+
}),
|
|
548
|
+
[],
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
return (
|
|
552
|
+
<TransitionPanel
|
|
553
|
+
activeIndex={tokenPanelView}
|
|
554
|
+
className="min-h-[400px]"
|
|
555
|
+
custom={animationDirection.current} // Pass direction to functional variants
|
|
556
|
+
variants={variants}
|
|
557
|
+
transition={transition}
|
|
558
|
+
>
|
|
559
|
+
{/*
|
|
560
|
+
🎯 THE STABLE ELEMENT PATTERN:
|
|
561
|
+
Using stable <div> wrappers with consistent keys ensures React never remounts
|
|
562
|
+
the containers, preserving input focus during animations.
|
|
563
|
+
The content inside can change freely via render functions.
|
|
564
|
+
*/}
|
|
565
|
+
{[<div key="token-list">{renderTokenListPanel()}</div>, <div key="send-token">{renderSendTokenPanel()}</div>]}
|
|
566
|
+
</TransitionPanel>
|
|
567
|
+
);
|
|
568
|
+
}
|