@openfort/react 1.0.3 → 1.0.5

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 (29) hide show
  1. package/build/components/ConnectModal/ConnectWithOAuth.js +6 -3
  2. package/build/components/ConnectModal/ConnectWithOAuth.js.map +1 -1
  3. package/build/components/Openfort/types.d.ts +3 -0
  4. package/build/components/Pages/AssetInventory/index.js +127 -48
  5. package/build/components/Pages/AssetInventory/index.js.map +1 -1
  6. package/build/components/Pages/CreateWallet/index.js +32 -3
  7. package/build/components/Pages/CreateWallet/index.js.map +1 -1
  8. package/build/components/Pages/LoadWallets/index.js +5 -4
  9. package/build/components/Pages/LoadWallets/index.js.map +1 -1
  10. package/build/components/Pages/Loading/index.js +10 -4
  11. package/build/components/Pages/Loading/index.js.map +1 -1
  12. package/build/components/Pages/SelectToken/styles.d.ts +13 -0
  13. package/build/components/Pages/SelectToken/styles.js +103 -1
  14. package/build/components/Pages/SelectToken/styles.js.map +1 -1
  15. package/build/components/Pages/SendConfirmation/index.js +7 -0
  16. package/build/components/Pages/SendConfirmation/index.js.map +1 -1
  17. package/build/constants/logos.d.ts +2 -0
  18. package/build/constants/logos.js +58 -0
  19. package/build/constants/logos.js.map +1 -0
  20. package/build/ethereum/hooks/useEthereumWalletAssets.d.ts +25 -11
  21. package/build/ethereum/hooks/useEthereumWalletAssets.js +134 -14
  22. package/build/ethereum/hooks/useEthereumWalletAssets.js.map +1 -1
  23. package/build/index.d.ts +1 -1
  24. package/build/openfort/hooks/useEmbeddedStateMachine.js +4 -1
  25. package/build/openfort/hooks/useEmbeddedStateMachine.js.map +1 -1
  26. package/build/utils/index.d.ts +1 -1
  27. package/build/version.d.ts +1 -1
  28. package/build/version.js +1 -1
  29. package/package.json +1 -1
@@ -73,9 +73,12 @@ const ConnectWithOAuth = () => {
73
73
  case states.REDIRECT: {
74
74
  if (hasProvider)
75
75
  return;
76
- const cleanURL = win.location.origin + win.location.pathname;
76
+ const baseURL = win.location.origin + win.location.pathname;
77
+ const hash = win.location.hash;
77
78
  const queryParams = Object.fromEntries([...url.searchParams.entries()].filter(([key]) => ['openfortAuthProviderUI', 'refresh_token', 'access_token', 'player_id'].includes(key)));
78
79
  queryParams.openfortAuthProviderUI = provider;
80
+ // Query params must come before the hash fragment in a valid URL
81
+ const redirectTo = `${baseURL}?${new URLSearchParams(queryParams).toString()}${hash}`;
79
82
  try {
80
83
  if (user) {
81
84
  const authToken = await client.getAccessToken();
@@ -86,7 +89,7 @@ const ConnectWithOAuth = () => {
86
89
  }
87
90
  const linkResponse = await client.auth.initLinkOAuth({
88
91
  provider,
89
- redirectTo: `${cleanURL}?${new URLSearchParams(queryParams).toString()}`,
92
+ redirectTo,
90
93
  });
91
94
  logger.log(linkResponse);
92
95
  win.location.href = linkResponse;
@@ -94,7 +97,7 @@ const ConnectWithOAuth = () => {
94
97
  else {
95
98
  const r = await client.auth.initOAuth({
96
99
  provider,
97
- redirectTo: `${cleanURL}?${new URLSearchParams(queryParams).toString()}`,
100
+ redirectTo,
98
101
  });
99
102
  logger.log(r);
100
103
  win.location.href = r;
@@ -1 +1 @@
1
- {"version":3,"file":"ConnectWithOAuth.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"ConnectWithOAuth.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -372,6 +372,9 @@ export type Asset = {
372
372
  };
373
373
  raw?: getAssets.Erc20Asset;
374
374
  };
375
+ export type MultiChainAsset = Asset & {
376
+ chainId: number;
377
+ };
375
378
  export type SendFormState = {
376
379
  recipient: string;
377
380
  amount: string;
@@ -1,10 +1,14 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { motion } from 'framer-motion';
3
+ import { useState, useEffect, useMemo } from 'react';
2
4
  import { formatUnits } from 'viem';
5
+ import { TOKEN_LOGO, symbolToColor } from '../../../constants/logos.js';
3
6
  import { useEthereumWalletAssets } from '../../../ethereum/hooks/useEthereumWalletAssets.js';
7
+ import Chain from '../../Common/Chain/index.js';
4
8
  import { ModalHeading } from '../../Common/Modal/styles.js';
5
- import { EmptyState } from '../BuyProviderSelect/styles.js';
6
- import { SelectTokenContent, TokenList, TokenContainer, TokenInfo, TokenSymbol, TokenBalance, TokenName } from '../SelectToken/styles.js';
7
- import { getAssetSymbol, getAssetDecimals, formatBalanceWithSymbol } from '../Send/utils.js';
9
+ import { useOpenfort } from '../../Openfort/useOpenfort.js';
10
+ import { SelectTokenContent, EmptyState, ContentWrapper, ChainGroup, ChainGroupHeader, TokenPill, TokenPillSymbol, InfoLink, TokenList, TokenContainer, TokenLeftGroup, TokenInfo, TokenSymbol, TokenName, TokenBalance, TokenLogoArea, TokenLogoImg, TokenLogoFallback, ChainBadge } from '../SelectToken/styles.js';
11
+ import { getAssetSymbol, getAssetDecimals } from '../Send/utils.js';
8
12
 
9
13
  const ZERO = BigInt(0);
10
14
  const usdFormatter = new Intl.NumberFormat('en-US', {
@@ -13,53 +17,128 @@ const usdFormatter = new Intl.NumberFormat('en-US', {
13
17
  minimumFractionDigits: 2,
14
18
  maximumFractionDigits: 2,
15
19
  });
16
- const AssetInventory = () => {
17
- const { data: walletAssets, isLoading: isBalancesLoading } = useEthereumWalletAssets();
18
- // Show all tokens for both buy and send flows
19
- const selectableTokens = walletAssets || [];
20
- const renderContent = () => {
21
- if (!selectableTokens.length) {
22
- if (isBalancesLoading) {
23
- return jsx(EmptyState, { children: "Loading balances\u2026" });
20
+ const priceFormatter = new Intl.NumberFormat('en-US', {
21
+ style: 'currency',
22
+ currency: 'USD',
23
+ minimumFractionDigits: 2,
24
+ maximumFractionDigits: 4,
25
+ });
26
+ function getTokenLogoUrl(token) {
27
+ var _a;
28
+ const symbol = getAssetSymbol(token).toUpperCase();
29
+ return (_a = TOKEN_LOGO[symbol]) !== null && _a !== void 0 ? _a : null;
30
+ }
31
+ function TokenLogo({ token }) {
32
+ const [imgError, setImgError] = useState(false);
33
+ const symbol = getAssetSymbol(token);
34
+ const logoUrl = getTokenLogoUrl(token);
35
+ return (jsxs(TokenLogoArea, { children: [logoUrl && !imgError ? (jsx(TokenLogoImg, { src: logoUrl, alt: symbol, onError: () => setImgError(true) })) : (jsx(TokenLogoFallback, { "$bg": symbolToColor(symbol), children: symbol.charAt(0).toUpperCase() })), jsx(ChainBadge, { children: jsx(Chain, { id: token.chainId, unsupported: false, size: 14 }) })] }));
36
+ }
37
+ function renderTokenRow(token) {
38
+ var _a, _b, _c, _d;
39
+ const key = token.type === 'erc20' ? `${token.chainId}-${token.address}` : `${token.chainId}-native`;
40
+ const displaySymbol = getAssetSymbol(token);
41
+ const displayName = ((_a = token.metadata) === null || _a === void 0 ? void 0 : _a.name) || displaySymbol || 'Unknown Token';
42
+ const decimals = getAssetDecimals(token);
43
+ const pricePerToken = (_c = (_b = token.metadata) === null || _b === void 0 ? void 0 : _b.fiat) === null || _c === void 0 ? void 0 : _c.value;
44
+ let usdValue = null;
45
+ let balanceNum = '';
46
+ let priceDisplay = null;
47
+ const isBalanceLoaded = token.balance !== undefined;
48
+ const hasZeroBalance = isBalanceLoaded && ((_d = token.balance) !== null && _d !== void 0 ? _d : ZERO) <= ZERO;
49
+ if (hasZeroBalance)
50
+ return null;
51
+ if (isBalanceLoaded && token.balance !== undefined) {
52
+ const amount = parseFloat(formatUnits(token.balance, decimals));
53
+ if (Number.isFinite(amount)) {
54
+ balanceNum = `${amount.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 4 })} ${displaySymbol}`;
55
+ if (pricePerToken !== undefined) {
56
+ const totalUsd = amount * pricePerToken;
57
+ if (totalUsd >= 0.01) {
58
+ usdValue = usdFormatter.format(totalUsd);
59
+ }
60
+ else if (totalUsd > 0) {
61
+ usdValue = '<$0.01';
62
+ }
63
+ else {
64
+ usdValue = usdFormatter.format(0);
65
+ }
66
+ priceDisplay = `@${priceFormatter.format(pricePerToken)}`;
24
67
  }
25
- return jsx(EmptyState, { children: "No supported tokens found for this network yet." });
26
68
  }
27
- return (jsx(TokenList, { children: selectableTokens.map((token) => {
28
- var _a, _b, _c, _d, _e;
29
- const key = token.type === 'erc20' ? token.address : 'native';
30
- const displaySymbol = getAssetSymbol(token);
31
- const displayName = ((_a = token.metadata) === null || _a === void 0 ? void 0 : _a.name) || displaySymbol || 'Unknown Token';
32
- const decimals = getAssetDecimals(token);
33
- const pricePerToken = (_c = (_b = token.metadata) === null || _b === void 0 ? void 0 : _b.fiat) === null || _c === void 0 ? void 0 : _c.value;
34
- let usdValue = null;
35
- // Show loading state for balances
36
- const isBalanceLoaded = token.balance !== undefined;
37
- const balanceDisplay = isBalanceLoaded
38
- ? formatBalanceWithSymbol(token.balance, decimals, ((_d = token.metadata) === null || _d === void 0 ? void 0 : _d.symbol) || '')
39
- : 'Loading...';
40
- // Check if token has zero balance (for send flow opacity)
41
- const hasZeroBalance = isBalanceLoaded && ((_e = token.balance) !== null && _e !== void 0 ? _e : ZERO) <= ZERO;
42
- if (hasZeroBalance)
43
- return null;
44
- if (isBalanceLoaded && pricePerToken !== undefined && token.balance !== undefined) {
45
- const amount = parseFloat(formatUnits(token.balance, decimals));
46
- if (Number.isFinite(amount)) {
47
- const totalUsd = amount * pricePerToken;
48
- if (totalUsd >= 0.01) {
49
- usdValue = usdFormatter.format(totalUsd);
50
- }
51
- else if (totalUsd > 0) {
52
- usdValue = '<$0.01';
53
- }
54
- else {
55
- usdValue = usdFormatter.format(0);
56
- }
57
- }
58
- }
59
- return (jsxs(TokenContainer, { children: [jsx(TokenInfo, { children: jsx(TokenSymbol, { children: displayName }) }), jsxs(TokenInfo, { children: [jsx(TokenBalance, { children: balanceDisplay }), usdValue ? jsx(TokenName, { style: { textAlign: 'end' }, children: usdValue }) : null] })] }, key));
60
- }) }));
61
- };
62
- return (jsxs(SelectTokenContent, { children: [jsx(ModalHeading, { children: "Your assets" }), renderContent()] }));
69
+ }
70
+ return (jsxs(TokenContainer, { children: [jsxs(TokenLeftGroup, { children: [jsx(TokenLogo, { token: token }), jsxs(TokenInfo, { style: { textAlign: 'left' }, children: [jsx(TokenSymbol, { children: displayName }), jsx(TokenName, { children: balanceNum || 'Loading...' })] })] }), jsxs(TokenInfo, { children: [usdValue ? jsx(TokenBalance, { children: usdValue }) : null, priceDisplay ? jsx(TokenName, { style: { textAlign: 'end' }, children: priceDisplay }) : null] })] }, key));
71
+ }
72
+ const PILL_LOGO_SIZE = 16;
73
+ function PillLogo({ symbol }) {
74
+ var _a;
75
+ const [imgError, setImgError] = useState(false);
76
+ const url = (_a = TOKEN_LOGO[symbol.toUpperCase()]) !== null && _a !== void 0 ? _a : null;
77
+ if (!url || imgError) {
78
+ return (jsx("span", { style: {
79
+ width: PILL_LOGO_SIZE,
80
+ height: PILL_LOGO_SIZE,
81
+ borderRadius: '50%',
82
+ background: symbolToColor(symbol),
83
+ display: 'inline-flex',
84
+ alignItems: 'center',
85
+ justifyContent: 'center',
86
+ fontSize: 9,
87
+ fontWeight: 700,
88
+ color: '#fff',
89
+ flexShrink: 0,
90
+ }, children: symbol.charAt(0).toUpperCase() }));
91
+ }
92
+ return (jsx("img", { src: url, alt: symbol, onError: () => setImgError(true), style: {
93
+ width: PILL_LOGO_SIZE,
94
+ height: PILL_LOGO_SIZE,
95
+ borderRadius: '50%',
96
+ objectFit: 'cover',
97
+ flexShrink: 0,
98
+ } }));
99
+ }
100
+ const AssetInventory = () => {
101
+ var _a;
102
+ const { data, multiChain, isLoading: isBalancesLoading } = useEthereumWalletAssets({ multiChain: true });
103
+ const { triggerResize, chains } = useOpenfort();
104
+ const [showDetails, setShowDetails] = useState(false);
105
+ useEffect(() => {
106
+ if (!isBalancesLoading)
107
+ triggerResize();
108
+ }, [isBalancesLoading]);
109
+ useEffect(() => {
110
+ triggerResize();
111
+ }, [showDetails]);
112
+ const tokens = (_a = (multiChain ? data : null)) !== null && _a !== void 0 ? _a : [];
113
+ const hasBalance = tokens.some((t) => t.balance > ZERO);
114
+ const chainNameMap = useMemo(() => {
115
+ const map = new Map();
116
+ for (const c of chains)
117
+ map.set(c.id, c.name);
118
+ return map;
119
+ }, [chains]);
120
+ const groupedByChain = useMemo(() => {
121
+ var _a;
122
+ const groups = new Map();
123
+ for (const t of tokens) {
124
+ if (!groups.has(t.chainId))
125
+ groups.set(t.chainId, []);
126
+ groups.get(t.chainId).push({
127
+ symbol: getAssetSymbol(t),
128
+ name: ((_a = t.metadata) === null || _a === void 0 ? void 0 : _a.name) || getAssetSymbol(t),
129
+ });
130
+ }
131
+ return groups;
132
+ }, [tokens]);
133
+ if (isBalancesLoading) {
134
+ return (jsxs(SelectTokenContent, { children: [jsx(ModalHeading, { children: "Your assets" }), jsx(EmptyState, { children: "Loading balances..." })] }));
135
+ }
136
+ if (showDetails) {
137
+ return (jsxs(SelectTokenContent, { onBack: () => {
138
+ setShowDetails(false);
139
+ }, children: [jsx(ModalHeading, { children: "Configured assets" }), jsx(motion.div, { initial: { opacity: 0, scale: 1.1 }, animate: { opacity: 1, scale: 1 }, transition: { duration: 0.2, ease: [0.26, 0.08, 0.25, 1] }, style: { display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }, children: jsx(ContentWrapper, { style: { overflowY: 'auto', maxHeight: 400 }, children: Array.from(groupedByChain.entries()).map(([chainId, assets]) => (jsxs(ChainGroup, { children: [jsxs(ChainGroupHeader, { children: [jsx(Chain, { id: chainId, unsupported: false, size: 18 }), chainNameMap.get(chainId) || `Chain ${chainId}`] }), assets.map((a) => (jsxs(TokenPill, { children: [jsx(PillLogo, { symbol: a.symbol }), jsx(TokenPillSymbol, { children: a.symbol }), a.name !== a.symbol && a.name] }, `${chainId}-${a.symbol}`)))] }, chainId))) }) })] }, "details"));
140
+ }
141
+ return (jsxs(SelectTokenContent, { children: [jsx(ModalHeading, { children: "Your assets" }), jsxs(ContentWrapper, { children: [jsxs(InfoLink, { type: "button", onClick: () => setShowDetails(true), children: [jsxs("svg", { role: "img", "aria-label": "Info", width: "12", height: "12", viewBox: "0 0 14 14", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [jsx("circle", { cx: "7", cy: "7", r: "6", stroke: "currentColor", strokeWidth: "1.25" }), jsx("path", { d: "M7 6.25V10", stroke: "currentColor", strokeWidth: "1.25", strokeLinecap: "round" }), jsx("circle", { cx: "7", cy: "4.25", r: "0.75", fill: "currentColor" })] }), "Only configured chains and tokens are shown"] }), jsx(TokenList, { children: hasBalance ? tokens.map(renderTokenRow) : jsx(EmptyState, { children: "No assets found" }) })] })] }, "assets"));
63
142
  };
64
143
 
65
144
  export { AssetInventory };
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -63,13 +63,14 @@ const OtherMethod = ({ currentMethod, onChangeMethod, }) => {
63
63
  };
64
64
  const CreateWalletAutomaticRecovery = ({ onBack, logoutOnBack, }) => {
65
65
  var _a;
66
- const { embeddedState } = useOpenfortCore();
66
+ const { embeddedState, isLoadingAccounts } = useOpenfortCore();
67
67
  const { setRoute, triggerResize, walletConfig } = useOpenfort();
68
68
  const [recoveryError, setRecoveryError] = useState(null);
69
69
  const { create } = useEthereumEmbeddedWallet();
70
70
  const { isEnabled: isWalletRecoveryOTPEnabled, requestOTP } = useRecoveryOTP();
71
71
  const [shouldCreateWallet, setShouldCreateWallet] = useState(false);
72
72
  const isCreatingRef = useRef(false);
73
+ const hasAttemptedCreationRef = useRef(false);
73
74
  const [needsOTP, setNeedsOTP] = useState(false);
74
75
  const [otpResponse, setOtpResponse] = useState(null);
75
76
  const [otpStatus, setOtpStatus] = useState('idle');
@@ -100,6 +101,10 @@ const CreateWalletAutomaticRecovery = ({ onBack, logoutOnBack, }) => {
100
101
  return;
101
102
  if (isCreatingRef.current)
102
103
  return;
104
+ // Wait for the state machine's fetchEmbeddedAccounts to finish before
105
+ // calling create() — concurrent SDK operations corrupt shared state.
106
+ if (isLoadingAccounts)
107
+ return;
103
108
  isCreatingRef.current = true;
104
109
  (async () => {
105
110
  logger.log('Creating wallet Automatic recover');
@@ -132,12 +137,18 @@ const CreateWalletAutomaticRecovery = ({ onBack, logoutOnBack, }) => {
132
137
  }
133
138
  triggerResize();
134
139
  })();
135
- }, [shouldCreateWallet, create, isWalletRecoveryOTPEnabled, requestOTP, triggerResize]);
140
+ }, [shouldCreateWallet, create, isWalletRecoveryOTPEnabled, requestOTP, triggerResize, isLoadingAccounts]);
136
141
  useEffect(() => {
137
142
  if (embeddedState !== EmbeddedState.EMBEDDED_SIGNER_NOT_CONFIGURED)
138
143
  return;
139
144
  if ((walletConfig === null || walletConfig === void 0 ? void 0 : walletConfig.connectOnLogin) === false)
140
145
  return;
146
+ // Guard against retry loop: when create() fails the SDK cycles the
147
+ // embeddedState back to EMBEDDED_SIGNER_NOT_CONFIGURED which re-triggers
148
+ // this effect. Only attempt creation once — the user can retry manually.
149
+ if (hasAttemptedCreationRef.current)
150
+ return;
151
+ hasAttemptedCreationRef.current = true;
141
152
  setShouldCreateWallet(true);
142
153
  }, [embeddedState, walletConfig === null || walletConfig === void 0 ? void 0 : walletConfig.connectOnLogin]);
143
154
  const handleResendClick = useCallback(() => {
@@ -160,13 +171,28 @@ const CreateWalletAutomaticRecovery = ({ onBack, logoutOnBack, }) => {
160
171
  logo: (otpResponse === null || otpResponse === void 0 ? void 0 : otpResponse.sentTo) === 'phone' ? jsx(PhoneIcon, {}) : jsx(EmailIcon, {}),
161
172
  } }), jsxs(ModalBody, { children: [jsxs(Body, { children: ["Please check ", jsx("b", { children: (otpResponse === null || otpResponse === void 0 ? void 0 : otpResponse.sentTo) === 'phone' ? otpResponse === null || otpResponse === void 0 ? void 0 : otpResponse.phone : otpResponse === null || otpResponse === void 0 ? void 0 : otpResponse.email }), " and enter your code below."] }), jsx(OtpInputStandalone, { length: 9, scale: "80%", onComplete: handleCompleteOtp, isLoading: otpStatus === 'loading', isError: otpStatus === 'error', isSuccess: otpStatus === 'success' }), jsxs(ResultContainer, { children: [otpStatus === 'success' && jsx(ModalBody, { "$valid": true, children: "Code verified successfully!" }), otpStatus === 'error' && jsx(ModalBody, { "$error": true, children: error || 'Invalid code. Please try again.' })] }), jsxs(FooterTextButton, { children: ["Didn't receive the code?", ' ', jsx(FooterButtonText, { type: "button", onClick: handleResendClick, disabled: isResendDisabled, children: sendButtonText })] })] })] }));
162
173
  }
163
- return (jsx(PageContent, { onBack: onBack, logoutOnBack: logoutOnBack, children: jsx(Loader, { isError: !!recoveryError, header: recoveryError ? 'Error creating wallet.' : `Creating wallet...`, description: recoveryError ? recoveryError.message : undefined }) }));
174
+ // When connectOnLogin is false, auto-creation is skipped show a manual
175
+ // trigger instead of an infinite spinner.
176
+ if (!shouldCreateWallet && !isCreatingRef.current && !recoveryError) {
177
+ return (jsxs(PageContent, { onBack: onBack, logoutOnBack: logoutOnBack, children: [jsx(ModalHeading, { children: "Create wallet" }), jsx(ModalBody, { style: { textAlign: 'center' }, children: "Create an embedded wallet to get started." }), jsx(Button, { onClick: () => {
178
+ hasAttemptedCreationRef.current = false;
179
+ setShouldCreateWallet(true);
180
+ }, children: "Create wallet" })] }));
181
+ }
182
+ return (jsx(PageContent, { onBack: onBack, logoutOnBack: logoutOnBack, children: jsx(Loader, { isError: !!recoveryError, header: recoveryError ? 'Error creating wallet.' : `Creating wallet...`, description: recoveryError ? recoveryError.message : undefined, onRetry: recoveryError
183
+ ? () => {
184
+ hasAttemptedCreationRef.current = false;
185
+ setRecoveryError(null);
186
+ setShouldCreateWallet(true);
187
+ }
188
+ : undefined }) }));
164
189
  };
165
190
  const CreateWalletPasskeyRecovery = ({ onChangeMethod, onBack, logoutOnBack, }) => {
166
191
  const { triggerResize, setRoute, walletConfig } = useOpenfort();
167
192
  const { create } = useEthereumEmbeddedWallet();
168
193
  const [shouldCreateWallet, setShouldCreateWallet] = useState(false);
169
194
  const isCreatingRef = useRef(false);
195
+ const hasAttemptedCreationRef = useRef(false);
170
196
  const [recoveryError, setRecoveryError] = useState(null);
171
197
  const { embeddedState } = useOpenfortCore();
172
198
  useEffect(() => {
@@ -197,6 +223,9 @@ const CreateWalletPasskeyRecovery = ({ onChangeMethod, onBack, logoutOnBack, })
197
223
  return;
198
224
  if ((walletConfig === null || walletConfig === void 0 ? void 0 : walletConfig.connectOnLogin) === false)
199
225
  return;
226
+ if (hasAttemptedCreationRef.current)
227
+ return;
228
+ hasAttemptedCreationRef.current = true;
200
229
  setShouldCreateWallet(true);
201
230
  }, [embeddedState, walletConfig === null || walletConfig === void 0 ? void 0 : walletConfig.connectOnLogin]);
202
231
  useEffect(() => {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -64,10 +64,11 @@ const LoadWallets = () => {
64
64
  }
65
65
  logger.log('User wallets loaded:', wallets.length);
66
66
  if (wallets.length === 0) {
67
- if ((walletConfig === null || walletConfig === void 0 ? void 0 : walletConfig.connectOnLogin) === false) {
68
- setRoute(routes.CONNECTED);
69
- return;
70
- }
67
+ // Always show the create wallet page when no wallets exist.
68
+ // connectOnLogin only controls whether creation starts automatically
69
+ // (handled inside CreateWalletAutomaticRecovery), not whether the
70
+ // page is shown — routing to CONNECTED with 0 wallets would show a
71
+ // broken "Connect wallet" button aimed at external wagmi wallets.
71
72
  setRoute(createRoute(chainType));
72
73
  return;
73
74
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -1,5 +1,5 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
- import { ChainTypeEnum } from '@openfort/openfort-js';
2
+ import { ChainTypeEnum, EmbeddedState } from '@openfort/openfort-js';
3
3
  import React, { useEffect } from 'react';
4
4
  import { useEthereumEmbeddedWallet } from '../../../ethereum/hooks/useEthereumEmbeddedWallet.js';
5
5
  import { useOpenfortCore } from '../../../openfort/useOpenfort.js';
@@ -11,7 +11,7 @@ import { PageContent } from '../../PageContent/index.js';
11
11
 
12
12
  const Loading = () => {
13
13
  const { setRoute, walletConfig } = useOpenfort();
14
- const { user, isLoadingAccounts, needsRecovery } = useOpenfortCore();
14
+ const { user, isLoadingAccounts, isLoading, needsRecovery, embeddedState } = useOpenfortCore();
15
15
  const { chainType } = useOpenfortCore();
16
16
  // Use chain-specific hooks
17
17
  const ethereumWallet = useEthereumEmbeddedWallet();
@@ -24,7 +24,13 @@ const Loading = () => {
24
24
  useEffect(() => {
25
25
  if (isFirstFrame)
26
26
  return;
27
- if (isLoadingAccounts)
27
+ // Wait for the SDK to settle. After storeCredentials the embedded state
28
+ // briefly stays UNAUTHENTICATED/NONE while the SDK processes the token.
29
+ // Routing to PROVIDERS here would abort the auth flow.
30
+ if (embeddedState === EmbeddedState.NONE || embeddedState === EmbeddedState.UNAUTHENTICATED)
31
+ return;
32
+ // Also wait while accounts or user are still loading.
33
+ if (isLoadingAccounts || isLoading)
28
34
  return;
29
35
  else if (!user)
30
36
  setRoute(routes.PROVIDERS);
@@ -42,7 +48,7 @@ const Loading = () => {
42
48
  }
43
49
  else
44
50
  setRoute(routes.CONNECTED);
45
- }, [isLoadingAccounts, user, address, needsRecovery, isFirstFrame, retryCount]);
51
+ }, [embeddedState, isLoadingAccounts, isLoading, user, address, needsRecovery, isFirstFrame, retryCount]);
46
52
  // Retry every 250ms
47
53
  useEffect(() => {
48
54
  const interval = setInterval(() => {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -12,4 +12,17 @@ export declare const TokenInfo: import("styled-components").StyledComponent<"div
12
12
  export declare const TokenSymbol: import("styled-components").StyledComponent<"span", any, {}, never>;
13
13
  export declare const TokenName: import("styled-components").StyledComponent<"span", any, {}, never>;
14
14
  export declare const TokenBalance: import("styled-components").StyledComponent<"span", any, {}, never>;
15
+ export declare const TokenLeftGroup: import("styled-components").StyledComponent<"div", any, {}, never>;
16
+ export declare const TokenLogoArea: import("styled-components").StyledComponent<"div", any, {}, never>;
17
+ export declare const TokenLogoFallback: import("styled-components").StyledComponent<"div", any, {
18
+ $bg: string;
19
+ }, never>;
20
+ export declare const TokenLogoImg: import("styled-components").StyledComponent<"img", any, {}, never>;
21
+ export declare const ChainBadge: import("styled-components").StyledComponent<"div", any, {}, never>;
15
22
  export declare const EmptyState: import("styled-components").StyledComponent<"div", any, {}, never>;
23
+ export declare const InfoLink: import("styled-components").StyledComponent<"button", any, {}, never>;
24
+ export declare const ContentWrapper: import("styled-components").StyledComponent<"div", any, {}, never>;
25
+ export declare const ChainGroup: import("styled-components").StyledComponent<"div", any, {}, never>;
26
+ export declare const ChainGroupHeader: import("styled-components").StyledComponent<"div", any, {}, never>;
27
+ export declare const TokenPill: import("styled-components").StyledComponent<"div", any, {}, never>;
28
+ export declare const TokenPillSymbol: import("styled-components").StyledComponent<"span", any, {}, never>;
@@ -67,12 +67,114 @@ const TokenBalance = styled.span `
67
67
  font-weight: 600;
68
68
  color: var(--ck-body-color);
69
69
  `;
70
+ const TokenLeftGroup = styled.div `
71
+ display: flex;
72
+ align-items: center;
73
+ gap: 10px;
74
+ min-width: 0;
75
+ `;
76
+ const TokenLogoArea = styled.div `
77
+ position: relative;
78
+ width: 40px;
79
+ height: 40px;
80
+ flex-shrink: 0;
81
+ `;
82
+ const TokenLogoFallback = styled.div `
83
+ width: 40px;
84
+ height: 40px;
85
+ border-radius: 50%;
86
+ background: ${(p) => p.$bg};
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ font-size: 16px;
91
+ font-weight: 700;
92
+ color: #fff;
93
+ user-select: none;
94
+ `;
95
+ const TokenLogoImg = styled.img `
96
+ width: 40px;
97
+ height: 40px;
98
+ border-radius: 50%;
99
+ object-fit: cover;
100
+ `;
101
+ const ChainBadge = styled.div `
102
+ position: absolute;
103
+ bottom: -2px;
104
+ right: -2px;
105
+ width: 18px;
106
+ height: 18px;
107
+ border-radius: 50%;
108
+ background: var(--ck-body-background);
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+ box-shadow: 0 0 0 0.7px var(--ck-body-background);
113
+ `;
70
114
  const EmptyState = styled.div `
71
115
  margin-top: 28px;
72
116
  font-size: 13px;
73
117
  color: var(--ck-body-color-muted);
74
118
  text-align: center;
75
119
  `;
120
+ const InfoLink = styled.button `
121
+ all: unset;
122
+ display: flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ gap: 5px;
126
+ width: 100%;
127
+ font-size: 13px;
128
+ font-weight: 400;
129
+ color: var(--ck-body-color-muted, rgba(255, 255, 255, 0.4));
130
+ padding: 0 0 8px;
131
+ cursor: pointer;
132
+ transition: color 0.15s ease;
133
+ svg { opacity: 0.6; }
134
+ &:hover {
135
+ color: var(--ck-body-color, #fff);
136
+ svg { opacity: 1; }
137
+ }
138
+ `;
139
+ const ContentWrapper = styled.div `
140
+ position: relative;
141
+ display: flex;
142
+ flex-direction: column;
143
+ flex: 1;
144
+ min-height: 0;
145
+ `;
146
+ const ChainGroup = styled.div `
147
+ &:not(:first-child) {
148
+ margin-top: 16px;
149
+ }
150
+ `;
151
+ const ChainGroupHeader = styled.div `
152
+ display: flex;
153
+ align-items: center;
154
+ gap: 8px;
155
+ font-size: 14px;
156
+ font-weight: 600;
157
+ color: var(--ck-body-color, #fff);
158
+ margin-bottom: 8px;
159
+ `;
160
+ const TokenPill = styled.div `
161
+ display: flex;
162
+ align-items: center;
163
+ gap: 6px;
164
+ padding: 6px 10px;
165
+ border-radius: 10px;
166
+ background: var(--ck-body-background-secondary, rgba(255, 255, 255, 0.06));
167
+ font-size: 13px;
168
+ font-weight: 500;
169
+ color: var(--ck-body-color-muted, rgba(255, 255, 255, 0.6));
170
+ &:not(:last-child) {
171
+ margin-bottom: 4px;
172
+ }
173
+ `;
174
+ const TokenPillSymbol = styled.span `
175
+ color: var(--ck-body-color, #fff);
176
+ font-weight: 600;
177
+ `;
76
178
 
77
- export { EmptyState, SelectTokenContent, TokenBalance, TokenButton, TokenContainer, TokenInfo, TokenList, TokenName, TokenSymbol };
179
+ export { ChainBadge, ChainGroup, ChainGroupHeader, ContentWrapper, EmptyState, InfoLink, SelectTokenContent, TokenBalance, TokenButton, TokenContainer, TokenInfo, TokenLeftGroup, TokenList, TokenLogoArea, TokenLogoFallback, TokenLogoImg, TokenName, TokenPill, TokenPillSymbol, TokenSymbol };
78
180
  //# sourceMappingURL=styles.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"styles.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"styles.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -116,6 +116,7 @@ const SendConfirmation = () => {
116
116
  const [isPollingBalance, setIsPollingBalance] = useState(false);
117
117
  const originalBalanceRef = useRef(undefined);
118
118
  const pollingIntervalRef = useRef(null);
119
+ const submittingRef = useRef(false);
119
120
  // Inline transaction state management (replaces useEthereumSendTransaction + useEthereumWriteContract)
120
121
  const [nativeTxHash, setNativeTxHash] = useState(undefined);
121
122
  const [isNativePending, setIsNativePending] = useState(false);
@@ -255,8 +256,11 @@ const SendConfirmation = () => {
255
256
  }
256
257
  }, [isPollingBalance, currentBalance]);
257
258
  const handleConfirm = async () => {
259
+ if (submittingRef.current)
260
+ return;
258
261
  if (!recipientAddress || !parsedAmount || parsedAmount <= BigInt(0) || insufficientBalance)
259
262
  return;
263
+ submittingRef.current = true;
260
264
  try {
261
265
  if (token.type === 'native') {
262
266
  await sendTransactionAsync({
@@ -278,6 +282,9 @@ const SendConfirmation = () => {
278
282
  catch (_error) {
279
283
  // Errors are surfaced through mutation hooks
280
284
  }
285
+ finally {
286
+ submittingRef.current = false;
287
+ }
281
288
  };
282
289
  const handleCancel = () => {
283
290
  // Keep the current token, amount, and recipient when going back - don't reset
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,2 @@
1
+ export declare const TOKEN_LOGO: Record<string, string>;
2
+ export declare function symbolToColor(symbol: string): string;
@@ -0,0 +1,58 @@
1
+ const TW = 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains';
2
+ // Unified symbol -> logo URL map (works for both native and ERC20)
3
+ const TOKEN_LOGO = {
4
+ // Native tokens (chain logos from Trust Wallet info/)
5
+ ETH: `${TW}/ethereum/info/logo.png`,
6
+ BNB: `${TW}/smartchain/info/logo.png`,
7
+ TBNB: `${TW}/smartchain/info/logo.png`,
8
+ MATIC: `${TW}/polygon/info/logo.png`,
9
+ POL: `${TW}/polygon/info/logo.png`,
10
+ AVAX: `${TW}/avalanchec/info/logo.png`,
11
+ FTM: `${TW}/fantom/info/logo.png`,
12
+ CELO: `${TW}/celo/info/logo.png`,
13
+ FIL: `${TW}/filecoin/info/logo.png`,
14
+ METIS: `${TW}/metis/info/logo.png`,
15
+ IOTX: `${TW}/iotex/info/logo.png`,
16
+ EVMOS: `${TW}/evmos/info/logo.png`,
17
+ XDAI: `${TW}/xdai/info/logo.png`,
18
+ FLR: `${TW}/flare/info/logo.png`,
19
+ TLOS: `${TW}/telos/info/logo.png`,
20
+ // ERC20 tokens (using mainnet Ethereum addresses)
21
+ USDC: `${TW}/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png`,
22
+ USDT: `${TW}/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png`,
23
+ DAI: `${TW}/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png`,
24
+ WETH: `${TW}/ethereum/assets/0xC02aaA39b223FE8D0A0e5c4F27eAD9083C756Cc2/logo.png`,
25
+ WBTC: `${TW}/ethereum/assets/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599/logo.png`,
26
+ LINK: `${TW}/ethereum/assets/0x514910771AF9Ca656af840dff83E8264EcF986CA/logo.png`,
27
+ UNI: `${TW}/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png`,
28
+ AAVE: `${TW}/ethereum/assets/0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9/logo.png`,
29
+ MKR: `${TW}/ethereum/assets/0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2/logo.png`,
30
+ CRV: `${TW}/ethereum/assets/0xD533a949740bb3306d119CC777fa900bA034cd52/logo.png`,
31
+ LDO: `${TW}/ethereum/assets/0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32/logo.png`,
32
+ SHIB: `${TW}/ethereum/assets/0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE/logo.png`,
33
+ ARB: `${TW}/arbitrum/assets/0x912CE59144191C1204E64559FE8253a0e49E6548/logo.png`,
34
+ OP: `${TW}/optimism/assets/0x4200000000000000000000000000000000000042/logo.png`,
35
+ STETH: `${TW}/ethereum/assets/0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84/logo.png`,
36
+ CBETH: `${TW}/ethereum/assets/0xBe9895146f7AF43049ca1c1AE358B0541Ea49704/logo.png`,
37
+ RETH: `${TW}/ethereum/assets/0xae78736Cd615f374D3085123A210448E74Fc6393/logo.png`,
38
+ GRT: `${TW}/ethereum/assets/0xc944E90C64B2c07662A292be6244BDf05Cda44a7/logo.png`,
39
+ SNX: `${TW}/ethereum/assets/0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F/logo.png`,
40
+ COMP: `${TW}/ethereum/assets/0xc00e94Cb662C3520282E6f5717214004A7f26888/logo.png`,
41
+ PEPE: `${TW}/ethereum/assets/0x6982508145454Ce325dDbE47a25d4ec3d2311933/logo.png`,
42
+ SUSHI: `${TW}/ethereum/assets/0x6B3595068778DD592e39A122f4f5a5cF09C90fE2/logo.png`,
43
+ DYDX: `${TW}/ethereum/assets/0x92D6C1e31e14520e676a687F0a93788B716BEff5/logo.png`,
44
+ BEAM: `${TW}/ethereum/assets/0x62D0A8458eD7719FDAF978fe5929C6D342B0bFcE/logo.png`,
45
+ EUL: `${TW}/ethereum/assets/0xd9Fcd98c322942075A5C3860693e9f4f03AAE07b/logo.png`,
46
+ };
47
+ // Deterministic color from symbol string (only used if no logo is found)
48
+ function symbolToColor(symbol) {
49
+ let hash = 0;
50
+ for (let i = 0; i < symbol.length; i++) {
51
+ hash = symbol.charCodeAt(i) + ((hash << 5) - hash);
52
+ }
53
+ const h = ((hash % 360) + 360) % 360;
54
+ return `hsl(${h}, 55%, 50%)`;
55
+ }
56
+
57
+ export { TOKEN_LOGO, symbolToColor };
58
+ //# sourceMappingURL=logos.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logos.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -1,29 +1,43 @@
1
- import type { Asset } from '../../components/Openfort/types';
1
+ import type { Asset, MultiChainAsset } from '../../components/Openfort/types';
2
2
  import { OpenfortError } from '../../core/errors';
3
3
  import type { EthereumConfig } from '../../ethereum/types';
4
4
  type UseEthereumWalletAssetsOptions = {
5
5
  assets?: EthereumConfig['assets'];
6
+ /** When true, fetches assets for all configured chains and returns MultiChainAsset[]. */
7
+ multiChain?: boolean;
6
8
  staleTime?: number;
7
9
  };
10
+ type WalletAssetsReturnBase = {
11
+ isLoading: boolean;
12
+ isError: boolean;
13
+ isSuccess: boolean;
14
+ isIdle: boolean;
15
+ error: OpenfortError | undefined;
16
+ refetch: () => Promise<unknown>;
17
+ };
18
+ type UseEthereumWalletAssetsResult = (WalletAssetsReturnBase & {
19
+ multiChain: true;
20
+ data: readonly MultiChainAsset[] | null;
21
+ }) | (WalletAssetsReturnBase & {
22
+ multiChain: false;
23
+ data: readonly Asset[] | null;
24
+ });
8
25
  /**
9
26
  * Returns wallet assets (tokens, NFTs) for the connected Ethereum address.
10
27
  * Uses ERC-7811 via Openfort's authenticated RPC proxy.
11
28
  *
12
- * @param options - Optional custom assets config and staleTime
29
+ * When `multiChain` is true, fetches assets across all configured chains
30
+ * via `wallet_getAssets` and returns `MultiChainAsset[]` (assets tagged with `chainId`).
31
+ *
32
+ * @param options - Optional custom assets config, multiChain flag, and staleTime
13
33
  * @returns assets, isLoading, error, refetch
14
34
  *
15
35
  * @example
16
36
  * ```tsx
17
37
  * const { data: assets, isLoading } = useEthereumWalletAssets()
38
+ * // Multi-chain:
39
+ * const { data, multiChain } = useEthereumWalletAssets({ multiChain: true })
18
40
  * ```
19
41
  */
20
- export declare const useEthereumWalletAssets: ({ assets: hookCustomAssets, staleTime, }?: UseEthereumWalletAssetsOptions) => {
21
- data: readonly Asset[] | null;
22
- isLoading: boolean;
23
- isError: boolean;
24
- isSuccess: boolean;
25
- isIdle: boolean;
26
- error: OpenfortError | undefined;
27
- refetch: () => Promise<readonly Asset[] | undefined>;
28
- };
42
+ export declare const useEthereumWalletAssets: ({ assets: hookCustomAssets, multiChain, staleTime, }?: UseEthereumWalletAssetsOptions) => UseEthereumWalletAssetsResult;
29
43
  export {};
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useMemo } from 'react';
2
- import { custom, createWalletClient, numberToHex } from 'viem';
2
+ import { numberToHex, custom, createWalletClient, formatUnits } from 'viem';
3
3
  import { erc7811Actions } from 'viem/experimental';
4
4
  import { useOpenfort } from '../../components/Openfort/useOpenfort.js';
5
5
  import { OpenfortError, OpenfortReactErrorType } from '../../types.js';
@@ -8,20 +8,34 @@ import { openfortKeys } from '../../query/queryKeys.js';
8
8
  import { useAsyncData } from '../../shared/hooks/useAsyncData.js';
9
9
  import { useEthereumEmbeddedWallet } from './useEthereumEmbeddedWallet.js';
10
10
 
11
+ function getUsdValue(asset) {
12
+ var _a, _b, _c;
13
+ const fiat = (_a = asset.metadata) === null || _a === void 0 ? void 0 : _a.fiat;
14
+ if (!(fiat === null || fiat === void 0 ? void 0 : fiat.value) || asset.balance === undefined)
15
+ return 0;
16
+ const decimals = (_c = (_b = asset.metadata) === null || _b === void 0 ? void 0 : _b.decimals) !== null && _c !== void 0 ? _c : 18;
17
+ const amount = Number.parseFloat(formatUnits(asset.balance, decimals));
18
+ return Number.isFinite(amount) ? amount * fiat.value : 0;
19
+ }
11
20
  /**
12
21
  * Returns wallet assets (tokens, NFTs) for the connected Ethereum address.
13
22
  * Uses ERC-7811 via Openfort's authenticated RPC proxy.
14
23
  *
15
- * @param options - Optional custom assets config and staleTime
24
+ * When `multiChain` is true, fetches assets across all configured chains
25
+ * via `wallet_getAssets` and returns `MultiChainAsset[]` (assets tagged with `chainId`).
26
+ *
27
+ * @param options - Optional custom assets config, multiChain flag, and staleTime
16
28
  * @returns assets, isLoading, error, refetch
17
29
  *
18
30
  * @example
19
31
  * ```tsx
20
32
  * const { data: assets, isLoading } = useEthereumWalletAssets()
33
+ * // Multi-chain:
34
+ * const { data, multiChain } = useEthereumWalletAssets({ multiChain: true })
21
35
  * ```
22
36
  */
23
- const useEthereumWalletAssets = ({ assets: hookCustomAssets, staleTime = 30000, } = {}) => {
24
- var _a;
37
+ const useEthereumWalletAssets = ({ assets: hookCustomAssets, multiChain = false, staleTime = 30000, } = {}) => {
38
+ var _a, _b;
25
39
  const wallet = useEthereumEmbeddedWallet();
26
40
  const isConnected = wallet.status === 'connected';
27
41
  const address = isConnected ? wallet.address : undefined;
@@ -29,6 +43,7 @@ const useEthereumWalletAssets = ({ assets: hookCustomAssets, staleTime = 30000,
29
43
  const { walletConfig, publishableKey, overrides, thirdPartyAuth, chains } = useOpenfort();
30
44
  const { getAccessToken } = useUser();
31
45
  const chain = chains.find((c) => c.id === chainId);
46
+ const backendUrl = (overrides === null || overrides === void 0 ? void 0 : overrides.backendUrl) || 'https://api.openfort.io';
32
47
  const buildHeaders = useCallback(async () => {
33
48
  if (thirdPartyAuth) {
34
49
  const accessToken = await thirdPartyAuth.getAccessToken();
@@ -51,10 +66,25 @@ const useEthereumWalletAssets = ({ assets: hookCustomAssets, staleTime = 30000,
51
66
  };
52
67
  return headers;
53
68
  }, [publishableKey, getAccessToken, thirdPartyAuth]);
69
+ /** For multiChain: walletConfig.ethereum.assets as backend assetFilter format (hex chainId -> [{ address, type }]). */
70
+ const customAssetsMultiChain = useMemo(() => {
71
+ var _a;
72
+ if (!multiChain)
73
+ return undefined;
74
+ const configAssets = (_a = walletConfig === null || walletConfig === void 0 ? void 0 : walletConfig.ethereum) === null || _a === void 0 ? void 0 : _a.assets;
75
+ if (!configAssets)
76
+ return undefined;
77
+ const mapped = {};
78
+ for (const [cid, addresses] of Object.entries(configAssets)) {
79
+ const hexChainId = numberToHex(Number(cid));
80
+ mapped[hexChainId] = addresses.map((addr) => ({ address: addr, type: 'erc20' }));
81
+ }
82
+ return Object.keys(mapped).length > 0 ? mapped : undefined;
83
+ }, [multiChain, (_a = walletConfig === null || walletConfig === void 0 ? void 0 : walletConfig.ethereum) === null || _a === void 0 ? void 0 : _a.assets]);
54
84
  const customTransport = useMemo(() => () => {
55
85
  return custom({
56
86
  async request({ method, params }) {
57
- const res = await fetch(`${(overrides === null || overrides === void 0 ? void 0 : overrides.backendUrl) || 'https://api.openfort.io'}/rpc`, {
87
+ const res = await fetch(`${backendUrl}/rpc`, {
58
88
  method: 'POST',
59
89
  headers: await buildHeaders(),
60
90
  body: JSON.stringify({
@@ -71,7 +101,7 @@ const useEthereumWalletAssets = ({ assets: hookCustomAssets, staleTime = 30000,
71
101
  return data.result;
72
102
  },
73
103
  });
74
- }, [buildHeaders, overrides === null || overrides === void 0 ? void 0 : overrides.backendUrl]);
104
+ }, [buildHeaders, backendUrl]);
75
105
  const customAssetsToFetch = useMemo(() => {
76
106
  var _a;
77
107
  if (!chainId)
@@ -80,11 +110,101 @@ const useEthereumWalletAssets = ({ assets: hookCustomAssets, staleTime = 30000,
80
110
  const assetsFromHook = hookCustomAssets ? hookCustomAssets[chainId] || [] : [];
81
111
  const allAssets = [...assetsFromConfig, ...assetsFromHook];
82
112
  return allAssets;
83
- }, [(_a = walletConfig === null || walletConfig === void 0 ? void 0 : walletConfig.ethereum) === null || _a === void 0 ? void 0 : _a.assets, hookCustomAssets, chainId]);
113
+ }, [(_b = walletConfig === null || walletConfig === void 0 ? void 0 : walletConfig.ethereum) === null || _b === void 0 ? void 0 : _b.assets, hookCustomAssets, chainId]);
84
114
  const { data, error, isLoading, refetch } = useAsyncData({
85
- queryKey: [...openfortKeys.walletAssets(chainId, customAssetsToFetch, address)],
115
+ queryKey: multiChain
116
+ ? ['wallet-assets', 'multi', address, customAssetsMultiChain]
117
+ : [...openfortKeys.walletAssets(chainId, customAssetsToFetch, address)],
86
118
  queryFn: async () => {
87
- var _a, _b, _c, _d;
119
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
120
+ if (multiChain) {
121
+ if (!address) {
122
+ throw new OpenfortError('No wallet address available', OpenfortReactErrorType.UNEXPECTED_ERROR);
123
+ }
124
+ const headers = await buildHeaders();
125
+ const defaultRequest = fetch(`${backendUrl}/rpc`, {
126
+ method: 'POST',
127
+ headers,
128
+ body: JSON.stringify({
129
+ method: 'wallet_getAssets',
130
+ params: { account: address },
131
+ id: 1,
132
+ jsonrpc: '2.0',
133
+ }),
134
+ });
135
+ const customRequest = customAssetsMultiChain
136
+ ? fetch(`${backendUrl}/rpc`, {
137
+ method: 'POST',
138
+ headers,
139
+ body: JSON.stringify({
140
+ method: 'wallet_getAssets',
141
+ params: { account: address, assetFilter: customAssetsMultiChain },
142
+ id: 2,
143
+ jsonrpc: '2.0',
144
+ }),
145
+ })
146
+ : null;
147
+ const responses = await Promise.all([defaultRequest, customRequest].filter(Boolean));
148
+ const [defaultData, customData] = await Promise.all(responses.map((r) => r.json()));
149
+ const result = { ...((_a = defaultData.result) !== null && _a !== void 0 ? _a : {}) };
150
+ if ((customData === null || customData === void 0 ? void 0 : customData.result) && typeof customData.result === 'object') {
151
+ for (const [chainKey, assets] of Object.entries(customData.result)) {
152
+ if (!Array.isArray(assets))
153
+ continue;
154
+ if (!result[chainKey]) {
155
+ result[chainKey] = assets;
156
+ }
157
+ else {
158
+ const existing = new Map(result[chainKey].map((a) => { var _a; return [(_a = a.address) !== null && _a !== void 0 ? _a : '', a]; }));
159
+ for (const asset of assets) {
160
+ existing.set((_b = asset.address) !== null && _b !== void 0 ? _b : '', asset);
161
+ }
162
+ result[chainKey] = Array.from(existing.values());
163
+ }
164
+ }
165
+ }
166
+ const allAssets = [];
167
+ for (const [chainIdKey, assets] of Object.entries(result)) {
168
+ const cid = Number(chainIdKey);
169
+ if (!Array.isArray(assets))
170
+ continue;
171
+ for (const a of assets) {
172
+ if (a.type === 'erc20') {
173
+ const asset = {
174
+ type: 'erc20',
175
+ address: ((_c = a.address) !== null && _c !== void 0 ? _c : '0x0'),
176
+ balance: BigInt((_d = a.balance) !== null && _d !== void 0 ? _d : 0),
177
+ metadata: {
178
+ name: ((_e = a.metadata) === null || _e === void 0 ? void 0 : _e.name) || 'Unknown Token',
179
+ symbol: ((_f = a.metadata) === null || _f === void 0 ? void 0 : _f.symbol) || 'UNKNOWN',
180
+ decimals: (_g = a.metadata) === null || _g === void 0 ? void 0 : _g.decimals,
181
+ fiat: (_h = a.metadata) === null || _h === void 0 ? void 0 : _h.fiat,
182
+ },
183
+ raw: a,
184
+ };
185
+ allAssets.push({ ...asset, chainId: cid });
186
+ }
187
+ else if (a.type === 'native') {
188
+ const meta = ((_j = a.metadata) !== null && _j !== void 0 ? _j : {});
189
+ const asset = {
190
+ type: 'native',
191
+ address: 'native',
192
+ balance: BigInt((_k = a.balance) !== null && _k !== void 0 ? _k : 0),
193
+ metadata: {
194
+ symbol: meta.symbol || 'ETH',
195
+ decimals: meta.decimals,
196
+ fiat: (_l = meta.fiat) !== null && _l !== void 0 ? _l : { value: 0, currency: 'USD' },
197
+ },
198
+ raw: a,
199
+ };
200
+ allAssets.push({ ...asset, chainId: cid });
201
+ }
202
+ }
203
+ }
204
+ allAssets.sort((a, b) => getUsdValue(b) - getUsdValue(a));
205
+ return allAssets;
206
+ }
207
+ // Single-chain path
88
208
  if (!address || !chainId || !chain) {
89
209
  throw new OpenfortError('Wallet not connected', OpenfortReactErrorType.UNEXPECTED_ERROR, {
90
210
  error: new Error('Address, chainId, or chain not available'),
@@ -115,8 +235,8 @@ const useEthereumWalletAssets = ({ assets: hookCustomAssets, staleTime = 30000,
115
235
  // ERC-7811 response keys may be hex (e.g. "0x14a34") or numeric depending on the RPC
116
236
  const rawByChain = defaultAssetsRaw;
117
237
  const customByChain = customAssets;
118
- const rawChainAssets = (_b = (_a = rawByChain[hexChainId]) !== null && _a !== void 0 ? _a : rawByChain[String(chainId)]) !== null && _b !== void 0 ? _b : [];
119
- const customChainAssets = (_d = (_c = customByChain[hexChainId]) !== null && _c !== void 0 ? _c : customByChain[String(chainId)]) !== null && _d !== void 0 ? _d : [];
238
+ const rawChainAssets = (_o = (_m = rawByChain[hexChainId]) !== null && _m !== void 0 ? _m : rawByChain[String(chainId)]) !== null && _o !== void 0 ? _o : [];
239
+ const customChainAssets = (_q = (_p = customByChain[hexChainId]) !== null && _p !== void 0 ? _p : customByChain[String(chainId)]) !== null && _q !== void 0 ? _q : [];
120
240
  const defaultAssets = rawChainAssets.map((a) => {
121
241
  var _a;
122
242
  let asset;
@@ -153,7 +273,6 @@ const useEthereumWalletAssets = ({ assets: hookCustomAssets, staleTime = 30000,
153
273
  const mergedAssets = [...defaultAssets];
154
274
  const customAssetsForChain = customChainAssets.flatMap((asset) => {
155
275
  var _a, _b;
156
- // Custom assets are explicitly requested as erc20; skip if the API returns something unexpected.
157
276
  if (asset.type !== 'erc20')
158
277
  return [];
159
278
  if (!((_a = walletConfig === null || walletConfig === void 0 ? void 0 : walletConfig.ethereum) === null || _a === void 0 ? void 0 : _a.assets))
@@ -178,7 +297,7 @@ const useEthereumWalletAssets = ({ assets: hookCustomAssets, staleTime = 30000,
178
297
  });
179
298
  return mergedAssets;
180
299
  },
181
- enabled: isConnected && !!chainId && !!chain && !!address,
300
+ enabled: multiChain ? isConnected && !!address : isConnected && !!chainId && !!chain && !!address,
182
301
  staleTime,
183
302
  });
184
303
  const mappedError = useMemo(() => {
@@ -191,10 +310,11 @@ const useEthereumWalletAssets = ({ assets: hookCustomAssets, staleTime = 30000,
191
310
  }, [error]);
192
311
  return {
193
312
  data: data !== null && data !== void 0 ? data : null,
313
+ multiChain,
194
314
  isLoading,
195
315
  isError: !!error,
196
316
  isSuccess: !!data && !error,
197
- isIdle: !isConnected || !chainId || !chain,
317
+ isIdle: multiChain ? !address : !isConnected || !chainId || !chain,
198
318
  error: mappedError,
199
319
  refetch,
200
320
  };
@@ -1 +1 @@
1
- {"version":3,"file":"useEthereumWalletAssets.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"useEthereumWalletAssets.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/build/index.d.ts CHANGED
@@ -44,7 +44,7 @@ export { default as Avatar } from './components/Common/Avatar';
44
44
  export { default as ChainIcon } from './components/Common/Chain';
45
45
  export { OpenfortButton } from './components/ConnectButton';
46
46
  export { OpenfortProvider } from './components/Openfort/OpenfortProvider';
47
- export type { CustomizableRoutes } from './components/Openfort/types';
47
+ export type { CustomizableRoutes, MultiChainAsset } from './components/Openfort/types';
48
48
  export { LinkWalletOnSignUpOption, UIAuthProvider as AuthProvider } from './components/Openfort/types';
49
49
  export { embeddedWalletId } from './constants/openfort';
50
50
  export { OpenfortError, OpenfortReactErrorType, OpenfortReactErrorType as OpenfortErrorType, } from './core/errors';
@@ -35,8 +35,11 @@ function useEmbeddedStateMachine({ openfort, storeEmbeddedState, storeUser, stor
35
35
  setIsConnectedWithEmbeddedSigner(false);
36
36
  // Validate token and fetch accounts. Auto-recovery is handled by the
37
37
  // dedicated useAutoRecovery hook (keyed on storeActiveEmbeddedAddress).
38
+ // Never auto-logout here: during first OAuth sign-in the user isn't in
39
+ // the store yet and user.get() may briefly 401/404 before the token
40
+ // propagates — logging out would abort the entire auth flow.
38
41
  const doFetch = async () => {
39
- updateUserRef.current(undefined, !userRef.current);
42
+ updateUserRef.current(undefined, false);
40
43
  await fetchEmbeddedAccountsRef.current();
41
44
  };
42
45
  doFetch().catch((err) => {
@@ -1 +1 @@
1
- {"version":3,"file":"useEmbeddedStateMachine.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"useEmbeddedStateMachine.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -9,4 +9,4 @@ declare function flattenChildren(children: React.ReactNode): ReactChildArray;
9
9
  export declare const isWalletConnectConnector: (connectorId?: string) => connectorId is "walletConnect";
10
10
  export declare const isCoinbaseWalletConnector: (connectorId?: string) => connectorId is "coinbaseWalletSDK";
11
11
  export declare const isInjectedConnector: (connectorId?: string) => connectorId is "injected";
12
- export { nFormatter, truncateEthAddress, truncateSolanaAddress, isMobile, isAndroid, detectBrowser, flattenChildren };
12
+ export { detectBrowser, flattenChildren, isAndroid, isMobile, nFormatter, truncateEthAddress, truncateSolanaAddress };
@@ -1 +1 @@
1
- export declare const OPENFORT_VERSION = "1.0.3";
1
+ export declare const OPENFORT_VERSION = "1.0.5";
package/build/version.js CHANGED
@@ -1,4 +1,4 @@
1
- const OPENFORT_VERSION = '1.0.3';
1
+ const OPENFORT_VERSION = '1.0.5';
2
2
 
3
3
  export { OPENFORT_VERSION };
4
4
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfort/react",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "author": "Openfort (https://www.openfort.io)",
5
5
  "license": "BSD-2-Clause license",
6
6
  "description": "The easiest way to integrate Openfort to your project.",