@swype-org/react-sdk 0.2.375 → 0.2.399

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/index.js CHANGED
@@ -11,6 +11,7 @@ import { getAccount, reconnect, disconnect, getWalletClient, getConnectors } fro
11
11
  import { encodeFunctionData, recoverTypedDataAddress, decodeAbiParameters } from 'viem';
12
12
  import { parseErc6492Signature } from 'viem/utils';
13
13
  import * as QRCode from 'qrcode';
14
+ import posthog from 'posthog-js';
14
15
  import * as Select from '@radix-ui/react-select';
15
16
 
16
17
  var __defProp = Object.defineProperty;
@@ -54,7 +55,11 @@ var darkTheme = {
54
55
  shadowLg: "0 18px 44px rgba(0,0,0,0.42)",
55
56
  radius: "14px",
56
57
  radiusLg: "24px",
57
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
58
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
59
+ fontWeightRegular: 400,
60
+ fontWeightMedium: 500,
61
+ fontWeightSemibold: 600,
62
+ fontWeightBold: 700
58
63
  };
59
64
  var lightTheme = {
60
65
  bg: "#ebf9fb",
@@ -86,7 +91,11 @@ var lightTheme = {
86
91
  shadowLg: "0 20px 48px rgba(19, 61, 75, 0.14)",
87
92
  radius: "14px",
88
93
  radiusLg: "24px",
89
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
94
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
95
+ fontWeightRegular: 400,
96
+ fontWeightMedium: 500,
97
+ fontWeightSemibold: 600,
98
+ fontWeightBold: 700
90
99
  };
91
100
  var lightTransparentTheme = {
92
101
  ...lightTheme
@@ -129,7 +138,13 @@ var lightThemeNew = {
129
138
  shadowLg: "0 20px 48px rgba(0, 0, 0, 0.14)",
130
139
  radius: "14px",
131
140
  radiusLg: "24px",
132
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
141
+ // Figma redesign uses Inter; fall back to the system stack when the host
142
+ // page hasn't loaded the Inter webfont.
143
+ fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
144
+ fontWeightRegular: 400,
145
+ fontWeightMedium: 500,
146
+ fontWeightSemibold: 600,
147
+ fontWeightBold: 700
133
148
  };
134
149
  var darkThemeNew = { ...lightThemeNew };
135
150
  var lightTransparentThemeNew = { ...lightThemeNew };
@@ -299,7 +314,7 @@ function resolveDepositPriorityContext(accounts, chains, destination) {
299
314
  for (const account of accounts) {
300
315
  for (const wallet of account.wallets) {
301
316
  if (wallet.chain.name !== destinationChainName) continue;
302
- const source = wallet.sources.find(
317
+ const source = (wallet.sources ?? []).find(
303
318
  (candidate) => candidate.address.toLowerCase() === destinationTokenAddress
304
319
  );
305
320
  if (source) {
@@ -333,6 +348,11 @@ function getWalletAddress(wallet) {
333
348
  const address = wallet.name.trim();
334
349
  return address.length > 0 ? address : null;
335
350
  }
351
+ function truncateMiddle(address, head = 8, tail = 6) {
352
+ const trimmed = address.trim();
353
+ if (trimmed.length <= head + tail + 4) return trimmed;
354
+ return `${trimmed.slice(0, head)}...${trimmed.slice(-tail)}`;
355
+ }
336
356
  function getAddressableWallets(account) {
337
357
  return account.wallets.filter((wallet) => getWalletAddress(wallet) != null);
338
358
  }
@@ -340,7 +360,7 @@ function getPreferredDepositWallet(account, transferAmount, priorityContext) {
340
360
  const wallets = getAddressableWallets(account);
341
361
  if (wallets.length === 0) return null;
342
362
  const ranked = wallets.map((wallet, walletIndex) => {
343
- const bestSource = wallet.sources.map((source, sourceIndex) => ({
363
+ const bestSource = (wallet.sources ?? []).map((source, sourceIndex) => ({
344
364
  source,
345
365
  sourceIndex
346
366
  })).sort((a, b) => {
@@ -365,7 +385,7 @@ function getPreferredDepositWallet(account, transferAmount, priorityContext) {
365
385
  if (a.wallet.status !== b.wallet.status) {
366
386
  return a.wallet.status === "ACTIVE" ? -1 : 1;
367
387
  }
368
- const balanceDiff = b.wallet.balance.available.amount - a.wallet.balance.available.amount;
388
+ const balanceDiff = (b.wallet.balance?.available.amount ?? 0) - (a.wallet.balance?.available.amount ?? 0);
369
389
  if (balanceDiff !== 0) return balanceDiff;
370
390
  return a.walletIndex - b.walletIndex;
371
391
  });
@@ -381,7 +401,7 @@ function resolveDepositSelectionAfterRefresh(accounts, transferAmount, prev, pri
381
401
  const wallet = acct?.wallets.find((w) => w.id === selectedWalletId);
382
402
  if (wallet) {
383
403
  if (selectedTokenSymbol) {
384
- const hasToken = wallet.sources.some((s) => s.token.symbol === selectedTokenSymbol);
404
+ const hasToken = wallet.sources == null || wallet.sources.some((s) => s.token.symbol === selectedTokenSymbol);
385
405
  if (hasToken) {
386
406
  return {
387
407
  defaults: { accountId: selectedAccountId, walletId: selectedWalletId },
@@ -419,7 +439,9 @@ function resolveDepositSelection(accounts, transferAmount, selectedAccountId, pr
419
439
  const eligibleAccounts = getDepositEligibleAccounts(accounts);
420
440
  if (eligibleAccounts.length === 0) return null;
421
441
  const accountHasSufficientBalanceWallet = (account) => getAddressableWallets(account).some(
422
- (wallet) => wallet.status === "ACTIVE" && wallet.sources.some((source) => source.balance.available.amount >= transferAmount)
442
+ (wallet) => wallet.status === "ACTIVE" && (wallet.sources ?? []).some(
443
+ (source) => source.balance.available.amount >= transferAmount
444
+ )
423
445
  );
424
446
  if (transferAmount <= 0 && selectedAccountId) {
425
447
  const selectedAccount = eligibleAccounts.find((account) => account.id === selectedAccountId);
@@ -441,7 +463,7 @@ function resolveDepositSelection(accounts, transferAmount, selectedAccountId, pr
441
463
  transferAmount,
442
464
  priorityContext
443
465
  );
444
- const preferredSource = preferredWallet ? [...preferredWallet.sources].sort((a, b) => compareDepositSourcePriority(
466
+ const preferredSource = preferredWallet ? [...preferredWallet.sources ?? []].sort((a, b) => compareDepositSourcePriority(
445
467
  sourcePriorityInput(preferredWallet.chain.name, a, transferAmount, priorityContext),
446
468
  sourcePriorityInput(preferredWallet.chain.name, b, transferAmount, priorityContext)
447
469
  ))[0] : void 0;
@@ -507,7 +529,7 @@ function buildNativeUnsupportedEntries(options) {
507
529
  logoURI: option.logoURI ?? null
508
530
  })).filter((entry) => entry.amount > 0);
509
531
  }
510
- function buildSelectSourceChoices(options, minTransferAmountUsd = DEFAULT_MIN_SEND_AMOUNT_USD) {
532
+ function buildSelectSourceChoices(options, minTransferAmountUsd = DEFAULT_MIN_SEND_AMOUNT_USD, includeUnfundedTokens = false) {
511
533
  const chainChoices = [];
512
534
  const chainIndexByName = /* @__PURE__ */ new Map();
513
535
  for (const option of options) {
@@ -548,7 +570,7 @@ function buildSelectSourceChoices(options, minTransferAmountUsd = DEFAULT_MIN_SE
548
570
  }
549
571
  }
550
572
  return chainChoices.map((chain) => {
551
- const visibleTokens = chain.tokens.filter((t) => isSelectableDepositSourceAmountUsd(t.balance, minTransferAmountUsd));
573
+ const visibleTokens = includeUnfundedTokens ? chain.tokens : chain.tokens.filter((t) => isSelectableDepositSourceAmountUsd(t.balance, minTransferAmountUsd));
552
574
  return {
553
575
  ...chain,
554
576
  balance: visibleTokens.reduce((sum, token) => sum + token.balance, 0),
@@ -1553,11 +1575,11 @@ function detachAccountChangeListeners() {
1553
1575
  accountChangeBinding = null;
1554
1576
  }
1555
1577
  function providerKey(selection) {
1556
- const providerName = selection.providerName?.trim();
1557
- if (!providerName) {
1578
+ const providerName2 = selection.providerName?.trim();
1579
+ if (!providerName2) {
1558
1580
  throw new Error("Solana wallet metadata is missing providerName.");
1559
1581
  }
1560
- return `${selection.providerId ?? ""}:${providerName.toLowerCase()}`;
1582
+ return `${selection.providerId ?? ""}:${providerName2.toLowerCase()}`;
1561
1583
  }
1562
1584
  function assertSupportedProvider(selection) {
1563
1585
  const name = selection.providerName?.trim().toLowerCase();
@@ -1936,464 +1958,197 @@ function screenForPhase(phase) {
1936
1958
  }
1937
1959
  }
1938
1960
 
1939
- // src/passkey-delegation.ts
1940
- var PasskeyIframeBlockedError = class extends Error {
1941
- constructor(message = "Passkey creation is not supported in this browser context.") {
1942
- super(message);
1943
- this.name = "PasskeyIframeBlockedError";
1961
+ // src/api.ts
1962
+ var api_exports = {};
1963
+ __export(api_exports, {
1964
+ createAccount: () => createAccount,
1965
+ createAccountAuthorizationSession: () => createAccountAuthorizationSession,
1966
+ createManualTransfer: () => createManualTransfer,
1967
+ createTransfer: () => createTransfer,
1968
+ fetchAccount: () => fetchAccount,
1969
+ fetchAccountBalances: () => fetchAccountBalances,
1970
+ fetchAccounts: () => fetchAccounts,
1971
+ fetchAuthorizationSession: () => fetchAuthorizationSession,
1972
+ fetchAuthorizationSessionByToken: () => fetchAuthorizationSessionByToken,
1973
+ fetchChains: () => fetchChains,
1974
+ fetchManualTransferSession: () => fetchManualTransferSession,
1975
+ fetchManualTransferSources: () => fetchManualTransferSources,
1976
+ fetchMerchantPublicKey: () => fetchMerchantPublicKey,
1977
+ fetchProviders: () => fetchProviders,
1978
+ fetchTransfer: () => fetchTransfer,
1979
+ fetchUserConfig: () => fetchUserConfig,
1980
+ postTransferQuote: () => postTransferQuote,
1981
+ probeActionCompletion: () => probeActionCompletion,
1982
+ refreshManualTransferQuote: () => refreshManualTransferQuote,
1983
+ regenerateTransferSignPayload: () => regenerateTransferSignPayload,
1984
+ registerPasskey: () => registerPasskey,
1985
+ reportActionCompletion: () => reportActionCompletion,
1986
+ reportPasskeyActivity: () => reportPasskeyActivity,
1987
+ setAuthorizationSessionPaymentIntentAmount: () => setAuthorizationSessionPaymentIntentAmount,
1988
+ setAuthorizationSessionProvider: () => setAuthorizationSessionProvider,
1989
+ signTransfer: () => signTransfer,
1990
+ updateManualTransferDepositTargetChain: () => updateManualTransferDepositTargetChain,
1991
+ updateUserConfig: () => updateUserConfig,
1992
+ updateUserConfigBySession: () => updateUserConfigBySession,
1993
+ waitForActionTransactionReceipt: () => waitForActionTransactionReceipt
1994
+ });
1995
+ var DEBUG_BUFFER_CAPACITY = 200;
1996
+ var nextId = 1;
1997
+ var entries = [];
1998
+ var listeners = /* @__PURE__ */ new Set();
1999
+ function notify() {
2000
+ for (const listener of listeners) {
2001
+ try {
2002
+ listener();
2003
+ } catch (err) {
2004
+ console.error("[blink-sdk][debug-log] listener threw:", err);
2005
+ }
1944
2006
  }
1945
- };
1946
- function isInCrossOriginIframe() {
1947
- if (typeof window === "undefined") return false;
1948
- if (window.parent === window) return false;
1949
- try {
1950
- void window.parent.location.origin;
1951
- return false;
1952
- } catch {
1953
- return true;
2007
+ }
2008
+ function appendDebug(level, message, data) {
2009
+ const entry = {
2010
+ id: nextId++,
2011
+ ts: Date.now(),
2012
+ level,
2013
+ message,
2014
+ data
2015
+ };
2016
+ const next = entries.length >= DEBUG_BUFFER_CAPACITY ? entries.slice(entries.length - DEBUG_BUFFER_CAPACITY + 1) : entries.slice();
2017
+ next.push(entry);
2018
+ entries = next;
2019
+ const prefix = "[blink-sdk][debug]";
2020
+ const sink = level === "error" ? console.error : level === "warn" ? console.warn : console.info;
2021
+ if (data !== void 0) {
2022
+ sink(`${prefix} ${message}`, data);
2023
+ } else {
2024
+ sink(`${prefix} ${message}`);
1954
2025
  }
2026
+ notify();
1955
2027
  }
1956
- function isSafari() {
1957
- if (typeof navigator === "undefined") return false;
1958
- const ua = navigator.userAgent;
1959
- return /Safari/i.test(ua) && !/Chrome|CriOS|Chromium|Edg|OPR|Firefox/i.test(ua);
2028
+ function subscribeDebug(listener) {
2029
+ listeners.add(listener);
2030
+ return () => {
2031
+ listeners.delete(listener);
2032
+ };
1960
2033
  }
1961
- var VERIFY_POPUP_TIMEOUT_MS = 6e4;
1962
- var POPUP_CLOSED_POLL_MS = 500;
1963
- var POPUP_CLOSED_GRACE_MS = 1e3;
1964
- function findDevicePasskeyViaPopup(options) {
1965
- return new Promise((resolve, reject) => {
1966
- const verificationToken = crypto.randomUUID();
1967
- const payload = {
1968
- ...options,
1969
- verificationToken
1970
- };
1971
- const encoded = btoa(JSON.stringify(payload));
1972
- const popupUrl = `${window.location.origin}/passkey-verify#${encoded}`;
1973
- const popup = window.open(popupUrl, "blink-passkey-verify");
1974
- if (!popup) {
1975
- reject(new Error("Pop-up blocked. Please allow pop-ups for this site and try again."));
1976
- return;
1977
- }
1978
- let settled = false;
1979
- const timer = setTimeout(() => {
1980
- cleanup();
1981
- resolve(null);
1982
- }, VERIFY_POPUP_TIMEOUT_MS);
1983
- const closedPoll = setInterval(() => {
1984
- if (popup.closed && !settled) {
1985
- clearInterval(closedPoll);
1986
- setTimeout(() => {
1987
- if (!settled) {
1988
- settled = true;
1989
- cleanup();
1990
- checkServerForPasskeyByToken(
1991
- options.authToken,
1992
- options.apiBaseUrl,
1993
- verificationToken
1994
- ).then((result) => {
1995
- resolve(result?.credentialId ?? null);
1996
- }).catch(() => {
1997
- resolve(null);
1998
- });
1999
- }
2000
- }, POPUP_CLOSED_GRACE_MS);
2034
+ function getDebugEntries() {
2035
+ return entries;
2036
+ }
2037
+ function clearDebugEntries() {
2038
+ entries = [];
2039
+ notify();
2040
+ }
2041
+ function useBlinkDebugLog() {
2042
+ return useSyncExternalStore(subscribeDebug, getDebugEntries, getDebugEntries);
2043
+ }
2044
+
2045
+ // src/fetchWithRetry.ts
2046
+ var DEFAULT_MAX_RETRIES = 3;
2047
+ var DEFAULT_BASE_DELAY_MS = 500;
2048
+ var DEFAULT_MAX_JITTER_MS = 200;
2049
+ function isNetworkTypeError(err) {
2050
+ return err instanceof TypeError && /fetch|network|load failed/i.test(err.message);
2051
+ }
2052
+ async function fetchWithRetry(input, init, options) {
2053
+ const maxRetries = DEFAULT_MAX_RETRIES;
2054
+ const baseDelayMs = DEFAULT_BASE_DELAY_MS;
2055
+ const maxJitterMs = DEFAULT_MAX_JITTER_MS;
2056
+ const label = String(input).replace(/https?:\/\/[^/]+/, "");
2057
+ let lastError;
2058
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
2059
+ try {
2060
+ return await fetch(input, init);
2061
+ } catch (err) {
2062
+ lastError = err;
2063
+ if (!isNetworkTypeError(err)) {
2064
+ throw err;
2065
+ }
2066
+ if (attempt < maxRetries) {
2067
+ const delay = baseDelayMs * Math.pow(2, attempt) + Math.random() * maxJitterMs;
2068
+ appendDebug("warn", `fetchWithRetry: network error, retrying ${label}`, {
2069
+ attempt: attempt + 1,
2070
+ maxRetries,
2071
+ delayMs: Math.round(delay),
2072
+ error: err instanceof Error ? err.message : String(err)
2073
+ });
2074
+ await new Promise((resolve) => setTimeout(resolve, delay));
2001
2075
  }
2002
- }, POPUP_CLOSED_POLL_MS);
2003
- function cleanup() {
2004
- clearTimeout(timer);
2005
- clearInterval(closedPoll);
2006
2076
  }
2077
+ }
2078
+ throw lastError;
2079
+ }
2080
+
2081
+ // src/apiError.ts
2082
+ var ApiError = class extends Error {
2083
+ status;
2084
+ code;
2085
+ constructor(status, code, message) {
2086
+ super(message);
2087
+ this.name = "ApiError";
2088
+ this.status = status;
2089
+ this.code = code;
2090
+ }
2091
+ };
2092
+ function isApiError(err) {
2093
+ if (err instanceof ApiError) return true;
2094
+ return typeof err === "object" && err !== null && "name" in err && err.name === "ApiError";
2095
+ }
2096
+ var SVM_SIGN_PAYLOAD_EXPIRED_CODE = "SVM_SIGN_PAYLOAD_EXPIRED";
2097
+ function isSvmSignExpiredError(err) {
2098
+ return isApiError(err) && err.code === SVM_SIGN_PAYLOAD_EXPIRED_CODE;
2099
+ }
2100
+
2101
+ // src/api.ts
2102
+ async function throwApiError(res) {
2103
+ const body = await res.json().catch(() => null);
2104
+ const detail = body?.error ?? body;
2105
+ const msg = detail?.message ?? res.statusText;
2106
+ const code = detail?.code ?? String(res.status);
2107
+ throw new ApiError(res.status, code, `${res.status} \u2014 ${code}: ${msg}`);
2108
+ }
2109
+ async function fetchProviders(apiBaseUrl, token) {
2110
+ const headers = {};
2111
+ if (token) {
2112
+ headers.Authorization = `Bearer ${token}`;
2113
+ }
2114
+ const res = await fetchWithRetry(`${apiBaseUrl}/v1/providers`, { headers });
2115
+ if (!res.ok) await throwApiError(res);
2116
+ const data = await res.json();
2117
+ return data.items;
2118
+ }
2119
+ async function fetchChains(apiBaseUrl, token) {
2120
+ const res = await fetchWithRetry(`${apiBaseUrl}/v1/chains`, {
2121
+ headers: { Authorization: `Bearer ${token}` }
2007
2122
  });
2123
+ if (!res.ok) await throwApiError(res);
2124
+ const data = await res.json();
2125
+ return data.items;
2008
2126
  }
2009
- async function checkServerForPasskeyByToken(authToken, apiBaseUrl, verificationToken) {
2010
- if (!authToken || !apiBaseUrl) return null;
2011
- const res = await fetch(`${apiBaseUrl}/v1/users/config`, {
2012
- headers: { Authorization: `Bearer ${authToken}` }
2127
+ async function fetchAccounts(apiBaseUrl, token, credentialId) {
2128
+ const params = new URLSearchParams({ credentialId });
2129
+ const res = await fetchWithRetry(`${apiBaseUrl}/v1/accounts?${params.toString()}`, {
2130
+ headers: { Authorization: `Bearer ${token}` }
2013
2131
  });
2014
- if (!res.ok) return null;
2015
- const body = await res.json();
2016
- const passkeys = body.config.passkeys ?? [];
2017
- const matched = passkeys.find((p) => p.lastVerificationToken === verificationToken);
2018
- return matched ? { credentialId: matched.credentialId, publicKey: matched.publicKey } : null;
2132
+ if (!res.ok) await throwApiError(res);
2133
+ const data = await res.json();
2134
+ return data.items;
2019
2135
  }
2020
- function shouldUsePasskeySignupPopup() {
2021
- return isSafari() && isInCrossOriginIframe();
2136
+ async function fetchAccount(apiBaseUrl, token, accountId, credentialId) {
2137
+ const params = new URLSearchParams({ credentialId });
2138
+ const res = await fetchWithRetry(`${apiBaseUrl}/v1/accounts/${accountId}?${params.toString()}`, {
2139
+ headers: { Authorization: `Bearer ${token}` }
2140
+ });
2141
+ if (!res.ok) await throwApiError(res);
2142
+ return await res.json();
2022
2143
  }
2023
- var SIGNUP_POPUP_TIMEOUT_MS = 12e4;
2024
- function signupWithPasskeyViaPopup() {
2025
- return new Promise((resolve, reject) => {
2026
- const popupUrl = `${window.location.origin}/passkey-signup`;
2027
- const popup = window.open(popupUrl, "blink-passkey-signup");
2028
- if (!popup) {
2029
- reject(new Error("Pop-up blocked. Please allow pop-ups for this site and try again."));
2030
- return;
2031
- }
2032
- let settled = false;
2033
- const timer = setTimeout(() => {
2034
- cleanup();
2035
- resolve(null);
2036
- }, SIGNUP_POPUP_TIMEOUT_MS);
2037
- function onMessage(event) {
2038
- if (event.origin !== window.location.origin) return;
2039
- if (event.source !== popup) return;
2040
- const data = event.data;
2041
- if (!data || data.type !== "blink:passkey-signup-complete") return;
2042
- if (typeof data.accessToken !== "string" || typeof data.credentialId !== "string" || typeof data.publicKey !== "string") return;
2043
- settled = true;
2044
- cleanup();
2045
- resolve({
2046
- accessToken: data.accessToken,
2047
- credentialId: data.credentialId,
2048
- publicKey: data.publicKey
2049
- });
2050
- }
2051
- window.addEventListener("message", onMessage);
2052
- const closedPoll = setInterval(() => {
2053
- if (popup.closed && !settled) {
2054
- settled = true;
2055
- cleanup();
2056
- resolve(null);
2057
- }
2058
- }, POPUP_CLOSED_POLL_MS);
2059
- function cleanup() {
2060
- clearTimeout(timer);
2061
- clearInterval(closedPoll);
2062
- window.removeEventListener("message", onMessage);
2063
- }
2064
- });
2065
- }
2066
- var LOGIN_POPUP_TIMEOUT_MS = 12e4;
2067
- function loginWithPasskeyViaPopup() {
2068
- return new Promise((resolve, reject) => {
2069
- const popupUrl = `${window.location.origin}/passkey-login`;
2070
- const popup = window.open(popupUrl, "blink-passkey-login");
2071
- if (!popup) {
2072
- reject(new Error("Pop-up blocked. Please allow pop-ups for this site and try again."));
2073
- return;
2074
- }
2075
- let settled = false;
2076
- const timer = setTimeout(() => {
2077
- cleanup();
2078
- resolve(null);
2079
- }, LOGIN_POPUP_TIMEOUT_MS);
2080
- function onMessage(event) {
2081
- if (event.origin !== window.location.origin) return;
2082
- if (event.source !== popup) return;
2083
- const data = event.data;
2084
- if (!data || data.type !== "blink:passkey-login-complete") return;
2085
- if (typeof data.accessToken !== "string" || typeof data.credentialId !== "string" || typeof data.publicKey !== "string") return;
2086
- settled = true;
2087
- cleanup();
2088
- resolve({
2089
- accessToken: data.accessToken,
2090
- credentialId: data.credentialId,
2091
- publicKey: data.publicKey
2092
- });
2093
- }
2094
- window.addEventListener("message", onMessage);
2095
- const closedPoll = setInterval(() => {
2096
- if (popup.closed && !settled) {
2097
- settled = true;
2098
- cleanup();
2099
- resolve(null);
2100
- }
2101
- }, POPUP_CLOSED_POLL_MS);
2102
- function cleanup() {
2103
- clearTimeout(timer);
2104
- clearInterval(closedPoll);
2105
- window.removeEventListener("message", onMessage);
2106
- }
2107
- });
2108
- }
2109
-
2110
- // src/credentialIdEncoding.ts
2111
- function credentialIdBase64ToBytes(value) {
2112
- const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
2113
- const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
2114
- const raw = atob(padded);
2115
- const bytes = new Uint8Array(raw.length);
2116
- for (let i = 0; i < raw.length; i++) {
2117
- bytes[i] = raw.charCodeAt(i);
2118
- }
2119
- return bytes;
2120
- }
2121
-
2122
- // src/passkeyRpId.ts
2123
- function normalizeConfiguredDomain(value) {
2124
- return value.replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^\./, "").trim();
2125
- }
2126
- function resolveRootDomainFromHostname(hostname) {
2127
- const trimmedHostname = hostname.trim().toLowerCase();
2128
- if (!trimmedHostname) {
2129
- return "localhost";
2130
- }
2131
- if (trimmedHostname === "localhost" || /^\d{1,3}(?:\.\d{1,3}){3}$/.test(trimmedHostname)) {
2132
- return trimmedHostname;
2133
- }
2134
- const parts = trimmedHostname.split(".").filter(Boolean);
2135
- if (parts.length < 2) {
2136
- return trimmedHostname;
2137
- }
2138
- return parts.slice(-2).join(".");
2139
- }
2140
-
2141
- // src/hooks/passkeyPublic.ts
2142
- function waitForDocumentFocus(timeoutMs = 5e3, intervalMs = 100) {
2143
- return new Promise((resolve) => {
2144
- if (typeof document === "undefined") {
2145
- resolve();
2146
- return;
2147
- }
2148
- if (document.hasFocus()) {
2149
- resolve();
2150
- return;
2151
- }
2152
- const deadline = Date.now() + timeoutMs;
2153
- const timer = setInterval(() => {
2154
- if (document.hasFocus()) {
2155
- clearInterval(timer);
2156
- resolve();
2157
- } else if (Date.now() >= deadline) {
2158
- clearInterval(timer);
2159
- resolve();
2160
- }
2161
- }, intervalMs);
2162
- });
2163
- }
2164
- function toBase64(buffer) {
2165
- return btoa(String.fromCharCode(...new Uint8Array(buffer)));
2166
- }
2167
- function readEnvValue(name) {
2168
- const meta = import.meta;
2169
- const metaValue = meta.env?.[name];
2170
- if (typeof metaValue === "string" && metaValue.trim().length > 0) {
2171
- return metaValue.trim();
2172
- }
2173
- const processValue = globalThis.process?.env?.[name];
2174
- if (typeof processValue === "string" && processValue.trim().length > 0) {
2175
- return processValue.trim();
2176
- }
2177
- return void 0;
2178
- }
2179
- function resolvePasskeyRpId() {
2180
- const configuredDomain = readEnvValue("VITE_DOMAIN") ?? readEnvValue("BLINK_DOMAIN");
2181
- if (configuredDomain) {
2182
- return normalizeConfiguredDomain(configuredDomain);
2183
- }
2184
- if (typeof window !== "undefined") {
2185
- return resolveRootDomainFromHostname(window.location.hostname);
2186
- }
2187
- return "localhost";
2188
- }
2189
- async function deviceHasPasskey(credentialId) {
2190
- const found = await findDevicePasskey([credentialId]);
2191
- return found != null;
2192
- }
2193
- async function findDevicePasskey(credentialIds) {
2194
- if (credentialIds.length === 0) return null;
2195
- try {
2196
- const challenge = new Uint8Array(32);
2197
- crypto.getRandomValues(challenge);
2198
- await waitForDocumentFocus();
2199
- const assertion = await navigator.credentials.get({
2200
- publicKey: {
2201
- challenge,
2202
- rpId: resolvePasskeyRpId(),
2203
- allowCredentials: credentialIds.map((id) => ({
2204
- type: "public-key",
2205
- id: credentialIdBase64ToBytes(id)
2206
- })),
2207
- userVerification: "discouraged",
2208
- timeout: 3e4
2209
- }
2210
- });
2211
- if (!assertion) return null;
2212
- return toBase64(assertion.rawId);
2213
- } catch {
2214
- return null;
2215
- }
2216
- }
2217
-
2218
- // src/api.ts
2219
- var api_exports = {};
2220
- __export(api_exports, {
2221
- createAccount: () => createAccount,
2222
- createAccountAuthorizationSession: () => createAccountAuthorizationSession,
2223
- createManualTransfer: () => createManualTransfer,
2224
- createTransfer: () => createTransfer,
2225
- fetchAccount: () => fetchAccount,
2226
- fetchAccounts: () => fetchAccounts,
2227
- fetchAuthorizationSession: () => fetchAuthorizationSession,
2228
- fetchAuthorizationSessionByToken: () => fetchAuthorizationSessionByToken,
2229
- fetchChains: () => fetchChains,
2230
- fetchManualTransferSession: () => fetchManualTransferSession,
2231
- fetchManualTransferSources: () => fetchManualTransferSources,
2232
- fetchMerchantPublicKey: () => fetchMerchantPublicKey,
2233
- fetchProviders: () => fetchProviders,
2234
- fetchTransfer: () => fetchTransfer,
2235
- fetchUserConfig: () => fetchUserConfig,
2236
- postTransferQuote: () => postTransferQuote,
2237
- probeActionCompletion: () => probeActionCompletion,
2238
- refreshManualTransferQuote: () => refreshManualTransferQuote,
2239
- regenerateTransferSignPayload: () => regenerateTransferSignPayload,
2240
- registerPasskey: () => registerPasskey,
2241
- reportActionCompletion: () => reportActionCompletion,
2242
- reportPasskeyActivity: () => reportPasskeyActivity,
2243
- setAuthorizationSessionPaymentIntentAmount: () => setAuthorizationSessionPaymentIntentAmount,
2244
- setAuthorizationSessionProvider: () => setAuthorizationSessionProvider,
2245
- signTransfer: () => signTransfer,
2246
- updateManualTransferDepositTargetChain: () => updateManualTransferDepositTargetChain,
2247
- updateUserConfig: () => updateUserConfig,
2248
- updateUserConfigBySession: () => updateUserConfigBySession,
2249
- waitForActionTransactionReceipt: () => waitForActionTransactionReceipt
2250
- });
2251
- var DEBUG_BUFFER_CAPACITY = 200;
2252
- var nextId = 1;
2253
- var entries = [];
2254
- var listeners = /* @__PURE__ */ new Set();
2255
- function notify() {
2256
- for (const listener of listeners) {
2257
- try {
2258
- listener();
2259
- } catch (err) {
2260
- console.error("[blink-sdk][debug-log] listener threw:", err);
2261
- }
2262
- }
2263
- }
2264
- function appendDebug(level, message, data) {
2265
- const entry = {
2266
- id: nextId++,
2267
- ts: Date.now(),
2268
- level,
2269
- message,
2270
- data
2271
- };
2272
- const next = entries.length >= DEBUG_BUFFER_CAPACITY ? entries.slice(entries.length - DEBUG_BUFFER_CAPACITY + 1) : entries.slice();
2273
- next.push(entry);
2274
- entries = next;
2275
- const prefix = "[blink-sdk][debug]";
2276
- const sink = level === "error" ? console.error : level === "warn" ? console.warn : console.info;
2277
- if (data !== void 0) {
2278
- sink(`${prefix} ${message}`, data);
2279
- } else {
2280
- sink(`${prefix} ${message}`);
2281
- }
2282
- notify();
2283
- }
2284
- function subscribeDebug(listener) {
2285
- listeners.add(listener);
2286
- return () => {
2287
- listeners.delete(listener);
2288
- };
2289
- }
2290
- function getDebugEntries() {
2291
- return entries;
2292
- }
2293
- function clearDebugEntries() {
2294
- entries = [];
2295
- notify();
2296
- }
2297
- function useBlinkDebugLog() {
2298
- return useSyncExternalStore(subscribeDebug, getDebugEntries, getDebugEntries);
2299
- }
2300
-
2301
- // src/fetchWithRetry.ts
2302
- var DEFAULT_MAX_RETRIES = 3;
2303
- var DEFAULT_BASE_DELAY_MS = 500;
2304
- var DEFAULT_MAX_JITTER_MS = 200;
2305
- function isNetworkTypeError(err) {
2306
- return err instanceof TypeError && /fetch|network|load failed/i.test(err.message);
2307
- }
2308
- async function fetchWithRetry(input, init, options) {
2309
- const maxRetries = DEFAULT_MAX_RETRIES;
2310
- const baseDelayMs = DEFAULT_BASE_DELAY_MS;
2311
- const maxJitterMs = DEFAULT_MAX_JITTER_MS;
2312
- const label = String(input).replace(/https?:\/\/[^/]+/, "");
2313
- let lastError;
2314
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
2315
- try {
2316
- return await fetch(input, init);
2317
- } catch (err) {
2318
- lastError = err;
2319
- if (!isNetworkTypeError(err)) {
2320
- throw err;
2321
- }
2322
- if (attempt < maxRetries) {
2323
- const delay = baseDelayMs * Math.pow(2, attempt) + Math.random() * maxJitterMs;
2324
- appendDebug("warn", `fetchWithRetry: network error, retrying ${label}`, {
2325
- attempt: attempt + 1,
2326
- maxRetries,
2327
- delayMs: Math.round(delay),
2328
- error: err instanceof Error ? err.message : String(err)
2329
- });
2330
- await new Promise((resolve) => setTimeout(resolve, delay));
2331
- }
2332
- }
2333
- }
2334
- throw lastError;
2335
- }
2336
-
2337
- // src/apiError.ts
2338
- var ApiError = class extends Error {
2339
- status;
2340
- code;
2341
- constructor(status, code, message) {
2342
- super(message);
2343
- this.name = "ApiError";
2344
- this.status = status;
2345
- this.code = code;
2346
- }
2347
- };
2348
- function isApiError(err) {
2349
- if (err instanceof ApiError) return true;
2350
- return typeof err === "object" && err !== null && "name" in err && err.name === "ApiError";
2351
- }
2352
- var SVM_SIGN_PAYLOAD_EXPIRED_CODE = "SVM_SIGN_PAYLOAD_EXPIRED";
2353
- function isSvmSignExpiredError(err) {
2354
- return isApiError(err) && err.code === SVM_SIGN_PAYLOAD_EXPIRED_CODE;
2355
- }
2356
-
2357
- // src/api.ts
2358
- async function throwApiError(res) {
2359
- const body = await res.json().catch(() => null);
2360
- const detail = body?.error ?? body;
2361
- const msg = detail?.message ?? res.statusText;
2362
- const code = detail?.code ?? String(res.status);
2363
- throw new ApiError(res.status, code, `${res.status} \u2014 ${code}: ${msg}`);
2364
- }
2365
- async function fetchProviders(apiBaseUrl, token) {
2366
- const headers = {};
2367
- if (token) {
2368
- headers.Authorization = `Bearer ${token}`;
2369
- }
2370
- const res = await fetchWithRetry(`${apiBaseUrl}/v1/providers`, { headers });
2371
- if (!res.ok) await throwApiError(res);
2372
- const data = await res.json();
2373
- return data.items;
2374
- }
2375
- async function fetchChains(apiBaseUrl, token) {
2376
- const res = await fetchWithRetry(`${apiBaseUrl}/v1/chains`, {
2377
- headers: { Authorization: `Bearer ${token}` }
2378
- });
2379
- if (!res.ok) await throwApiError(res);
2380
- const data = await res.json();
2381
- return data.items;
2382
- }
2383
- async function fetchAccounts(apiBaseUrl, token, credentialId) {
2384
- const params = new URLSearchParams({ credentialId });
2385
- const res = await fetchWithRetry(`${apiBaseUrl}/v1/accounts?${params.toString()}`, {
2386
- headers: { Authorization: `Bearer ${token}` }
2387
- });
2388
- if (!res.ok) await throwApiError(res);
2389
- const data = await res.json();
2390
- return data.items;
2391
- }
2392
- async function fetchAccount(apiBaseUrl, token, accountId, credentialId) {
2393
- const params = new URLSearchParams({ credentialId });
2394
- const res = await fetchWithRetry(`${apiBaseUrl}/v1/accounts/${accountId}?${params.toString()}`, {
2395
- headers: { Authorization: `Bearer ${token}` }
2396
- });
2144
+ async function fetchAccountBalances(apiBaseUrl, token, accountId, credentialId) {
2145
+ const params = new URLSearchParams({ credentialId });
2146
+ const res = await fetchWithRetry(
2147
+ `${apiBaseUrl}/v1/accounts/${accountId}/balances?${params.toString()}`,
2148
+ {
2149
+ headers: { Authorization: `Bearer ${token}` }
2150
+ }
2151
+ );
2397
2152
  if (!res.ok) await throwApiError(res);
2398
2153
  return await res.json();
2399
2154
  }
@@ -2714,44 +2469,323 @@ async function waitForActionTransactionReceipt(apiBaseUrl, actionId, txHash) {
2714
2469
  headers: { "Content-Type": "application/json" },
2715
2470
  body: JSON.stringify({ txHash })
2716
2471
  }
2717
- );
2718
- if (!res.ok) await throwApiError(res);
2719
- return await res.json();
2472
+ );
2473
+ if (!res.ok) await throwApiError(res);
2474
+ return await res.json();
2475
+ }
2476
+ async function probeActionCompletion(apiBaseUrl, actionId) {
2477
+ const res = await fetchWithRetry(
2478
+ `${apiBaseUrl}/v1/authorization-actions/${actionId}`,
2479
+ {
2480
+ method: "PATCH",
2481
+ headers: { "Content-Type": "application/json" },
2482
+ body: JSON.stringify({ status: "COMPLETED", result: {} })
2483
+ }
2484
+ );
2485
+ if (res.ok) {
2486
+ const session = await res.json();
2487
+ return { detected: true, session };
2488
+ }
2489
+ const body = await res.json().catch(() => null);
2490
+ const detail = body?.error ?? body;
2491
+ const code = detail?.code;
2492
+ const message = detail?.message ?? res.statusText ?? `HTTP ${res.status}`;
2493
+ if (res.status === 422 && code === "DEPOSIT_TX_NOT_FOUND") {
2494
+ return { detected: false, reason: "not-found", status: res.status, code, message };
2495
+ }
2496
+ const approvalNotDetectedCodes = /* @__PURE__ */ new Set([
2497
+ "APPROVE_NOT_DETECTED",
2498
+ "APPROVE_SPL_NOT_DETECTED",
2499
+ "SPL_DELEGATE_MISSING",
2500
+ "SPL_DELEGATE_INSUFFICIENT",
2501
+ "SPL_DELEGATE_WRONG_OWNER"
2502
+ ]);
2503
+ if (res.status === 422 && code && approvalNotDetectedCodes.has(code)) {
2504
+ return { detected: false, reason: "not-found", status: res.status, code, message };
2505
+ }
2506
+ if (res.status === 422 && code === "INVALID_TRANSFER_STATE") {
2507
+ return { detected: false, reason: "invalid-state", status: res.status, code, message };
2508
+ }
2509
+ return { detected: false, reason: "error", status: res.status, code, message };
2510
+ }
2511
+
2512
+ // src/passkey-delegation.ts
2513
+ var PasskeyIframeBlockedError = class extends Error {
2514
+ constructor(message = "Passkey creation is not supported in this browser context.") {
2515
+ super(message);
2516
+ this.name = "PasskeyIframeBlockedError";
2517
+ }
2518
+ };
2519
+ function isInCrossOriginIframe() {
2520
+ if (typeof window === "undefined") return false;
2521
+ if (window.parent === window) return false;
2522
+ try {
2523
+ void window.parent.location.origin;
2524
+ return false;
2525
+ } catch {
2526
+ return true;
2527
+ }
2528
+ }
2529
+ function isSafari() {
2530
+ if (typeof navigator === "undefined") return false;
2531
+ const ua = navigator.userAgent;
2532
+ return /Safari/i.test(ua) && !/Chrome|CriOS|Chromium|Edg|OPR|Firefox/i.test(ua);
2533
+ }
2534
+ var VERIFY_POPUP_TIMEOUT_MS = 6e4;
2535
+ var POPUP_CLOSED_POLL_MS = 500;
2536
+ var POPUP_CLOSED_GRACE_MS = 1e3;
2537
+ function findDevicePasskeyViaPopup(options) {
2538
+ return new Promise((resolve, reject) => {
2539
+ const verificationToken = crypto.randomUUID();
2540
+ const payload = {
2541
+ ...options,
2542
+ verificationToken
2543
+ };
2544
+ const encoded = btoa(JSON.stringify(payload));
2545
+ const popupUrl = `${window.location.origin}/passkey-verify#${encoded}`;
2546
+ const popup = window.open(popupUrl, "blink-passkey-verify");
2547
+ if (!popup) {
2548
+ reject(new Error("Pop-up blocked. Please allow pop-ups for this site and try again."));
2549
+ return;
2550
+ }
2551
+ let settled = false;
2552
+ const timer = setTimeout(() => {
2553
+ cleanup();
2554
+ resolve(null);
2555
+ }, VERIFY_POPUP_TIMEOUT_MS);
2556
+ const closedPoll = setInterval(() => {
2557
+ if (popup.closed && !settled) {
2558
+ clearInterval(closedPoll);
2559
+ setTimeout(() => {
2560
+ if (!settled) {
2561
+ settled = true;
2562
+ cleanup();
2563
+ checkServerForPasskeyByToken(
2564
+ options.authToken,
2565
+ options.apiBaseUrl,
2566
+ verificationToken
2567
+ ).then((result) => {
2568
+ resolve(result?.credentialId ?? null);
2569
+ }).catch(() => {
2570
+ resolve(null);
2571
+ });
2572
+ }
2573
+ }, POPUP_CLOSED_GRACE_MS);
2574
+ }
2575
+ }, POPUP_CLOSED_POLL_MS);
2576
+ function cleanup() {
2577
+ clearTimeout(timer);
2578
+ clearInterval(closedPoll);
2579
+ }
2580
+ });
2581
+ }
2582
+ async function checkServerForPasskeyByToken(authToken, apiBaseUrl, verificationToken) {
2583
+ if (!authToken || !apiBaseUrl) return null;
2584
+ const res = await fetch(`${apiBaseUrl}/v1/users/config`, {
2585
+ headers: { Authorization: `Bearer ${authToken}` }
2586
+ });
2587
+ if (!res.ok) return null;
2588
+ const body = await res.json();
2589
+ const passkeys = body.config.passkeys ?? [];
2590
+ const matched = passkeys.find((p) => p.lastVerificationToken === verificationToken);
2591
+ return matched ? { credentialId: matched.credentialId, publicKey: matched.publicKey } : null;
2592
+ }
2593
+ function shouldUsePasskeySignupPopup() {
2594
+ return isSafari() && isInCrossOriginIframe();
2595
+ }
2596
+ var SIGNUP_POPUP_TIMEOUT_MS = 12e4;
2597
+ function signupWithPasskeyViaPopup() {
2598
+ return new Promise((resolve, reject) => {
2599
+ const popupUrl = `${window.location.origin}/passkey-signup`;
2600
+ const popup = window.open(popupUrl, "blink-passkey-signup");
2601
+ if (!popup) {
2602
+ reject(new Error("Pop-up blocked. Please allow pop-ups for this site and try again."));
2603
+ return;
2604
+ }
2605
+ let settled = false;
2606
+ const timer = setTimeout(() => {
2607
+ cleanup();
2608
+ resolve(null);
2609
+ }, SIGNUP_POPUP_TIMEOUT_MS);
2610
+ function onMessage(event) {
2611
+ if (event.origin !== window.location.origin) return;
2612
+ if (event.source !== popup) return;
2613
+ const data = event.data;
2614
+ if (!data || data.type !== "blink:passkey-signup-complete") return;
2615
+ if (typeof data.accessToken !== "string" || typeof data.credentialId !== "string" || typeof data.publicKey !== "string") return;
2616
+ settled = true;
2617
+ cleanup();
2618
+ resolve({
2619
+ accessToken: data.accessToken,
2620
+ credentialId: data.credentialId,
2621
+ publicKey: data.publicKey
2622
+ });
2623
+ }
2624
+ window.addEventListener("message", onMessage);
2625
+ const closedPoll = setInterval(() => {
2626
+ if (popup.closed && !settled) {
2627
+ settled = true;
2628
+ cleanup();
2629
+ resolve(null);
2630
+ }
2631
+ }, POPUP_CLOSED_POLL_MS);
2632
+ function cleanup() {
2633
+ clearTimeout(timer);
2634
+ clearInterval(closedPoll);
2635
+ window.removeEventListener("message", onMessage);
2636
+ }
2637
+ });
2638
+ }
2639
+ var LOGIN_POPUP_TIMEOUT_MS = 12e4;
2640
+ function loginWithPasskeyViaPopup() {
2641
+ return new Promise((resolve, reject) => {
2642
+ const popupUrl = `${window.location.origin}/passkey-login`;
2643
+ const popup = window.open(popupUrl, "blink-passkey-login");
2644
+ if (!popup) {
2645
+ reject(new Error("Pop-up blocked. Please allow pop-ups for this site and try again."));
2646
+ return;
2647
+ }
2648
+ let settled = false;
2649
+ const timer = setTimeout(() => {
2650
+ cleanup();
2651
+ resolve(null);
2652
+ }, LOGIN_POPUP_TIMEOUT_MS);
2653
+ function onMessage(event) {
2654
+ if (event.origin !== window.location.origin) return;
2655
+ if (event.source !== popup) return;
2656
+ const data = event.data;
2657
+ if (!data || data.type !== "blink:passkey-login-complete") return;
2658
+ if (typeof data.accessToken !== "string" || typeof data.credentialId !== "string" || typeof data.publicKey !== "string") return;
2659
+ settled = true;
2660
+ cleanup();
2661
+ resolve({
2662
+ accessToken: data.accessToken,
2663
+ credentialId: data.credentialId,
2664
+ publicKey: data.publicKey
2665
+ });
2666
+ }
2667
+ window.addEventListener("message", onMessage);
2668
+ const closedPoll = setInterval(() => {
2669
+ if (popup.closed && !settled) {
2670
+ settled = true;
2671
+ cleanup();
2672
+ resolve(null);
2673
+ }
2674
+ }, POPUP_CLOSED_POLL_MS);
2675
+ function cleanup() {
2676
+ clearTimeout(timer);
2677
+ clearInterval(closedPoll);
2678
+ window.removeEventListener("message", onMessage);
2679
+ }
2680
+ });
2681
+ }
2682
+
2683
+ // src/credentialIdEncoding.ts
2684
+ function credentialIdBase64ToBytes(value) {
2685
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
2686
+ const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
2687
+ const raw = atob(padded);
2688
+ const bytes = new Uint8Array(raw.length);
2689
+ for (let i = 0; i < raw.length; i++) {
2690
+ bytes[i] = raw.charCodeAt(i);
2691
+ }
2692
+ return bytes;
2693
+ }
2694
+
2695
+ // src/passkeyRpId.ts
2696
+ function normalizeConfiguredDomain(value) {
2697
+ return value.replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^\./, "").trim();
2698
+ }
2699
+ function resolveRootDomainFromHostname(hostname) {
2700
+ const trimmedHostname = hostname.trim().toLowerCase();
2701
+ if (!trimmedHostname) {
2702
+ return "localhost";
2703
+ }
2704
+ if (trimmedHostname === "localhost" || /^\d{1,3}(?:\.\d{1,3}){3}$/.test(trimmedHostname)) {
2705
+ return trimmedHostname;
2706
+ }
2707
+ const parts = trimmedHostname.split(".").filter(Boolean);
2708
+ if (parts.length < 2) {
2709
+ return trimmedHostname;
2710
+ }
2711
+ return parts.slice(-2).join(".");
2712
+ }
2713
+
2714
+ // src/hooks/passkeyPublic.ts
2715
+ function waitForDocumentFocus(timeoutMs = 5e3, intervalMs = 100) {
2716
+ return new Promise((resolve) => {
2717
+ if (typeof document === "undefined") {
2718
+ resolve();
2719
+ return;
2720
+ }
2721
+ if (document.hasFocus()) {
2722
+ resolve();
2723
+ return;
2724
+ }
2725
+ const deadline = Date.now() + timeoutMs;
2726
+ const timer = setInterval(() => {
2727
+ if (document.hasFocus()) {
2728
+ clearInterval(timer);
2729
+ resolve();
2730
+ } else if (Date.now() >= deadline) {
2731
+ clearInterval(timer);
2732
+ resolve();
2733
+ }
2734
+ }, intervalMs);
2735
+ });
2720
2736
  }
2721
- async function probeActionCompletion(apiBaseUrl, actionId) {
2722
- const res = await fetchWithRetry(
2723
- `${apiBaseUrl}/v1/authorization-actions/${actionId}`,
2724
- {
2725
- method: "PATCH",
2726
- headers: { "Content-Type": "application/json" },
2727
- body: JSON.stringify({ status: "COMPLETED", result: {} })
2728
- }
2729
- );
2730
- if (res.ok) {
2731
- const session = await res.json();
2732
- return { detected: true, session };
2737
+ function toBase64(buffer) {
2738
+ return btoa(String.fromCharCode(...new Uint8Array(buffer)));
2739
+ }
2740
+ function readEnvValue(name) {
2741
+ const meta = import.meta;
2742
+ const metaValue = meta.env?.[name];
2743
+ if (typeof metaValue === "string" && metaValue.trim().length > 0) {
2744
+ return metaValue.trim();
2733
2745
  }
2734
- const body = await res.json().catch(() => null);
2735
- const detail = body?.error ?? body;
2736
- const code = detail?.code;
2737
- const message = detail?.message ?? res.statusText ?? `HTTP ${res.status}`;
2738
- if (res.status === 422 && code === "DEPOSIT_TX_NOT_FOUND") {
2739
- return { detected: false, reason: "not-found", status: res.status, code, message };
2746
+ const processValue = globalThis.process?.env?.[name];
2747
+ if (typeof processValue === "string" && processValue.trim().length > 0) {
2748
+ return processValue.trim();
2740
2749
  }
2741
- const approvalNotDetectedCodes = /* @__PURE__ */ new Set([
2742
- "APPROVE_NOT_DETECTED",
2743
- "APPROVE_SPL_NOT_DETECTED",
2744
- "SPL_DELEGATE_MISSING",
2745
- "SPL_DELEGATE_INSUFFICIENT",
2746
- "SPL_DELEGATE_WRONG_OWNER"
2747
- ]);
2748
- if (res.status === 422 && code && approvalNotDetectedCodes.has(code)) {
2749
- return { detected: false, reason: "not-found", status: res.status, code, message };
2750
+ return void 0;
2751
+ }
2752
+ function resolvePasskeyRpId() {
2753
+ const configuredDomain = readEnvValue("VITE_DOMAIN") ?? readEnvValue("BLINK_DOMAIN");
2754
+ if (configuredDomain) {
2755
+ return normalizeConfiguredDomain(configuredDomain);
2750
2756
  }
2751
- if (res.status === 422 && code === "INVALID_TRANSFER_STATE") {
2752
- return { detected: false, reason: "invalid-state", status: res.status, code, message };
2757
+ if (typeof window !== "undefined") {
2758
+ return resolveRootDomainFromHostname(window.location.hostname);
2759
+ }
2760
+ return "localhost";
2761
+ }
2762
+ async function deviceHasPasskey(credentialId) {
2763
+ const found = await findDevicePasskey([credentialId]);
2764
+ return found != null;
2765
+ }
2766
+ async function findDevicePasskey(credentialIds) {
2767
+ if (credentialIds.length === 0) return null;
2768
+ try {
2769
+ const challenge = new Uint8Array(32);
2770
+ crypto.getRandomValues(challenge);
2771
+ await waitForDocumentFocus();
2772
+ const assertion = await navigator.credentials.get({
2773
+ publicKey: {
2774
+ challenge,
2775
+ rpId: resolvePasskeyRpId(),
2776
+ allowCredentials: credentialIds.map((id) => ({
2777
+ type: "public-key",
2778
+ id: credentialIdBase64ToBytes(id)
2779
+ })),
2780
+ userVerification: "discouraged",
2781
+ timeout: 3e4
2782
+ }
2783
+ });
2784
+ if (!assertion) return null;
2785
+ return toBase64(assertion.rawId);
2786
+ } catch {
2787
+ return null;
2753
2788
  }
2754
- return { detected: false, reason: "error", status: res.status, code, message };
2755
2789
  }
2756
2790
 
2757
2791
  // src/transferPolling.ts
@@ -4125,14 +4159,14 @@ function isBatchableAction(action) {
4125
4159
  return BATCHABLE_ACTION_TYPES.has(action.type);
4126
4160
  }
4127
4161
  function getSolanaWalletSelection(action) {
4128
- const providerName = action.metadata?.providerName;
4129
- if (typeof providerName !== "string" || providerName.trim() === "") {
4162
+ const providerName2 = action.metadata?.providerName;
4163
+ if (typeof providerName2 !== "string" || providerName2.trim() === "") {
4130
4164
  throw new Error(`${action.type} metadata is missing providerName.`);
4131
4165
  }
4132
4166
  const providerId = action.metadata?.providerId;
4133
4167
  return {
4134
4168
  ...typeof providerId === "string" && providerId.trim() !== "" ? { providerId } : {},
4135
- providerName
4169
+ providerName: providerName2
4136
4170
  };
4137
4171
  }
4138
4172
  function isPhantomEvmProviderAvailable() {
@@ -5782,6 +5816,43 @@ function useAuthorizationExecutor(options) {
5782
5816
  }),
5783
5817
  []
5784
5818
  );
5819
+ const [awaitingApproval, setAwaitingApproval] = useState(false);
5820
+ const [approvalDestinationAddress, setApprovalDestinationAddress] = useState(null);
5821
+ const approvalResolverRef = useRef(null);
5822
+ const approvalRejectRef = useRef(null);
5823
+ const approvalInitiatedRef = useRef(false);
5824
+ const waitForApproval = useCallback(
5825
+ (action, destinationAddress) => {
5826
+ if (approvalInitiatedRef.current) return Promise.resolve();
5827
+ return new Promise((resolve, reject) => {
5828
+ approvalResolverRef.current = resolve;
5829
+ approvalRejectRef.current = reject;
5830
+ setCurrentAction(action);
5831
+ setApprovalDestinationAddress(destinationAddress);
5832
+ setAwaitingApproval(true);
5833
+ });
5834
+ },
5835
+ []
5836
+ );
5837
+ const approveAuthorization = useCallback(() => {
5838
+ approvalInitiatedRef.current = true;
5839
+ setAwaitingApproval(false);
5840
+ if (approvalResolverRef.current) {
5841
+ approvalResolverRef.current();
5842
+ approvalResolverRef.current = null;
5843
+ approvalRejectRef.current = null;
5844
+ }
5845
+ }, []);
5846
+ const resetApprovalGate = useCallback(() => {
5847
+ approvalInitiatedRef.current = false;
5848
+ setAwaitingApproval(false);
5849
+ setApprovalDestinationAddress(null);
5850
+ if (approvalRejectRef.current) {
5851
+ approvalRejectRef.current(new AuthorizationSessionCancelledError());
5852
+ approvalRejectRef.current = null;
5853
+ approvalResolverRef.current = null;
5854
+ }
5855
+ }, []);
5785
5856
  const cancelPendingExecution = useCallback(() => {
5786
5857
  if (selectSourceRejectRef.current) {
5787
5858
  selectSourceRejectRef.current(new AuthorizationSessionCancelledError());
@@ -5789,6 +5860,12 @@ function useAuthorizationExecutor(options) {
5789
5860
  selectSourceResolverRef.current = null;
5790
5861
  setPendingSelectSource(null);
5791
5862
  }
5863
+ if (approvalRejectRef.current) {
5864
+ approvalRejectRef.current(new AuthorizationSessionCancelledError());
5865
+ approvalRejectRef.current = null;
5866
+ approvalResolverRef.current = null;
5867
+ }
5868
+ setAwaitingApproval(false);
5792
5869
  setError(null);
5793
5870
  setCurrentAction(null);
5794
5871
  setApproveSplConfirming(null);
@@ -6191,6 +6268,11 @@ function useAuthorizationExecutor(options) {
6191
6268
  currentAction,
6192
6269
  approveSplConfirming,
6193
6270
  pendingSelectSource,
6271
+ awaitingApproval,
6272
+ approvalDestinationAddress,
6273
+ waitForApproval,
6274
+ approveAuthorization,
6275
+ resetApprovalGate,
6194
6276
  resolveSelectSource,
6195
6277
  cancelPendingExecution,
6196
6278
  resetWalletConnect,
@@ -6485,6 +6567,7 @@ function useAuthorizationOrchestrator(deps) {
6485
6567
  const [pendingSelectSourceAction, setPendingSelectSourceAction] = useState(null);
6486
6568
  const [orchestratorCompleted, setOrchestratorCompleted] = useState(false);
6487
6569
  const [sourceSelectionResolved, setSourceSelectionResolved] = useState(false);
6570
+ const [approvalSmartAccountAddress, setApprovalSmartAccountAddress] = useState(null);
6488
6571
  const selectSourceResolverRef = useRef(null);
6489
6572
  const selectSourceRejectRef = useRef(null);
6490
6573
  const submittedApprovePermit2ActionIdsRef = useRef(/* @__PURE__ */ new Set());
@@ -6510,6 +6593,7 @@ function useAuthorizationOrchestrator(deps) {
6510
6593
  const cancellation = new AuthorizationSessionCancelledError();
6511
6594
  setOrchestratorCompleted(false);
6512
6595
  setSourceSelectionResolved(false);
6596
+ setApprovalSmartAccountAddress(null);
6513
6597
  if (selectSourceRejectRef.current) {
6514
6598
  selectSourceRejectRef.current(cancellation);
6515
6599
  selectSourceRejectRef.current = null;
@@ -6532,6 +6616,10 @@ function useAuthorizationOrchestrator(deps) {
6532
6616
  lastRunRef.current = { sessionId, options };
6533
6617
  setOrchestratorCompleted(false);
6534
6618
  setSourceSelectionResolved(false);
6619
+ setApprovalSmartAccountAddress(null);
6620
+ if (!options?.keepApprovalGate) {
6621
+ authExecutor.resetApprovalGate();
6622
+ }
6535
6623
  if (!authExecutor.beginExecution()) {
6536
6624
  appendDebug("warn", "orchestrator:run aborted \u2014 already executing");
6537
6625
  return { status: "cancelled" };
@@ -6560,6 +6648,8 @@ function useAuthorizationOrchestrator(deps) {
6560
6648
  let action = mergedPending[0];
6561
6649
  if (completedIds.has(action.id)) break;
6562
6650
  const ownerSessionId = actionSessionMap.get(action.id) ?? sessionId;
6651
+ const ownerSessionSca = sessions.find((s) => s.id === ownerSessionId)?.session.smartAccountAddress;
6652
+ if (ownerSessionSca) setApprovalSmartAccountAddress(ownerSessionSca);
6563
6653
  console.info("[blink-sdk][orchestrator] Next pending action.", {
6564
6654
  actionId: action.id,
6565
6655
  actionType: action.type,
@@ -6584,6 +6674,7 @@ function useAuthorizationOrchestrator(deps) {
6584
6674
  const autoResult = createSelectSourceResult(action, options.autoResolveSource);
6585
6675
  completedIds.add(action.id);
6586
6676
  authExecutor.addResult(autoResult);
6677
+ setSourceSelectionResolved(true);
6587
6678
  const reportedSession3 = await reportActionCompletionWithLogging(
6588
6679
  apiBaseUrl,
6589
6680
  action,
@@ -6677,6 +6768,13 @@ function useAuthorizationOrchestrator(deps) {
6677
6768
  throw new Error(SIGN_PERMIT2_CONFIRMING_MESSAGE);
6678
6769
  }
6679
6770
  }
6771
+ if (isApprovalGatedAction(action)) {
6772
+ const sessionSca = sessions.find((s) => s.id === ownerSessionId)?.session.smartAccountAddress;
6773
+ await authExecutor.waitForApproval(
6774
+ action,
6775
+ sessionSca ?? readActionMetadataString(action.metadata, "smartAccountAddress") ?? readActionMetadataString(action.metadata, "ownerPubkey")
6776
+ );
6777
+ }
6680
6778
  appendDebug("info", `orchestrator:executeAction start ${action.type}`, {
6681
6779
  actionId: action.id,
6682
6780
  ownerSessionId
@@ -6873,7 +6971,8 @@ function useAuthorizationOrchestrator(deps) {
6873
6971
  authExecutor.setError(null);
6874
6972
  return run(lastRun.sessionId, {
6875
6973
  ...lastRun.options,
6876
- probeBeforePrompt: true
6974
+ probeBeforePrompt: true,
6975
+ keepApprovalGate: true
6877
6976
  });
6878
6977
  }, [authExecutor, run, cancelPendingFlow]);
6879
6978
  return {
@@ -6883,9 +6982,17 @@ function useAuthorizationOrchestrator(deps) {
6883
6982
  resolveSelectSource,
6884
6983
  sourceSelectionResolved,
6885
6984
  orchestratorCompleted,
6985
+ approvalSmartAccountAddress,
6886
6986
  cancelPendingFlow
6887
6987
  };
6888
6988
  }
6989
+ function readActionMetadataString(metadata, key) {
6990
+ const value = metadata?.[key];
6991
+ return typeof value === "string" && value.trim() !== "" ? value : null;
6992
+ }
6993
+ function isApprovalGatedAction(action) {
6994
+ return action.type === "APPROVE_PERMIT2" || action.type === "SIGN_PERMIT2" || action.type === "APPROVE_SPL";
6995
+ }
6889
6996
  function createSelectSourceResult(action, selection) {
6890
6997
  return {
6891
6998
  actionId: action.id,
@@ -7856,6 +7963,26 @@ function deriveSourceTypeAndId(state) {
7856
7963
  }
7857
7964
  return { sourceType: "accountId", sourceId: "" };
7858
7965
  }
7966
+ function hasAnyBalances(accounts) {
7967
+ return accounts.some(
7968
+ (account) => account.wallets.some(
7969
+ (wallet) => wallet.balance !== void 0 || wallet.sources !== void 0
7970
+ )
7971
+ );
7972
+ }
7973
+ function carryOverBalances(prev, next) {
7974
+ const prevWallets = new Map(
7975
+ prev.flatMap((account) => account.wallets.map((wallet) => [wallet.id, wallet]))
7976
+ );
7977
+ return next.map((account) => ({
7978
+ ...account,
7979
+ wallets: account.wallets.map((wallet) => {
7980
+ if (wallet.balance !== void 0 || wallet.sources !== void 0) return wallet;
7981
+ const prior = prevWallets.get(wallet.id);
7982
+ return prior && (prior.balance !== void 0 || prior.sources !== void 0) ? { ...wallet, balance: prior.balance, sources: prior.sources } : wallet;
7983
+ })
7984
+ }));
7985
+ }
7859
7986
  function clearStaleSelection(state) {
7860
7987
  if (state.selectedAccountId == null) return state;
7861
7988
  if (state.desktopWait != null || state.standardDesktopInlineOpenWallet) return state;
@@ -7877,6 +8004,7 @@ function createInitialState(config) {
7877
8004
  accounts: [],
7878
8005
  chains: [],
7879
8006
  loadingData: false,
8007
+ balancesLoading: false,
7880
8008
  depositSelectionRefreshing: false,
7881
8009
  selectedProviderId: null,
7882
8010
  selectedAccountId: null,
@@ -7906,6 +8034,7 @@ function createInitialState(config) {
7906
8034
  privyAuthenticated: false,
7907
8035
  lastResumedAt: 0,
7908
8036
  setupDepositToken: null,
8037
+ setupSpendingLimit: null,
7909
8038
  guestWalletPrepared: null,
7910
8039
  guestWalletDeeplinksPreparing: false,
7911
8040
  amountTooLow: null,
@@ -7925,6 +8054,7 @@ function clearAuthenticatedSessionState(state) {
7925
8054
  accounts: [],
7926
8055
  chains: [],
7927
8056
  loadingData: false,
8057
+ balancesLoading: false,
7928
8058
  depositSelectionRefreshing: false,
7929
8059
  selectedProviderId: null,
7930
8060
  selectedAccountId: null,
@@ -7950,6 +8080,7 @@ function clearAuthenticatedSessionState(state) {
7950
8080
  mobileTokenAuthorizationPending: false,
7951
8081
  linkSettling: false,
7952
8082
  setupDepositToken: null,
8083
+ setupSpendingLimit: null,
7953
8084
  guestWalletPrepared: null,
7954
8085
  guestWalletDeeplinksPreparing: false,
7955
8086
  amountTooLow: null,
@@ -8018,6 +8149,8 @@ function applyAction(state, action) {
8018
8149
  providers: action.providers,
8019
8150
  accounts: action.accounts,
8020
8151
  chains: action.chains,
8152
+ // Accounts arrive balance-free; balances follow via BALANCES_LOADED.
8153
+ balancesLoading: true,
8021
8154
  depositSelectionRefreshing: false,
8022
8155
  initialDataLoaded: true
8023
8156
  };
@@ -8041,10 +8174,12 @@ function applyAction(state, action) {
8041
8174
  depositSelectionRefreshing: false
8042
8175
  };
8043
8176
  case "ACCOUNTS_RELOADED": {
8177
+ const hadBalances = hasAnyBalances(state.accounts);
8044
8178
  const next = {
8045
8179
  ...state,
8046
- accounts: action.accounts,
8180
+ accounts: carryOverBalances(state.accounts, action.accounts),
8047
8181
  providers: action.providers,
8182
+ balancesLoading: !hadBalances,
8048
8183
  initialDataLoaded: true
8049
8184
  };
8050
8185
  if (action.defaults) {
@@ -8056,6 +8191,24 @@ function applyAction(state, action) {
8056
8191
  }
8057
8192
  return clearStaleSelection(next);
8058
8193
  }
8194
+ case "BALANCES_LOADED": {
8195
+ const { balancesByAccountId } = action;
8196
+ const accounts = state.accounts.map((account) => {
8197
+ const accountBalances = balancesByAccountId[account.id];
8198
+ if (!accountBalances) return account;
8199
+ const balancesByWalletId = new Map(
8200
+ accountBalances.wallets.map((w) => [w.id, w])
8201
+ );
8202
+ return {
8203
+ ...account,
8204
+ wallets: account.wallets.map((wallet) => {
8205
+ const merged = balancesByWalletId.get(wallet.id);
8206
+ return merged ? { ...wallet, balance: merged.balance, sources: merged.sources } : wallet;
8207
+ })
8208
+ };
8209
+ });
8210
+ return { ...state, accounts, balancesLoading: false };
8211
+ }
8059
8212
  case "SET_DEPOSIT_SELECTION_REFRESHING":
8060
8213
  return { ...state, depositSelectionRefreshing: action.value };
8061
8214
  case "SAVE_SELECTION":
@@ -8127,6 +8280,7 @@ function applyAction(state, action) {
8127
8280
  mobileFlow: false,
8128
8281
  mobileTokenAuthorizationPending: false,
8129
8282
  setupDepositToken: null,
8283
+ setupSpendingLimit: null,
8130
8284
  setupFlowScreen: null
8131
8285
  };
8132
8286
  case "PAY_ENDED":
@@ -8372,6 +8526,7 @@ function applyAction(state, action) {
8372
8526
  linkSettling: false,
8373
8527
  savedSelection: null,
8374
8528
  setupDepositToken: null,
8529
+ setupSpendingLimit: null,
8375
8530
  setupFlowScreen: null,
8376
8531
  guestWalletPrepared: null,
8377
8532
  guestWalletDeeplinksPreparing: false,
@@ -8437,12 +8592,217 @@ function applyAction(state, action) {
8437
8592
  ...action.chainId != null ? { chainId: action.chainId } : {}
8438
8593
  }
8439
8594
  };
8595
+ case "SET_SETUP_SPENDING_LIMIT":
8596
+ return { ...state, setupSpendingLimit: action.limit };
8440
8597
  case "CLEAR_SETUP_DEPOSIT_TOKEN":
8441
- return { ...state, setupDepositToken: null };
8598
+ return { ...state, setupDepositToken: null, setupSpendingLimit: null };
8442
8599
  default:
8443
8600
  return state;
8444
8601
  }
8445
8602
  }
8603
+ var BLOCKED_SUBSTRINGS = [
8604
+ "bearer",
8605
+ "signature",
8606
+ "signedpayload",
8607
+ "userop",
8608
+ "jwt",
8609
+ "secret",
8610
+ "password",
8611
+ "privatekey",
8612
+ "mnemonic",
8613
+ "seedphrase"
8614
+ ];
8615
+ function isBlockedKey(key) {
8616
+ const lower = key.toLowerCase();
8617
+ if (lower === "token" || lower.endsWith("token")) return true;
8618
+ return BLOCKED_SUBSTRINGS.some((blocked) => lower.includes(blocked));
8619
+ }
8620
+ function isReady() {
8621
+ return typeof posthog !== "undefined" && posthog.__loaded === true;
8622
+ }
8623
+ function sanitizeProps(props) {
8624
+ if (!props) return props;
8625
+ const clean = {};
8626
+ for (const [key, value] of Object.entries(props)) {
8627
+ if (isBlockedKey(key)) continue;
8628
+ if (value === void 0) continue;
8629
+ clean[key] = value;
8630
+ }
8631
+ return clean;
8632
+ }
8633
+ function registerSuperProps(props) {
8634
+ if (!isReady()) return;
8635
+ try {
8636
+ posthog.register(sanitizeProps(props) ?? {});
8637
+ } catch {
8638
+ }
8639
+ }
8640
+ function track(event, props) {
8641
+ if (!isReady()) return;
8642
+ try {
8643
+ posthog.capture(event, sanitizeProps(props));
8644
+ } catch {
8645
+ }
8646
+ }
8647
+ function identifyUser(privyUserId, props) {
8648
+ if (!isReady() || !privyUserId) return;
8649
+ try {
8650
+ posthog.identify(privyUserId, sanitizeProps(props));
8651
+ } catch {
8652
+ }
8653
+ }
8654
+ function resolveAnalyticsEnvironment(opts) {
8655
+ const explicit = opts.explicit?.trim();
8656
+ if (explicit) return explicit;
8657
+ const raw = opts.apiBaseUrl ?? "";
8658
+ let host = raw;
8659
+ try {
8660
+ host = new URL(raw).hostname;
8661
+ } catch {
8662
+ }
8663
+ const h = host.toLowerCase();
8664
+ if (h === "" || h.includes("localhost") || h.startsWith("127.")) return "development";
8665
+ if (h.includes("smokebox")) return "smokebox";
8666
+ if (h.includes("sandbox")) return "sandbox";
8667
+ if (h.includes("staging")) return "staging";
8668
+ return "production";
8669
+ }
8670
+ function resetUser() {
8671
+ if (!isReady()) return;
8672
+ try {
8673
+ posthog.reset();
8674
+ } catch {
8675
+ }
8676
+ }
8677
+
8678
+ // src/analyticsEvents.ts
8679
+ function providerName(state, providerId) {
8680
+ return state.providers.find((p) => p.id === providerId)?.name;
8681
+ }
8682
+ function amountUsd(transfer) {
8683
+ return transfer.amount?.currency === "USD" ? transfer.amount.amount : transfer.amount?.amount;
8684
+ }
8685
+ function sourceChain(transfer) {
8686
+ return transfer.sources?.[0]?.provider?.chainFamily;
8687
+ }
8688
+ function trackAction(action, state, ctx) {
8689
+ const device = ctx.device;
8690
+ switch (action.type) {
8691
+ case "PASSKEY_ACTIVATED":
8692
+ track("auth_succeeded", { method: "passkey", device });
8693
+ break;
8694
+ case "SELECT_PROVIDER":
8695
+ track("wallet_provider_selected", {
8696
+ device,
8697
+ providerId: action.providerId,
8698
+ providerName: providerName(state, action.providerId),
8699
+ transport: device === "desktop" ? "inline" : "deeplink"
8700
+ });
8701
+ break;
8702
+ case "MOBILE_DEEPLINK_READY":
8703
+ track("wallet_deeplink_opened", { device });
8704
+ break;
8705
+ case "SET_SETUP_DEPOSIT_TOKEN":
8706
+ track("token_selected", {
8707
+ device,
8708
+ tokenSymbol: action.symbol,
8709
+ chainName: action.chainName
8710
+ });
8711
+ break;
8712
+ case "FINALIZE_AMOUNT": {
8713
+ const parsed = Number(state.amount);
8714
+ track("amount_entered", {
8715
+ device,
8716
+ amountUsd: Number.isFinite(parsed) ? parsed : void 0
8717
+ });
8718
+ break;
8719
+ }
8720
+ case "PAY_STARTED":
8721
+ track("deposit_clicked", { device });
8722
+ break;
8723
+ case "TRANSFER_CREATED":
8724
+ track("transfer_created", {
8725
+ device,
8726
+ chainFamily: sourceChain(action.transfer)
8727
+ });
8728
+ break;
8729
+ case "TRANSFER_SIGNED":
8730
+ track("transfer_signed", { device });
8731
+ break;
8732
+ case "TRANSFER_COMPLETED":
8733
+ track("transfer_completed", {
8734
+ device,
8735
+ amountUsd: amountUsd(action.transfer),
8736
+ chainFamily: sourceChain(action.transfer)
8737
+ });
8738
+ break;
8739
+ case "CONFIRM_SIGN_SUCCESS":
8740
+ track("confirm_sign_success", { device });
8741
+ break;
8742
+ case "MOBILE_SETUP_COMPLETE":
8743
+ track("wallet_setup_complete", { device });
8744
+ break;
8745
+ case "TRANSFER_FAILED":
8746
+ track("transfer_error", {
8747
+ device,
8748
+ type: "transfer_failed",
8749
+ stage: "processing",
8750
+ message: action.error
8751
+ });
8752
+ break;
8753
+ case "PROCESSING_TIMEOUT":
8754
+ track("transfer_error", {
8755
+ device,
8756
+ type: "processing_timeout",
8757
+ stage: "processing",
8758
+ message: action.error
8759
+ });
8760
+ break;
8761
+ case "SIGN_PAYLOAD_EXPIRED":
8762
+ track("transfer_error", {
8763
+ device,
8764
+ type: "sign_payload_expired",
8765
+ stage: "signing",
8766
+ message: action.error
8767
+ });
8768
+ break;
8769
+ case "PAY_ERROR":
8770
+ track("transfer_error", {
8771
+ device,
8772
+ type: "pay_error",
8773
+ stage: "creation",
8774
+ message: action.error
8775
+ });
8776
+ break;
8777
+ }
8778
+ }
8779
+ function useAnalyticsScreenView(phase, device) {
8780
+ const lastScreenRef = useRef(null);
8781
+ const screen = screenForPhase(phase);
8782
+ useEffect(() => {
8783
+ if (lastScreenRef.current === screen) return;
8784
+ lastScreenRef.current = screen;
8785
+ track("screen_viewed", { screen, phase: phase.step, device });
8786
+ }, [screen, phase.step, device]);
8787
+ }
8788
+ function usePostHogIdentify() {
8789
+ const { ready, authenticated, user } = usePrivy();
8790
+ const identifiedIdRef = useRef(null);
8791
+ useEffect(() => {
8792
+ if (!ready) return;
8793
+ if (authenticated && user?.id) {
8794
+ if (identifiedIdRef.current !== user.id) {
8795
+ identifyUser(user.id);
8796
+ identifiedIdRef.current = user.id;
8797
+ }
8798
+ return;
8799
+ }
8800
+ if (identifiedIdRef.current !== null) {
8801
+ resetUser();
8802
+ identifiedIdRef.current = null;
8803
+ }
8804
+ }, [ready, authenticated, user?.id]);
8805
+ }
8446
8806
 
8447
8807
  // src/setupDepositConfirmation.ts
8448
8808
  function deriveEffectiveSetupSource(setupDepositToken, setupSelectedSourceOption) {
@@ -9021,9 +9381,12 @@ function PrimaryButton({
9021
9381
  }
9022
9382
  );
9023
9383
  }
9384
+ var BUTTON_MIN_HEIGHT = 60;
9024
9385
  var progressButtonStyle = (tokens, paused) => ({
9025
9386
  position: "relative",
9026
9387
  width: "100%",
9388
+ minHeight: BUTTON_MIN_HEIGHT,
9389
+ boxSizing: "border-box",
9027
9390
  padding: "18px 24px",
9028
9391
  background: `linear-gradient(180deg, ${tokens.accent}88, ${tokens.accentHover}88)`,
9029
9392
  color: tokens.accentText,
@@ -9060,6 +9423,8 @@ var buttonStyle2 = (tokens, state) => {
9060
9423
  const gradient = `linear-gradient(180deg, ${tokens.accent}, ${tokens.accentHover})`;
9061
9424
  return {
9062
9425
  width: "100%",
9426
+ minHeight: BUTTON_MIN_HEIGHT,
9427
+ boxSizing: "border-box",
9063
9428
  padding: "18px 24px",
9064
9429
  // Layer a faint white film over the gradient on hover rather than using
9065
9430
  // `filter: brightness()` — the accent gradient is near-black in the new
@@ -9307,6 +9672,7 @@ var USDH_LOGO = svgToDataUri(USDH_SVG);
9307
9672
  var ETH_LOGO = "https://assets.relay.link/icons/currencies/eth.png";
9308
9673
  var SOL_LOGO = "https://assets.relay.link/icons/currencies/sol.png";
9309
9674
  var MON_LOGO = "https://assets.relay.link/icons/currencies/mon.png";
9675
+ var BNB_LOGO = "https://assets.relay.link/icons/currencies/bnb.png";
9310
9676
  var KNOWN_LOGOS = {
9311
9677
  metamask: METAMASK_LOGO,
9312
9678
  base: BASE_LOGO,
@@ -9325,7 +9691,8 @@ var TOKEN_LOGOS = {
9325
9691
  USDH: USDH_LOGO,
9326
9692
  ETH: ETH_LOGO,
9327
9693
  SOL: SOL_LOGO,
9328
- MON: MON_LOGO
9694
+ MON: MON_LOGO,
9695
+ BNB: BNB_LOGO
9329
9696
  };
9330
9697
  var CHAIN_LOGOS = {
9331
9698
  base: BASE_CHAIN_LOGO,
@@ -9984,6 +10351,8 @@ function SourcePill({
9984
10351
  logo,
9985
10352
  name,
9986
10353
  balance,
10354
+ nameSlot,
10355
+ balanceSlot,
9987
10356
  expanded,
9988
10357
  showChevron = true,
9989
10358
  icon
@@ -9991,8 +10360,8 @@ function SourcePill({
9991
10360
  const { tokens } = useBlinkConfig();
9992
10361
  return /* @__PURE__ */ jsxs("span", { style: pillStyle2(tokens.bgCardTranslucent), children: [
9993
10362
  icon ?? (logo ? /* @__PURE__ */ jsx("img", { src: logo, alt: "", "aria-hidden": "true", style: logoStyle }) : name ? /* @__PURE__ */ jsx("span", { style: logoFallbackStyle(tokens.bgInput, tokens.textMuted), children: name.charAt(0).toUpperCase() }) : null),
9994
- name && /* @__PURE__ */ jsx("span", { style: labelStyle2(tokens.text), children: name }),
9995
- balance && /* @__PURE__ */ jsx("span", { style: balanceStyle(tokens.text), children: balance }),
10363
+ nameSlot ?? (name && /* @__PURE__ */ jsx("span", { style: labelStyle2(tokens.text), children: name })),
10364
+ balanceSlot ?? (balance && /* @__PURE__ */ jsx("span", { style: balanceStyle(tokens.text), children: balance })),
9996
10365
  showChevron && /* @__PURE__ */ jsx("svg", { width: "17", height: "24", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: expanded ? /* @__PURE__ */ jsx(
9997
10366
  "path",
9998
10367
  {
@@ -10055,6 +10424,37 @@ var balanceStyle = (color) => ({
10055
10424
  color,
10056
10425
  whiteSpace: "nowrap"
10057
10426
  });
10427
+ var SHIMMER_KEYFRAMES = `
10428
+ @keyframes blink-shimmer-sweep {
10429
+ 0% { background-position: 200% 0; }
10430
+ 100% { background-position: -200% 0; }
10431
+ }`;
10432
+ function Shimmer({
10433
+ width,
10434
+ height,
10435
+ borderRadius = 999,
10436
+ baseColor,
10437
+ highlightColor,
10438
+ style
10439
+ }) {
10440
+ return /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", style: { display: "inline-block", ...style }, children: [
10441
+ /* @__PURE__ */ jsx("style", { children: SHIMMER_KEYFRAMES }),
10442
+ /* @__PURE__ */ jsx(
10443
+ "span",
10444
+ {
10445
+ style: {
10446
+ display: "block",
10447
+ width,
10448
+ height,
10449
+ borderRadius,
10450
+ background: `linear-gradient(90deg, ${baseColor} 0%, ${highlightColor} 50%, ${baseColor} 100%)`,
10451
+ backgroundSize: "200% 100%",
10452
+ animation: "blink-shimmer-sweep 1.4s ease-in-out infinite"
10453
+ }
10454
+ }
10455
+ )
10456
+ ] });
10457
+ }
10058
10458
  function formatUsdTwoDecimals(value) {
10059
10459
  const n = Number(value);
10060
10460
  return (Number.isFinite(n) ? n : 0).toLocaleString("en-US", {
@@ -10755,6 +11155,14 @@ function LockIcon({ size = 24 }) {
10755
11155
  /* @__PURE__ */ jsx("path", { d: "M8 11V8a4 4 0 1 1 8 0v3", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round" })
10756
11156
  ] });
10757
11157
  }
11158
+ function KeyIcon({ size = 24 }) {
11159
+ return /* @__PURE__ */ jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [
11160
+ /* @__PURE__ */ jsx("circle", { cx: "7", cy: "12", r: "3.6", stroke: "currentColor", strokeWidth: "2" }),
11161
+ /* @__PURE__ */ jsx("path", { d: "M10.6 12H20", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }),
11162
+ /* @__PURE__ */ jsx("path", { d: "M17 12v3.2", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }),
11163
+ /* @__PURE__ */ jsx("path", { d: "M20 12v3.2", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
11164
+ ] });
11165
+ }
10758
11166
  function FaceIdIcon({ size = 24 }) {
10759
11167
  return /* @__PURE__ */ jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [
10760
11168
  /* @__PURE__ */ jsx("path", { d: "M4 9V6a2 2 0 0 1 2-2h3", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round" }),
@@ -10768,6 +11176,21 @@ function FaceIdIcon({ size = 24 }) {
10768
11176
  /* @__PURE__ */ jsx("path", { d: "M9.5 16c.7.6 1.6 1 2.5 1s1.8-.4 2.5-1", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round" })
10769
11177
  ] });
10770
11178
  }
11179
+ function PencilIcon({ size = 18 } = {}) {
11180
+ return /* @__PURE__ */ jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [
11181
+ /* @__PURE__ */ jsx(
11182
+ "path",
11183
+ {
11184
+ d: "M4 20h4l10.5-10.5a2 2 0 0 0 0-2.83l-1.17-1.17a2 2 0 0 0-2.83 0L4 16v4Z",
11185
+ stroke: "currentColor",
11186
+ strokeWidth: "1.7",
11187
+ strokeLinecap: "round",
11188
+ strokeLinejoin: "round"
11189
+ }
11190
+ ),
11191
+ /* @__PURE__ */ jsx("path", { d: "M13.5 6.5l4 4", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round" })
11192
+ ] });
11193
+ }
10771
11194
  function WalletIcon() {
10772
11195
  return /* @__PURE__ */ jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [
10773
11196
  /* @__PURE__ */ jsx(
@@ -11139,7 +11562,7 @@ function CopyAddressButton({
11139
11562
  }) {
11140
11563
  const { tokens } = useBlinkConfig();
11141
11564
  const [hovered, setHovered] = useState(false);
11142
- const short = address.length > 18 ? `${address.slice(0, 8)}...${address.slice(-6)}` : address;
11565
+ const short = truncateMiddle(address);
11143
11566
  return /* @__PURE__ */ jsxs(
11144
11567
  "button",
11145
11568
  {
@@ -13099,7 +13522,7 @@ var lockBannerTextStyle = (color) => ({
13099
13522
  function ManualTransferPasskeyScreen({
13100
13523
  onCreatePasskey,
13101
13524
  onNoThanks,
13102
- amountUsd,
13525
+ amountUsd: amountUsd2,
13103
13526
  loading = false,
13104
13527
  error,
13105
13528
  onBack,
@@ -13297,7 +13720,6 @@ var errorBannerStyle6 = (tokens) => ({
13297
13720
  textAlign: "left",
13298
13721
  boxSizing: "border-box"
13299
13722
  });
13300
- var SHIMMER_ROWS = 3;
13301
13723
  function LinkTokensScreen({
13302
13724
  entries: entries2,
13303
13725
  selectedIndex,
@@ -13310,44 +13732,75 @@ function LinkTokensScreen({
13310
13732
  approving = false
13311
13733
  }) {
13312
13734
  const { tokens: t } = useBlinkConfig();
13735
+ const [view, setView] = useState("summary");
13736
+ const [pendingIndex, setPendingIndex] = useState(selectedIndex);
13737
+ const [limit, setLimit] = useState("unlimited");
13738
+ const [editing, setEditing] = useState(false);
13313
13739
  const showShimmer = loading && entries2.length === 0;
13314
13740
  const showEmpty = !loading && entries2.length === 0;
13315
13741
  const selected = entries2[selectedIndex];
13742
+ const limitUsd = limit === "unlimited" ? null : parseAmount(limit);
13743
+ const isUnlimited = limitUsd == null;
13744
+ const limitLabel = isUnlimited ? "Unlimited" : `$${limit}`;
13745
+ const currentSelection = () => limitUsd == null ? { unlimited: true } : { usd: limitUsd };
13316
13746
  const approveDisabled = loading || approving || entries2.length === 0 || selectedIndex < 0 || selectedIndex >= entries2.length || !!selected?.notSupported;
13317
- const ctaLabel = selected ? `Authorize ${selected.tokenSymbol} on ${shortChainName(selected.chainName)}` : "Approve";
13318
- return /* @__PURE__ */ jsxs(
13319
- ScreenLayout,
13320
- {
13321
- scrollableBody: false,
13322
- footer: /* @__PURE__ */ jsxs("div", { style: footerStackStyle4, children: [
13323
- /* @__PURE__ */ jsxs("div", { style: lockBannerStyle2, children: [
13324
- /* @__PURE__ */ jsx("span", { style: lockIconWrapStyle2(t.text), children: /* @__PURE__ */ jsx(LockIcon3, {}) }),
13325
- /* @__PURE__ */ jsx("p", { style: lockBannerTextStyle2(t.text), children: "Your passkey is required each time you deposit. Funds cannot move without your approval." })
13326
- ] }),
13327
- /* @__PURE__ */ jsx(PrimaryButton, { onClick: onApprove, disabled: approveDisabled, loading: approving, children: ctaLabel })
13328
- ] }),
13329
- children: [
13330
- /* @__PURE__ */ jsx("style", { children: `
13331
- @keyframes blink-link-tokens-shimmer {
13332
- 0% { background-position: 200% 0; }
13333
- 100% { background-position: -200% 0; }
13334
- }
13335
- .blink-link-tokens-list {
13336
- scrollbar-width: thin;
13337
- scrollbar-color: ${t.textTertiary} transparent;
13338
- }
13339
- .blink-link-tokens-list::-webkit-scrollbar {
13340
- width: 4px;
13341
- }
13342
- .blink-link-tokens-list::-webkit-scrollbar-track {
13343
- background: transparent;
13344
- margin: 8px 0;
13345
- }
13346
- .blink-link-tokens-list::-webkit-scrollbar-thumb {
13347
- background: ${t.textTertiary};
13348
- border-radius: 999px;
13349
- }
13350
- ` }),
13747
+ function openSelectToken() {
13748
+ setPendingIndex(selectedIndex);
13749
+ setView("selectToken");
13750
+ }
13751
+ function commitSelectToken() {
13752
+ const row = entries2[pendingIndex];
13753
+ if (row && !row.notSupported) onSelect(pendingIndex);
13754
+ setView("summary");
13755
+ }
13756
+ if (view === "selectToken") {
13757
+ const pending = entries2[pendingIndex];
13758
+ const continueDisabled = !pending || !!pending.notSupported || entries2.length === 0;
13759
+ return /* @__PURE__ */ jsxs(
13760
+ ScreenLayout,
13761
+ {
13762
+ scrollableBody: false,
13763
+ footer: /* @__PURE__ */ jsx(PrimaryButton, { onClick: commitSelectToken, disabled: continueDisabled, children: "Continue" }),
13764
+ children: [
13765
+ /* @__PURE__ */ jsx(ListScrollbarStyles, { textTertiary: t.textTertiary }),
13766
+ /* @__PURE__ */ jsx(
13767
+ ScreenHeader,
13768
+ {
13769
+ onBack: () => setView("summary"),
13770
+ center: /* @__PURE__ */ jsx("img", { src: BLINK_WORDMARK, alt: "Blink", style: wordmarkImgStyle3 })
13771
+ }
13772
+ ),
13773
+ /* @__PURE__ */ jsxs("div", { style: contentStyle10, children: [
13774
+ /* @__PURE__ */ jsx("h2", { style: headingStyle7(t.text, t.fontWeightBold), children: "Select token" }),
13775
+ /* @__PURE__ */ jsxs("div", { className: "blink-link-tokens-list", style: listCardStyle(t.bgRecessed), children: [
13776
+ entries2.map((entry, i) => /* @__PURE__ */ jsx(
13777
+ TokenSourceRow,
13778
+ {
13779
+ symbol: entry.tokenSymbol,
13780
+ chainName: entry.chainName,
13781
+ tokenLogoUri: entry.tokenLogoUri,
13782
+ balance: entry.balanceLabel == null ? entry.balanceUsd : void 0,
13783
+ balanceLabel: entry.balanceLabel,
13784
+ selected: i === pendingIndex,
13785
+ notSupported: entry.notSupported,
13786
+ onClick: () => setPendingIndex(i)
13787
+ },
13788
+ `${entry.tokenSymbol}-${entry.chainName}-${i}`
13789
+ )),
13790
+ showEmpty && /* @__PURE__ */ jsx("div", { style: emptyStyle(t.textMuted), children: "No tokens available to link. Switch wallets or fund this account to continue." })
13791
+ ] })
13792
+ ] })
13793
+ ]
13794
+ }
13795
+ );
13796
+ }
13797
+ return /* @__PURE__ */ jsxs(
13798
+ ScreenLayout,
13799
+ {
13800
+ scrollableBody: false,
13801
+ footer: /* @__PURE__ */ jsx(PrimaryButton, { onClick: () => onApprove(currentSelection()), disabled: approveDisabled, loading: approving, children: "Review" }),
13802
+ children: [
13803
+ /* @__PURE__ */ jsx(ListScrollbarStyles, { textTertiary: t.textTertiary }),
13351
13804
  /* @__PURE__ */ jsx(
13352
13805
  ScreenHeader,
13353
13806
  {
@@ -13357,71 +13810,198 @@ function LinkTokensScreen({
13357
13810
  }
13358
13811
  ),
13359
13812
  /* @__PURE__ */ jsxs("div", { style: contentStyle10, children: [
13360
- /* @__PURE__ */ jsx("h2", { style: headingStyle7(t.text), children: "Pick your stablecoin for passkey deposits" }),
13361
- /* @__PURE__ */ jsxs("div", { className: "blink-link-tokens-list", style: listCardStyle(t.bgRecessed), children: [
13362
- showShimmer ? Array.from({ length: SHIMMER_ROWS }).map((_, i) => /* @__PURE__ */ jsxs(
13363
- "div",
13364
- {
13365
- "data-testid": "link-tokens-shimmer-row",
13366
- "aria-hidden": "true",
13367
- style: shimmerRowStyle,
13368
- children: [
13369
- /* @__PURE__ */ jsx("div", { style: shimmerCircleStyle(t.bgInput, t.border) }),
13370
- /* @__PURE__ */ jsxs("div", { style: shimmerInfoStyle, children: [
13371
- /* @__PURE__ */ jsx("div", { style: shimmerLineStyle(72, t.bgInput, t.border) }),
13372
- /* @__PURE__ */ jsx("div", { style: shimmerLineStyle(48, t.bgInput, t.border) })
13373
- ] }),
13813
+ /* @__PURE__ */ jsx("div", { style: flexSpacerStyle }),
13814
+ /* @__PURE__ */ jsxs("div", { style: topGroupStyle, children: [
13815
+ /* @__PURE__ */ jsx("h2", { style: headingStyle7(t.text, t.fontWeightBold), children: "Let your passkey spend" }),
13816
+ /* @__PURE__ */ jsxs("div", { style: pillsGroupStyle, children: [
13817
+ showShimmer ? /* @__PURE__ */ jsxs("div", { "data-testid": "link-tokens-summary-shimmer", "aria-hidden": "true", style: pillStyle3(t.bgRecessed), children: [
13818
+ /* @__PURE__ */ jsx("div", { style: shimmerCircleStyle(t.bgInput, t.border) }),
13819
+ /* @__PURE__ */ jsxs("div", { style: shimmerInfoStyle, children: [
13820
+ /* @__PURE__ */ jsx("div", { style: shimmerLineStyle(96, t.bgInput, t.border) }),
13374
13821
  /* @__PURE__ */ jsx("div", { style: shimmerLineStyle(56, t.bgInput, t.border) })
13375
- ]
13376
- },
13377
- `shimmer-${i}`
13378
- )) : entries2.map((entry, i) => /* @__PURE__ */ jsx(
13379
- TokenSourceRow,
13380
- {
13381
- symbol: entry.tokenSymbol,
13382
- chainName: entry.chainName,
13383
- tokenLogoUri: entry.tokenLogoUri,
13384
- balance: entry.balanceLabel == null ? entry.balanceUsd : void 0,
13385
- balanceLabel: entry.balanceLabel,
13386
- selected: i === selectedIndex,
13387
- notSupported: entry.notSupported,
13388
- onClick: () => onSelect(i)
13389
- },
13390
- `${entry.tokenSymbol}-${entry.chainName}-${i}`
13391
- )),
13392
- showEmpty && /* @__PURE__ */ jsx("div", { style: emptyStyle(t.textMuted), children: "No tokens available to link. Switch wallets or fund this account to continue." })
13822
+ ] })
13823
+ ] }) : selected ? /* @__PURE__ */ jsxs(
13824
+ "button",
13825
+ {
13826
+ type: "button",
13827
+ onClick: openSelectToken,
13828
+ style: pillButtonStyle(t.bgRecessed),
13829
+ "aria-label": `Selected token ${selected.tokenSymbol} on ${selected.chainName}. Change token.`,
13830
+ "data-testid": "link-tokens-selected-pill",
13831
+ children: [
13832
+ /* @__PURE__ */ jsx(TokenPillLogo, { entry: selected, fallbackBg: t.bgRecessed, fallbackColor: t.textMuted }),
13833
+ /* @__PURE__ */ jsxs("span", { style: pillTextColStyle, children: [
13834
+ /* @__PURE__ */ jsxs("span", { style: pillTitleStyle(t.text, t.fontWeightRegular), children: [
13835
+ selected.tokenSymbol,
13836
+ " on ",
13837
+ selected.chainName
13838
+ ] }),
13839
+ /* @__PURE__ */ jsx("span", { style: pillSubStyle(t.textMuted, t.fontWeightRegular), children: selected.balanceLabel ?? `$${formatUsdTwoDecimals2(selected.balanceUsd)}` })
13840
+ ] }),
13841
+ /* @__PURE__ */ jsx("span", { style: pillChevronStyle(t.textMuted), children: /* @__PURE__ */ jsx(ChevronDownIcon, { color: t.textMuted }) })
13842
+ ]
13843
+ }
13844
+ ) : null,
13845
+ editing ? /* @__PURE__ */ jsxs("div", { style: pillStyle3(t.bgRecessed), children: [
13846
+ /* @__PURE__ */ jsx("span", { style: limitEditLabelStyle(t.text, t.fontWeightRegular), children: "Spending limit" }),
13847
+ /* @__PURE__ */ jsxs("div", { style: limitEditRightStyle, children: [
13848
+ /* @__PURE__ */ jsxs("span", { style: limitAmountStyle(t.text, t.fontWeightRegular), children: [
13849
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: limitDollarStyle(isUnlimited, t.text), children: "$" }),
13850
+ /* @__PURE__ */ jsx(
13851
+ "input",
13852
+ {
13853
+ type: "text",
13854
+ inputMode: "decimal",
13855
+ autoFocus: true,
13856
+ placeholder: "0",
13857
+ "aria-label": "Lifetime spending limit",
13858
+ value: limit === "unlimited" ? "" : limit,
13859
+ onChange: (e) => setLimit(sanitizeRawInput(e.target.value)),
13860
+ onBlur: () => setEditing(false),
13861
+ "data-testid": "link-tokens-limit-input",
13862
+ style: limitInputStyle(t.text, t.textMuted)
13863
+ }
13864
+ )
13865
+ ] }),
13866
+ /* @__PURE__ */ jsx(
13867
+ "button",
13868
+ {
13869
+ type: "button",
13870
+ onMouseDown: (e) => e.preventDefault(),
13871
+ onClick: () => {
13872
+ setLimit("unlimited");
13873
+ setEditing(false);
13874
+ },
13875
+ style: limitResetStyle(t.border, t.textMuted, t.fontWeightMedium),
13876
+ "data-testid": "link-tokens-limit-reset",
13877
+ children: "Unlimited"
13878
+ }
13879
+ )
13880
+ ] })
13881
+ ] }) : /* @__PURE__ */ jsxs(
13882
+ "button",
13883
+ {
13884
+ type: "button",
13885
+ onClick: () => setEditing(true),
13886
+ style: pillButtonStyle(t.bgRecessed),
13887
+ "aria-label": `Lifetime spending limit: ${limitLabel}. Edit.`,
13888
+ "data-testid": "link-tokens-limit-pill",
13889
+ children: [
13890
+ /* @__PURE__ */ jsx("span", { style: pillTitleStyle(t.text, t.fontWeightRegular), children: "Lifetime spending limit" }),
13891
+ /* @__PURE__ */ jsxs("span", { style: limitValueStyle, children: [
13892
+ /* @__PURE__ */ jsx("span", { style: pillSubStyle(isUnlimited ? t.textMuted : t.text, t.fontWeightRegular), children: limitLabel }),
13893
+ /* @__PURE__ */ jsx("span", { style: pillChevronStyle(t.textMuted), children: /* @__PURE__ */ jsx(PencilIcon, { size: 16 }) })
13894
+ ] })
13895
+ ]
13896
+ }
13897
+ )
13898
+ ] })
13393
13899
  ] }),
13394
- error && /* @__PURE__ */ jsx("div", { style: bannerSlotStyle, children: /* @__PURE__ */ jsx(NotificationBanner, { title: "Something went wrong", description: error }) })
13900
+ /* @__PURE__ */ jsx("div", { style: flexSpacerStyle }),
13901
+ /* @__PURE__ */ jsxs("div", { style: bottomGroupStyle, children: [
13902
+ /* @__PURE__ */ jsxs("div", { style: infoCardStyle3(t.bgRecessed), children: [
13903
+ /* @__PURE__ */ jsx(
13904
+ InfoRow,
13905
+ {
13906
+ icon: /* @__PURE__ */ jsx(LockIcon, { size: 22 }),
13907
+ title: "Blink is non-custodial",
13908
+ body: "Your passkey moves money, not Blink.",
13909
+ t
13910
+ }
13911
+ ),
13912
+ /* @__PURE__ */ jsx(
13913
+ InfoRow,
13914
+ {
13915
+ icon: /* @__PURE__ */ jsx(FaceIdIcon, { size: 22 }),
13916
+ title: "Every transaction is signed",
13917
+ body: "Nothing can move without your direct approval.",
13918
+ t
13919
+ }
13920
+ )
13921
+ ] }),
13922
+ error && /* @__PURE__ */ jsx("div", { style: bannerSlotStyle, children: /* @__PURE__ */ jsx(NotificationBanner, { title: "Something went wrong", description: error }) })
13923
+ ] })
13395
13924
  ] })
13396
13925
  ]
13397
13926
  }
13398
13927
  );
13399
13928
  }
13400
- function shortChainName(chainName) {
13401
- return chainName.replace(/\s+One$/i, "");
13929
+ function TokenPillLogo({
13930
+ entry,
13931
+ fallbackBg,
13932
+ fallbackColor
13933
+ }) {
13934
+ const logo = entry.tokenLogoUri ?? TOKEN_LOGOS[entry.tokenSymbol];
13935
+ return /* @__PURE__ */ jsx("span", { style: pillLogoSlotStyle, children: logo ? /* @__PURE__ */ jsx("img", { src: logo, alt: "", "aria-hidden": "true", style: pillLogoImgStyle }) : /* @__PURE__ */ jsx("span", { style: pillLogoFallbackStyle(fallbackBg, fallbackColor), children: entry.tokenSymbol.charAt(0).toUpperCase() }) });
13402
13936
  }
13403
- function LockIcon3() {
13404
- return /* @__PURE__ */ jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
13405
- "path",
13406
- {
13407
- d: "M7 10V8a5 5 0 0 1 10 0v2m-12 0h14a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1z",
13408
- stroke: "currentColor",
13409
- strokeWidth: "2",
13410
- strokeLinecap: "round",
13411
- strokeLinejoin: "round"
13412
- }
13413
- ) });
13937
+ function InfoRow({
13938
+ icon,
13939
+ title,
13940
+ body,
13941
+ t
13942
+ }) {
13943
+ return /* @__PURE__ */ jsxs("div", { style: infoRowStyle2, children: [
13944
+ /* @__PURE__ */ jsx("span", { style: infoIconStyle(t.text), children: icon }),
13945
+ /* @__PURE__ */ jsxs("span", { style: infoTextColStyle, children: [
13946
+ /* @__PURE__ */ jsx("span", { style: infoTitleStyle2(t.text, t.fontWeightSemibold), children: title }),
13947
+ /* @__PURE__ */ jsx("span", { style: infoBodyStyle(t.textMuted, t.fontWeightMedium), children: body })
13948
+ ] })
13949
+ ] });
13950
+ }
13951
+ function ListScrollbarStyles({ textTertiary }) {
13952
+ return /* @__PURE__ */ jsx("style", { children: `
13953
+ @keyframes blink-link-tokens-shimmer {
13954
+ 0% { background-position: 200% 0; }
13955
+ 100% { background-position: -200% 0; }
13956
+ }
13957
+ .blink-link-tokens-list {
13958
+ scrollbar-width: thin;
13959
+ scrollbar-color: ${textTertiary} transparent;
13960
+ }
13961
+ .blink-link-tokens-list::-webkit-scrollbar { width: 4px; }
13962
+ .blink-link-tokens-list::-webkit-scrollbar-track {
13963
+ background: transparent;
13964
+ margin: 8px 0;
13965
+ }
13966
+ .blink-link-tokens-list::-webkit-scrollbar-thumb {
13967
+ background: ${textTertiary};
13968
+ border-radius: 999px;
13969
+ }
13970
+ input[data-testid="link-tokens-limit-input"]::placeholder {
13971
+ color: var(--link-limit-placeholder);
13972
+ opacity: 1;
13973
+ }
13974
+ ` });
13414
13975
  }
13415
13976
  var contentStyle10 = {
13416
13977
  display: "flex",
13417
13978
  flexDirection: "column",
13418
- alignItems: "center",
13419
- gap: 16,
13420
- paddingTop: 8,
13421
13979
  flex: 1,
13422
13980
  minHeight: 0,
13423
13981
  width: "100%"
13424
13982
  };
13983
+ var flexSpacerStyle = {
13984
+ flex: 1,
13985
+ minHeight: 16
13986
+ };
13987
+ var topGroupStyle = {
13988
+ display: "flex",
13989
+ flexDirection: "column",
13990
+ gap: 24,
13991
+ width: "100%"
13992
+ };
13993
+ var bottomGroupStyle = {
13994
+ display: "flex",
13995
+ flexDirection: "column",
13996
+ gap: 12,
13997
+ width: "100%"
13998
+ };
13999
+ var pillsGroupStyle = {
14000
+ display: "flex",
14001
+ flexDirection: "column",
14002
+ gap: 8,
14003
+ width: "100%"
14004
+ };
13425
14005
  var wordmarkImgStyle3 = {
13426
14006
  height: 22,
13427
14007
  width: "auto",
@@ -13429,9 +14009,9 @@ var wordmarkImgStyle3 = {
13429
14009
  pointerEvents: "none",
13430
14010
  userSelect: "none"
13431
14011
  };
13432
- var headingStyle7 = (color) => ({
14012
+ var headingStyle7 = (color, weight) => ({
13433
14013
  fontSize: 24,
13434
- fontWeight: 700,
14014
+ fontWeight: weight,
13435
14015
  lineHeight: "normal",
13436
14016
  letterSpacing: 0,
13437
14017
  color,
@@ -13451,68 +14031,155 @@ var listCardStyle = (bg) => ({
13451
14031
  minHeight: 0,
13452
14032
  overflowY: "auto"
13453
14033
  });
13454
- var shimmerRowStyle = {
14034
+ var pillStyle3 = (bg) => ({
13455
14035
  display: "flex",
13456
14036
  alignItems: "center",
13457
14037
  gap: 16,
13458
- padding: "12px 16px 12px 12px",
13459
- background: "transparent",
13460
- border: "none",
13461
- borderRadius: 16,
13462
14038
  width: "100%",
13463
- boxSizing: "border-box"
14039
+ boxSizing: "border-box",
14040
+ padding: "14px 16px",
14041
+ borderRadius: 20,
14042
+ background: bg
14043
+ });
14044
+ var pillButtonStyle = (bg) => ({
14045
+ ...pillStyle3(bg),
14046
+ border: "none",
14047
+ cursor: "pointer",
14048
+ fontFamily: "inherit",
14049
+ textAlign: "left"
14050
+ });
14051
+ var pillTextColStyle = {
14052
+ display: "flex",
14053
+ flexDirection: "column",
14054
+ flex: 1,
14055
+ minWidth: 0,
14056
+ gap: 2
13464
14057
  };
13465
- var shimmerCircleStyle = (baseColor, highlightColor) => ({
14058
+ var pillTitleStyle = (color, weight) => ({
14059
+ fontSize: "1rem",
14060
+ fontWeight: weight,
14061
+ lineHeight: "normal",
14062
+ color,
14063
+ whiteSpace: "nowrap",
14064
+ overflow: "hidden",
14065
+ textOverflow: "ellipsis"
14066
+ });
14067
+ var pillSubStyle = (color, weight) => ({
14068
+ fontSize: "0.95rem",
14069
+ fontWeight: weight,
14070
+ color
14071
+ });
14072
+ var pillChevronStyle = (color) => ({
14073
+ flexShrink: 0,
14074
+ display: "inline-flex",
14075
+ alignItems: "center",
14076
+ color
14077
+ });
14078
+ var limitValueStyle = {
14079
+ marginLeft: "auto",
14080
+ display: "inline-flex",
14081
+ alignItems: "center",
14082
+ gap: 8
14083
+ };
14084
+ var pillLogoSlotStyle = {
14085
+ position: "relative",
13466
14086
  width: 40,
13467
14087
  height: 40,
13468
- borderRadius: "50%",
13469
14088
  flexShrink: 0,
13470
- background: `linear-gradient(90deg, ${baseColor} 0%, ${highlightColor} 50%, ${baseColor} 100%)`,
13471
- backgroundSize: "200% 100%",
13472
- animation: "blink-link-tokens-shimmer 1.4s ease-in-out infinite"
14089
+ display: "inline-flex",
14090
+ alignItems: "center",
14091
+ justifyContent: "center"
14092
+ };
14093
+ var pillLogoImgStyle = {
14094
+ width: "100%",
14095
+ height: "100%",
14096
+ borderRadius: "50%",
14097
+ objectFit: "cover"
14098
+ };
14099
+ var pillLogoFallbackStyle = (bg, color) => ({
14100
+ width: "100%",
14101
+ height: "100%",
14102
+ borderRadius: "50%",
14103
+ background: bg,
14104
+ color,
14105
+ display: "inline-flex",
14106
+ alignItems: "center",
14107
+ justifyContent: "center",
14108
+ fontSize: "0.95rem",
14109
+ fontWeight: 700
13473
14110
  });
13474
- var shimmerInfoStyle = {
14111
+ var limitEditLabelStyle = (color, weight) => ({
14112
+ fontSize: "1rem",
14113
+ fontWeight: weight,
14114
+ lineHeight: "normal",
14115
+ color,
14116
+ whiteSpace: "nowrap",
14117
+ flexShrink: 0
14118
+ });
14119
+ var limitEditRightStyle = {
14120
+ marginLeft: "auto",
13475
14121
  display: "flex",
13476
- flexDirection: "column",
13477
- gap: 6,
13478
- flex: 1,
14122
+ alignItems: "center",
14123
+ gap: 8,
13479
14124
  minWidth: 0
13480
14125
  };
13481
- var shimmerLineStyle = (width, baseColor, highlightColor) => ({
13482
- width,
13483
- height: 12,
13484
- borderRadius: 6,
13485
- background: `linear-gradient(90deg, ${baseColor} 0%, ${highlightColor} 50%, ${baseColor} 100%)`,
13486
- backgroundSize: "200% 100%",
13487
- animation: "blink-link-tokens-shimmer 1.4s ease-in-out infinite"
14126
+ var limitAmountStyle = (color, weight) => ({
14127
+ display: "inline-flex",
14128
+ alignItems: "baseline",
14129
+ gap: 1,
14130
+ color,
14131
+ fontSize: "1rem",
14132
+ fontWeight: weight
13488
14133
  });
13489
- var emptyStyle = (color) => ({
13490
- padding: "32px 16px",
13491
- textAlign: "center",
14134
+ var limitDollarStyle = (isZero, color) => ({
14135
+ opacity: isZero ? 0.5 : 1,
14136
+ color
14137
+ });
14138
+ var limitInputStyle = (color, mutedColor) => ({
14139
+ background: "transparent",
14140
+ border: "none",
14141
+ outline: "none",
13492
14142
  color,
13493
- fontSize: "0.92rem"
14143
+ font: "inherit",
14144
+ textAlign: "right",
14145
+ padding: 0,
14146
+ margin: 0,
14147
+ width: "auto",
14148
+ minWidth: "1ch",
14149
+ ...{ fieldSizing: "content" },
14150
+ fontVariantNumeric: "tabular-nums",
14151
+ caretColor: color,
14152
+ ["--link-limit-placeholder"]: mutedColor
13494
14153
  });
13495
- var bannerSlotStyle = {
13496
- display: "flex",
13497
- justifyContent: "center",
13498
- width: "100%"
13499
- };
13500
- var footerStackStyle4 = {
14154
+ var limitResetStyle = (borderColor, color, weight) => ({
14155
+ flexShrink: 0,
14156
+ padding: "5px 12px",
14157
+ borderRadius: 999,
14158
+ fontSize: "0.8rem",
14159
+ fontWeight: weight,
14160
+ fontFamily: "inherit",
14161
+ cursor: "pointer",
14162
+ border: `1.5px solid ${borderColor}`,
14163
+ background: "transparent",
14164
+ color
14165
+ });
14166
+ var infoCardStyle3 = (bg) => ({
13501
14167
  display: "flex",
13502
14168
  flexDirection: "column",
13503
14169
  gap: 16,
13504
- width: "100%"
13505
- };
13506
- var lockBannerStyle2 = {
14170
+ width: "100%",
14171
+ boxSizing: "border-box",
14172
+ padding: 16,
14173
+ borderRadius: 20,
14174
+ background: bg
14175
+ });
14176
+ var infoRowStyle2 = {
13507
14177
  display: "flex",
13508
14178
  alignItems: "flex-start",
13509
- gap: 16,
13510
- padding: "12px 16px 12px 12px",
13511
- borderRadius: 16,
13512
- width: "100%",
13513
- boxSizing: "border-box"
14179
+ gap: 14,
14180
+ width: "100%"
13514
14181
  };
13515
- var lockIconWrapStyle2 = (color) => ({
14182
+ var infoIconStyle = (color) => ({
13516
14183
  flexShrink: 0,
13517
14184
  width: 24,
13518
14185
  height: 24,
@@ -13521,14 +14188,60 @@ var lockIconWrapStyle2 = (color) => ({
13521
14188
  justifyContent: "center",
13522
14189
  color
13523
14190
  });
13524
- var lockBannerTextStyle2 = (color) => ({
13525
- margin: 0,
13526
- flex: 1,
13527
- fontSize: 12,
13528
- fontWeight: 500,
13529
- lineHeight: "normal",
13530
- color
14191
+ var infoTextColStyle = {
14192
+ display: "flex",
14193
+ flexDirection: "column",
14194
+ gap: 2,
14195
+ flex: 1,
14196
+ minWidth: 0
14197
+ };
14198
+ var infoTitleStyle2 = (color, weight) => ({
14199
+ fontSize: "1rem",
14200
+ fontWeight: weight,
14201
+ lineHeight: "normal",
14202
+ color
14203
+ });
14204
+ var infoBodyStyle = (color, weight) => ({
14205
+ fontSize: "0.85rem",
14206
+ fontWeight: weight,
14207
+ lineHeight: 1.4,
14208
+ color
14209
+ });
14210
+ var shimmerCircleStyle = (baseColor, highlightColor) => ({
14211
+ width: 40,
14212
+ height: 40,
14213
+ borderRadius: "50%",
14214
+ flexShrink: 0,
14215
+ background: `linear-gradient(90deg, ${baseColor} 0%, ${highlightColor} 50%, ${baseColor} 100%)`,
14216
+ backgroundSize: "200% 100%",
14217
+ animation: "blink-link-tokens-shimmer 1.4s ease-in-out infinite"
14218
+ });
14219
+ var shimmerInfoStyle = {
14220
+ display: "flex",
14221
+ flexDirection: "column",
14222
+ gap: 6,
14223
+ flex: 1,
14224
+ minWidth: 0
14225
+ };
14226
+ var shimmerLineStyle = (width, baseColor, highlightColor) => ({
14227
+ width,
14228
+ height: 12,
14229
+ borderRadius: 6,
14230
+ background: `linear-gradient(90deg, ${baseColor} 0%, ${highlightColor} 50%, ${baseColor} 100%)`,
14231
+ backgroundSize: "200% 100%",
14232
+ animation: "blink-link-tokens-shimmer 1.4s ease-in-out infinite"
14233
+ });
14234
+ var emptyStyle = (color) => ({
14235
+ padding: "32px 16px",
14236
+ textAlign: "center",
14237
+ color,
14238
+ fontSize: "0.92rem"
13531
14239
  });
14240
+ var bannerSlotStyle = {
14241
+ display: "flex",
14242
+ justifyContent: "center",
14243
+ width: "100%"
14244
+ };
13532
14245
  function DepositCompleteScreen({ amount }) {
13533
14246
  const { tokens } = useBlinkConfig();
13534
14247
  return /* @__PURE__ */ jsxs(ScreenLayout, { children: [
@@ -13590,6 +14303,7 @@ function SelectDepositSourceScreen({
13590
14303
  tokenOptions,
13591
14304
  selectedTokenSymbol,
13592
14305
  selectedChainName,
14306
+ balancesLoading,
13593
14307
  selectedWalletId,
13594
14308
  onPickToken,
13595
14309
  useDeeplink,
@@ -13606,8 +14320,9 @@ function SelectDepositSourceScreen({
13606
14320
  const [pendingMobileSelection, setPendingMobileSelection] = useState(null);
13607
14321
  const [preparedAuthorization, setPreparedAuthorization] = useState(null);
13608
14322
  const [preparingAuthorization, setPreparingAuthorization] = useState(false);
14323
+ const isAuthorizedOption = (opt) => !opt.status || opt.status === "AUTHORIZED";
13609
14324
  const selectableOptions = tokenOptions.filter(
13610
- (opt) => opt.balance == null || isSelectableDepositSourceAmountUsd(opt.balance, minTransferAmountUsd)
14325
+ (opt) => isAuthorizedOption(opt) || opt.balance == null || isSelectableDepositSourceAmountUsd(opt.balance, minTransferAmountUsd)
13611
14326
  );
13612
14327
  const authorized = selectableOptions.filter(
13613
14328
  (opt) => !opt.status || opt.status === "AUTHORIZED"
@@ -13761,6 +14476,30 @@ function SelectDepositSourceScreen({
13761
14476
  "data-testid": "select-deposit-source-scroll-content",
13762
14477
  style: contentStyle12,
13763
14478
  children: [
14479
+ balancesLoading && tokenOptions.length === 0 && /* @__PURE__ */ jsx("div", { style: accountSectionStyle, "data-testid": "select-source-shimmer", children: /* @__PURE__ */ jsx("div", { style: groupCardStyle(tokens.bgRecessed), children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxs(
14480
+ "div",
14481
+ {
14482
+ style: { display: "flex", alignItems: "center", gap: 12, padding: 12 },
14483
+ children: [
14484
+ /* @__PURE__ */ jsx(
14485
+ Shimmer,
14486
+ {
14487
+ width: 36,
14488
+ height: 36,
14489
+ borderRadius: "50%",
14490
+ baseColor: tokens.bgInput,
14491
+ highlightColor: tokens.border
14492
+ }
14493
+ ),
14494
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6, flex: 1 }, children: [
14495
+ /* @__PURE__ */ jsx(Shimmer, { width: 96, height: 12, baseColor: tokens.bgInput, highlightColor: tokens.border }),
14496
+ /* @__PURE__ */ jsx(Shimmer, { width: 56, height: 10, baseColor: tokens.bgInput, highlightColor: tokens.border })
14497
+ ] }),
14498
+ /* @__PURE__ */ jsx(Shimmer, { width: 48, height: 12, baseColor: tokens.bgInput, highlightColor: tokens.border })
14499
+ ]
14500
+ },
14501
+ `shimmer-row-${i}`
14502
+ )) }) }),
13764
14503
  orderedAccounts.map((account) => {
13765
14504
  const authRows = authorized.filter((opt) => rowMatchesSection(opt, account));
13766
14505
  const reqRows = requiresAuth.filter((opt) => rowMatchesSection(opt, account)).sort((a, b) => Number(!!a.notSupported) - Number(!!b.notSupported));
@@ -13975,6 +14714,7 @@ function ShimmerBlock({
13975
14714
  );
13976
14715
  }
13977
14716
  function DepositScreen({
14717
+ balancesLoading,
13978
14718
  availableBalance,
13979
14719
  remainingLimit,
13980
14720
  initialAmount,
@@ -14028,6 +14768,10 @@ function DepositScreen({
14028
14768
  const showMobileKeypad = isEntryMode && !isDesktop;
14029
14769
  const [keypadOpen, setKeypadOpen] = useState(showMobileKeypad);
14030
14770
  const [tokenPickerOpen, setTokenPickerOpen] = useState(false);
14771
+ const hasLoadedQuoteRef = useRef(false);
14772
+ if (!quoteLoading) {
14773
+ hasLoadedQuoteRef.current = true;
14774
+ }
14031
14775
  const selectableTokenOptions = tokenOptions?.filter(
14032
14776
  (opt) => opt.balance == null || isSelectableDepositSourceAmountUsd(opt.balance, minDepositFloor)
14033
14777
  ) ?? [];
@@ -14045,11 +14789,11 @@ function DepositScreen({
14045
14789
  }, []);
14046
14790
  const selectedAccount = accounts?.find((a) => a.id === selectedAccountId);
14047
14791
  const selectedProviderName = selectedAccount?.name ?? "Wallet";
14048
- const isLowBalance = availableBalance < minDepositFloor;
14049
- const insufficientFunds = availableBalance < validationAmount;
14792
+ const isLowBalance = !balancesLoading && availableBalance < minDepositFloor;
14793
+ const insufficientFunds = !balancesLoading && availableBalance < validationAmount;
14050
14794
  const needsAuthorization = selectedTokenStatus != null && selectedTokenStatus !== "AUTHORIZED" && !insufficientFunds && !isLowBalance;
14051
14795
  const exceedsLimit = remainingLimit != null && validationAmount > remainingLimit && !isLowBalance && !needsAuthorization;
14052
- const canDeposit = validationAmount >= minDepositFloor && !exceedsLimit && !isLowBalance && !insufficientFunds && !needsAuthorization && !processing;
14796
+ const canDeposit = validationAmount >= minDepositFloor && !balancesLoading && !exceedsLimit && !isLowBalance && !insufficientFunds && !needsAuthorization && !processing;
14053
14797
  const hasAccountPill = !!accounts && accounts.length > 0;
14054
14798
  const pillClickable = canOpenInlineSheet || !!onSelectToken;
14055
14799
  const formattedBalance = selectedTokenSymbol != null ? `$${formatUsdTwoDecimals2(availableBalance)}` : void 0;
@@ -14072,6 +14816,7 @@ function DepositScreen({
14072
14816
  accounts: depositSourceAccounts,
14073
14817
  selectedAccountId: selectedAccountId ?? null,
14074
14818
  tokenOptions: selectableTokenOptions,
14819
+ balancesLoading,
14075
14820
  selectedTokenSymbol,
14076
14821
  selectedChainName,
14077
14822
  selectedWalletId: selectedWalletId ?? null,
@@ -14093,7 +14838,7 @@ function DepositScreen({
14093
14838
  const mobileEntryWithKeypad = isEntryMode && !isDesktop && keypadOpen;
14094
14839
  const showLiveMobileHero = isEntryMode && !isDesktop && keypadOpen;
14095
14840
  const showDesktopInputHero = isEntryMode && isDesktop;
14096
- const showFormattedHero = !showLiveMobileHero && !showDesktopInputHero;
14841
+ const showFeeRow = validationAmount > 0;
14097
14842
  let primaryButton;
14098
14843
  if (mobileEntryWithKeypad) {
14099
14844
  const canConfirmEntry = canContinue(amountEntryValue ?? "");
@@ -14223,7 +14968,32 @@ function DepositScreen({
14223
14968
  "aria-label": tokenAriaLabel,
14224
14969
  "aria-expanded": canOpenInlineSheet ? tokenPickerOpen : void 0,
14225
14970
  "aria-haspopup": canOpenInlineSheet ? "listbox" : void 0,
14226
- children: /* @__PURE__ */ jsx(
14971
+ children: balancesLoading ? (
14972
+ // Until balances merge in we don't know the selected token or its
14973
+ // balance, so the whole pill (icon + name + balance) is a shimmer
14974
+ // placeholder rather than showing the account name as a default.
14975
+ // Render through SourcePill so the placeholder inherits the exact
14976
+ // same container (fill, radius, padding, chevron) as the loaded
14977
+ // pill and does not change size when balances arrive.
14978
+ /* @__PURE__ */ jsx(
14979
+ "span",
14980
+ {
14981
+ "data-testid": "deposit-pill-shimmer",
14982
+ "aria-label": "Loading balance",
14983
+ "aria-busy": "true",
14984
+ style: { display: "inline-flex" },
14985
+ children: /* @__PURE__ */ jsx(
14986
+ SourcePill,
14987
+ {
14988
+ icon: /* @__PURE__ */ jsx(Shimmer, { width: 28, height: 28, borderRadius: "50%", baseColor: tokens.bgInput, highlightColor: tokens.border }),
14989
+ nameSlot: /* @__PURE__ */ jsx(Shimmer, { width: 72, height: 12, baseColor: tokens.bgInput, highlightColor: tokens.border }),
14990
+ balanceSlot: /* @__PURE__ */ jsx(Shimmer, { width: 48, height: 12, baseColor: tokens.bgInput, highlightColor: tokens.border }),
14991
+ showChevron: pillClickable
14992
+ }
14993
+ )
14994
+ }
14995
+ )
14996
+ ) : /* @__PURE__ */ jsx(
14227
14997
  SourcePill,
14228
14998
  {
14229
14999
  icon: /* @__PURE__ */ jsx(
@@ -14243,7 +15013,7 @@ function DepositScreen({
14243
15013
  )
14244
15014
  }
14245
15015
  ),
14246
- showFormattedHero && /* @__PURE__ */ jsx("div", { style: redesignFeeRowStyle, children: quoteLoading ? /* @__PURE__ */ jsx(
15016
+ /* @__PURE__ */ jsx("div", { "data-testid": "deposit-fee-row", style: redesignFeeRowStyle, children: showFeeRow && (quoteLoading && !hasLoadedQuoteRef.current ? /* @__PURE__ */ jsx(
14247
15017
  "div",
14248
15018
  {
14249
15019
  "data-testid": "deposit-fee-shimmer",
@@ -14261,7 +15031,7 @@ function DepositScreen({
14261
15031
  }
14262
15032
  )
14263
15033
  }
14264
- ) : /* @__PURE__ */ jsx("span", { style: redesignNoFeesStyle(tokens.text), children: "No fees" }) })
15034
+ ) : /* @__PURE__ */ jsx("span", { style: redesignNoFeesStyle(tokens.text), children: "No fees" })) })
14265
15035
  ] }),
14266
15036
  /* @__PURE__ */ jsx("div", { style: bannerSlotStyle2, children: mobileEntryWithKeypad ? null : showLowFunds ? /* @__PURE__ */ jsx(
14267
15037
  NotificationBanner,
@@ -15638,7 +16408,7 @@ function OpenWalletScreen({
15638
16408
  return /* @__PURE__ */ jsxs(
15639
16409
  ScreenLayout,
15640
16410
  {
15641
- footer: /* @__PURE__ */ jsxs("div", { style: footerStackStyle5, children: [
16411
+ footer: /* @__PURE__ */ jsxs("div", { style: footerStackStyle4, children: [
15642
16412
  error && /* @__PURE__ */ jsx(InfoBanner, { children: error }),
15643
16413
  /* @__PURE__ */ jsxs(
15644
16414
  SecondaryButton,
@@ -15685,7 +16455,7 @@ function OpenWalletScreen({
15685
16455
  return /* @__PURE__ */ jsxs(
15686
16456
  ScreenLayout,
15687
16457
  {
15688
- footer: /* @__PURE__ */ jsxs("div", { style: footerStackStyle5, children: [
16458
+ footer: /* @__PURE__ */ jsxs("div", { style: footerStackStyle4, children: [
15689
16459
  error && /* @__PURE__ */ jsx(InfoBanner, { children: error }),
15690
16460
  /* @__PURE__ */ jsxs(
15691
16461
  SecondaryButton,
@@ -15769,7 +16539,7 @@ var primaryClusterStyle = {
15769
16539
  textAlign: "center",
15770
16540
  gap: 12
15771
16541
  };
15772
- var footerStackStyle5 = {
16542
+ var footerStackStyle4 = {
15773
16543
  display: "flex",
15774
16544
  flexDirection: "column",
15775
16545
  gap: 8
@@ -15897,6 +16667,12 @@ function ApprovingInWalletScreen({
15897
16667
  chainName,
15898
16668
  step,
15899
16669
  complete,
16670
+ signing,
16671
+ spendingLimitLabel,
16672
+ destinationAddress,
16673
+ tokenLogoUri,
16674
+ awaitingApproval,
16675
+ onApprove,
15900
16676
  error,
15901
16677
  onRetry,
15902
16678
  onBack,
@@ -15906,10 +16682,11 @@ function ApprovingInWalletScreen({
15906
16682
  onOpenWallet
15907
16683
  }) {
15908
16684
  const { tokens: t } = useBlinkConfig();
15909
- const showDelayedRetry = useDelayedRetry(error == null && onRetry != null);
16685
+ const showDelayedRetry = useDelayedRetry(!awaitingApproval && error == null && onRetry != null);
15910
16686
  const retryVisible = onRetry != null && (error != null || showDelayedRetry);
15911
16687
  const token = tokenSymbol ?? "token";
15912
- const steps = buildSteps(step, tokenSymbol, chainName, complete);
16688
+ const chain = chainName ?? "chain";
16689
+ const steps = buildSteps(step, tokenSymbol, complete, signing);
15913
16690
  const foregroundNavigation = foregroundDeeplink ? classifyWalletDeeplinkNavigation(foregroundDeeplink) : null;
15914
16691
  const foregroundWithJs = foregroundNavigation ? shouldOpenWithJavaScript(foregroundNavigation) : false;
15915
16692
  const showOpenWalletButton = !!foregroundDeeplink && onOpenWallet != null;
@@ -15917,11 +16694,7 @@ function ApprovingInWalletScreen({
15917
16694
  ScreenLayout,
15918
16695
  {
15919
16696
  scrollableBody: false,
15920
- footer: /* @__PURE__ */ jsxs("div", { style: footerStackStyle6, children: [
15921
- /* @__PURE__ */ jsxs("div", { style: lockBannerStyle3, children: [
15922
- /* @__PURE__ */ jsx("span", { style: lockIconWrapStyle3(t.text), children: /* @__PURE__ */ jsx(LockIcon4, {}) }),
15923
- /* @__PURE__ */ jsx("p", { style: lockBannerTextStyle3(t.text), children: "Your passkey is required each time you deposit. Funds cannot move without your approval." })
15924
- ] }),
16697
+ footer: /* @__PURE__ */ jsxs("div", { style: footerStackStyle5, children: [
15925
16698
  showOpenWalletButton && /* @__PURE__ */ jsxs(
15926
16699
  SecondaryButton,
15927
16700
  {
@@ -15938,7 +16711,7 @@ function ApprovingInWalletScreen({
15938
16711
  retryVisible ? /* @__PURE__ */ jsxs("div", { style: retryStackStyle, children: [
15939
16712
  error && /* @__PURE__ */ jsx(InfoBanner, { children: error }),
15940
16713
  /* @__PURE__ */ jsx(OutlineButton, { onClick: onRetry, children: "Retry" })
15941
- ] }) : /* @__PURE__ */ jsx(PrimaryButton, { spinnerOnly: true })
16714
+ ] }) : awaitingApproval ? /* @__PURE__ */ jsx(PrimaryButton, { onClick: onApprove, children: "Approve" }) : /* @__PURE__ */ jsx(PrimaryButton, { spinnerOnly: true })
15942
16715
  ] }),
15943
16716
  children: [
15944
16717
  /* @__PURE__ */ jsx(
@@ -15950,7 +16723,39 @@ function ApprovingInWalletScreen({
15950
16723
  }
15951
16724
  ),
15952
16725
  /* @__PURE__ */ jsxs("div", { style: contentStyle16, children: [
15953
- /* @__PURE__ */ jsx("h2", { style: headingStyle13(t.text), children: `Authorizing ${token} for passkey deposits` }),
16726
+ /* @__PURE__ */ jsx("h2", { style: headingStyle13(t.text, t.fontWeightBold), children: "Approve in wallet" }),
16727
+ /* @__PURE__ */ jsxs("div", { style: transferCardStyle(t.bgCardTranslucent, t.radiusLg), children: [
16728
+ /* @__PURE__ */ jsx(
16729
+ AddressRow,
16730
+ {
16731
+ circle: /* @__PURE__ */ jsx(
16732
+ LogoCircle,
16733
+ {
16734
+ src: tokenLogoUri ?? TOKEN_LOGOS[token] ?? void 0,
16735
+ fallback: token.slice(0, 1).toUpperCase(),
16736
+ size: 40
16737
+ }
16738
+ ),
16739
+ title: `${token} on ${chain}`,
16740
+ subtitle: spendingLimitLabel
16741
+ }
16742
+ ),
16743
+ /* @__PURE__ */ jsx("div", { style: arrowRowStyle, children: /* @__PURE__ */ jsx(ArrowDownIcon, { color: t.textMuted }) }),
16744
+ /* @__PURE__ */ jsx(
16745
+ AddressRow,
16746
+ {
16747
+ circle: /* @__PURE__ */ jsx("span", { style: passkeyCircleStyle(t.accent, t.highlight), children: /* @__PURE__ */ jsx(KeyIcon, { size: 22 }) }),
16748
+ title: "Your Blink Passkey",
16749
+ address: destinationAddress,
16750
+ addressLoading: destinationAddress == null
16751
+ }
16752
+ )
16753
+ ] }),
16754
+ /* @__PURE__ */ jsxs("div", { style: dividerRowStyle3, children: [
16755
+ /* @__PURE__ */ jsx("span", { style: dividerLineStyle(t.textMuted) }),
16756
+ /* @__PURE__ */ jsx("span", { style: dividerLabelStyle3(t.textMuted, t.fontWeightSemibold), children: "Continue in wallet" }),
16757
+ /* @__PURE__ */ jsx("span", { style: dividerLineStyle(t.textMuted) })
16758
+ ] }),
15954
16759
  /* @__PURE__ */ jsxs("div", { style: cardStyle2(t.bgRecessed, t.radiusLg), children: [
15955
16760
  /* @__PURE__ */ jsx("style", { children: SPIN_KEYFRAMES_CSS2 }),
15956
16761
  steps.map((s, i) => /* @__PURE__ */ jsx(ChecklistRow, { step: s }, i))
@@ -15960,6 +16765,32 @@ function ApprovingInWalletScreen({
15960
16765
  }
15961
16766
  );
15962
16767
  }
16768
+ function AddressRow({
16769
+ circle,
16770
+ title,
16771
+ address,
16772
+ subtitle,
16773
+ addressLoading
16774
+ }) {
16775
+ const { tokens: t } = useBlinkConfig();
16776
+ const sub = subtitle ?? (address ? truncateMiddle(address) : null);
16777
+ return /* @__PURE__ */ jsxs("div", { style: addressRowStyle, children: [
16778
+ circle,
16779
+ /* @__PURE__ */ jsxs("div", { style: addressTextColStyle, children: [
16780
+ /* @__PURE__ */ jsx("span", { style: addressTitleStyle(t.text, t.fontWeightSemibold), children: title }),
16781
+ (sub || addressLoading) && /* @__PURE__ */ jsx("span", { style: addressSubSlotStyle, children: sub ? /* @__PURE__ */ jsx("span", { style: addressSubStyle(t.textMuted, t.fontWeightRegular), children: sub }) : /* @__PURE__ */ jsx(
16782
+ Shimmer,
16783
+ {
16784
+ width: 132,
16785
+ height: 12,
16786
+ borderRadius: 4,
16787
+ baseColor: t.bgInput,
16788
+ highlightColor: t.border
16789
+ }
16790
+ ) })
16791
+ ] })
16792
+ ] });
16793
+ }
15963
16794
  function ChecklistRow({ step }) {
15964
16795
  const { tokens: t } = useBlinkConfig();
15965
16796
  const isComplete = step.status === "complete";
@@ -15972,7 +16803,7 @@ function ChecklistRow({ step }) {
15972
16803
  fill: t.accentText
15973
16804
  }
15974
16805
  ) }) }) : isActive ? /* @__PURE__ */ jsx(RowSpinner, { color: t.accent }) : /* @__PURE__ */ jsx("span", { style: pendingBadgeStyle(t.bgRecessed) }),
15975
- /* @__PURE__ */ jsx("span", { style: labelStyle7(isComplete || isActive ? t.text : t.textMuted), children: step.label })
16806
+ /* @__PURE__ */ jsx("span", { style: labelStyle7(isComplete || isActive ? t.text : t.textMuted, t.fontWeightRegular), children: step.label })
15976
16807
  ] });
15977
16808
  }
15978
16809
  function RowSpinner({ color }) {
@@ -16007,22 +16838,28 @@ function RowSpinner({ color }) {
16007
16838
  }
16008
16839
  );
16009
16840
  }
16010
- function buildSteps(step, tokenSymbol, chainName, complete) {
16841
+ function ArrowDownIcon({ color }) {
16842
+ return /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", style: { color }, children: [
16843
+ /* @__PURE__ */ jsx("path", { d: "M12 5v14", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }),
16844
+ /* @__PURE__ */ jsx("path", { d: "M6 13l6 6 6-6", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" })
16845
+ ] });
16846
+ }
16847
+ function buildSteps(step, tokenSymbol, complete, signing) {
16011
16848
  const token = tokenSymbol ?? "token";
16012
- const chain = chainName ?? "chain";
16013
- const passkeyLabel = `Approve ${token} for passkey`;
16849
+ const passkeyLabel = "Assign spending right to your passkey";
16850
+ const activeStatus = signing ? "active" : "pending";
16014
16851
  if (step === "spl") {
16015
- return [{ label: passkeyLabel, status: complete ? "complete" : "active" }];
16852
+ return [{ label: passkeyLabel, status: complete ? "complete" : activeStatus }];
16016
16853
  }
16017
- const approveLabel = `Approve ${token} on ${chain}`;
16854
+ const approveLabel = `Approve ${token} for spending via Permit2`;
16018
16855
  return [
16019
16856
  {
16020
16857
  label: approveLabel,
16021
- status: complete || step !== "approve" ? "complete" : "active"
16858
+ status: complete || step !== "approve" ? "complete" : activeStatus
16022
16859
  },
16023
16860
  {
16024
16861
  label: passkeyLabel,
16025
- status: complete ? "complete" : step === "approve" ? "pending" : "active"
16862
+ status: complete ? "complete" : step === "approve" ? "pending" : activeStatus
16026
16863
  }
16027
16864
  ];
16028
16865
  }
@@ -16038,18 +16875,6 @@ function useDelayedRetry(enabled) {
16038
16875
  }, [enabled]);
16039
16876
  return visible;
16040
16877
  }
16041
- function LockIcon4() {
16042
- return /* @__PURE__ */ jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
16043
- "path",
16044
- {
16045
- d: "M7 10V8a5 5 0 0 1 10 0v2m-12 0h14a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1z",
16046
- stroke: "currentColor",
16047
- strokeWidth: "2",
16048
- strokeLinecap: "round",
16049
- strokeLinejoin: "round"
16050
- }
16051
- ) });
16052
- }
16053
16878
  var wordmarkImgStyle5 = {
16054
16879
  height: 22,
16055
16880
  width: "auto",
@@ -16067,15 +16892,84 @@ var contentStyle16 = {
16067
16892
  minHeight: 0,
16068
16893
  width: "100%"
16069
16894
  };
16070
- var headingStyle13 = (color) => ({
16895
+ var headingStyle13 = (color, fontWeight) => ({
16071
16896
  fontSize: 24,
16072
- fontWeight: 700,
16897
+ fontWeight,
16073
16898
  lineHeight: "normal",
16074
16899
  letterSpacing: 0,
16075
16900
  color,
16076
- margin: 0,
16901
+ margin: "8px 0 8px",
16077
16902
  textAlign: "center"
16078
16903
  });
16904
+ var transferCardStyle = (bg, radius) => ({
16905
+ background: bg,
16906
+ borderRadius: radius,
16907
+ padding: "20px 16px",
16908
+ width: "100%",
16909
+ boxSizing: "border-box",
16910
+ display: "flex",
16911
+ flex: 1,
16912
+ flexDirection: "column",
16913
+ justifyContent: "space-between",
16914
+ gap: 12
16915
+ });
16916
+ var dividerRowStyle3 = {
16917
+ display: "flex",
16918
+ alignItems: "center",
16919
+ gap: 12,
16920
+ width: "100%"
16921
+ };
16922
+ var dividerLineStyle = (color) => ({
16923
+ flex: 1,
16924
+ height: 1,
16925
+ background: color
16926
+ });
16927
+ var dividerLabelStyle3 = (color, fontWeight) => ({
16928
+ fontSize: 11,
16929
+ fontWeight,
16930
+ letterSpacing: "0.08em",
16931
+ textTransform: "uppercase",
16932
+ color,
16933
+ whiteSpace: "nowrap"
16934
+ });
16935
+ var addressRowStyle = {
16936
+ display: "flex",
16937
+ alignItems: "center",
16938
+ gap: 12,
16939
+ width: "100%"
16940
+ };
16941
+ var addressTextColStyle = {
16942
+ display: "flex",
16943
+ flexDirection: "column",
16944
+ gap: 2,
16945
+ minWidth: 0
16946
+ };
16947
+ var addressTitleStyle = (color, fontWeight) => ({
16948
+ fontSize: 16,
16949
+ fontWeight,
16950
+ lineHeight: 1.25,
16951
+ color
16952
+ });
16953
+ var addressSubSlotStyle = {
16954
+ display: "flex",
16955
+ alignItems: "center",
16956
+ minHeight: 18
16957
+ };
16958
+ var addressSubStyle = (color, fontWeight) => ({
16959
+ fontSize: 14,
16960
+ fontWeight,
16961
+ lineHeight: 1.25,
16962
+ color,
16963
+ overflow: "hidden",
16964
+ textOverflow: "ellipsis",
16965
+ whiteSpace: "nowrap"
16966
+ });
16967
+ var arrowRowStyle = {
16968
+ display: "flex",
16969
+ alignItems: "center",
16970
+ height: 20,
16971
+ paddingLeft: 10
16972
+ };
16079
16973
  var cardStyle2 = (bg, radius) => ({
16080
16974
  background: bg,
16081
16975
  borderRadius: radius,
@@ -16083,7 +16977,9 @@ var cardStyle2 = (bg, radius) => ({
16083
16977
  width: "100%",
16084
16978
  boxSizing: "border-box",
16085
16979
  display: "flex",
16980
+ flex: 1,
16086
16981
  flexDirection: "column",
16982
+ justifyContent: "center",
16087
16983
  gap: 4
16088
16984
  });
16089
16985
  var rowStyle6 = {
@@ -16091,10 +16987,25 @@ var rowStyle6 = {
16091
16987
  alignItems: "center",
16092
16988
  gap: 16,
16093
16989
  padding: "12px 16px 12px 12px",
16990
+ // Reserve a stable height (two label lines + vertical padding) so swapping the
16991
+ // leading badge between the pending circle, the spinner, and the completion
16992
+ // checkmark — and the muted→active label restyle — never reflows the card.
16993
+ minHeight: 64,
16094
16994
  borderRadius: 16,
16095
16995
  width: "100%",
16096
16996
  boxSizing: "border-box"
16097
16997
  };
16998
+ var passkeyCircleStyle = (bg, iconColor) => ({
16999
+ width: 40,
17000
+ height: 40,
17001
+ borderRadius: "50%",
17002
+ background: bg,
17003
+ color: iconColor,
17004
+ display: "inline-flex",
17005
+ alignItems: "center",
17006
+ justifyContent: "center",
17007
+ flexShrink: 0
17008
+ });
16098
17009
  var completeBadgeStyle = (color) => ({
16099
17010
  width: 24,
16100
17011
  height: 24,
@@ -16112,13 +17023,13 @@ var pendingBadgeStyle = (color) => ({
16112
17023
  background: color,
16113
17024
  flexShrink: 0
16114
17025
  });
16115
- var labelStyle7 = (color) => ({
17026
+ var labelStyle7 = (color, fontWeight) => ({
16116
17027
  fontSize: "1rem",
16117
- fontWeight: 400,
17028
+ fontWeight,
16118
17029
  lineHeight: 1.25,
16119
17030
  color
16120
17031
  });
16121
- var footerStackStyle6 = {
17032
+ var footerStackStyle5 = {
16122
17033
  display: "flex",
16123
17034
  flexDirection: "column",
16124
17035
  gap: 16,
@@ -16130,32 +17041,6 @@ var retryStackStyle = {
16130
17041
  gap: 8,
16131
17042
  width: "100%"
16132
17043
  };
16133
- var lockBannerStyle3 = {
16134
- display: "flex",
16135
- alignItems: "flex-start",
16136
- gap: 16,
16137
- padding: "12px 16px 12px 12px",
16138
- borderRadius: 16,
16139
- width: "100%",
16140
- boxSizing: "border-box"
16141
- };
16142
- var lockIconWrapStyle3 = (color) => ({
16143
- flexShrink: 0,
16144
- width: 24,
16145
- height: 24,
16146
- display: "inline-flex",
16147
- alignItems: "center",
16148
- justifyContent: "center",
16149
- color
16150
- });
16151
- var lockBannerTextStyle3 = (color) => ({
16152
- margin: 0,
16153
- flex: 1,
16154
- fontSize: 12,
16155
- fontWeight: 500,
16156
- lineHeight: "normal",
16157
- color
16158
- });
16159
17044
  function ConfirmSignScreen({
16160
17045
  walletName,
16161
17046
  chainFamily,
@@ -16281,7 +17166,7 @@ function TokenPickerScreen({
16281
17166
  }
16282
17167
  const entries2 = [];
16283
17168
  for (const wallet of account.wallets) {
16284
- for (const source of wallet.sources) {
17169
+ for (const source of wallet.sources ?? []) {
16285
17170
  const visibleBalance = source.balance.available.amount;
16286
17171
  if (!isSelectableDepositSourceAmountUsd(visibleBalance, minDepositFloor)) continue;
16287
17172
  entries2.push({
@@ -17122,22 +18007,45 @@ function buildOpenWalletScreenProps({
17122
18007
  function isApprovingInWalletAction(actionType) {
17123
18008
  return actionType === "APPROVE_PERMIT2" || actionType === "SIGN_PERMIT2" || actionType === "APPROVE_SPL";
17124
18009
  }
18010
+ function formatUsdTwoDecimals3(value) {
18011
+ return (Number.isFinite(value) ? value : 0).toLocaleString("en-US", {
18012
+ minimumFractionDigits: 2,
18013
+ maximumFractionDigits: 2
18014
+ });
18015
+ }
18016
+ function readMetadataString(metadata, key) {
18017
+ const value = metadata?.[key];
18018
+ return typeof value === "string" && value.trim() !== "" ? value : null;
18019
+ }
17125
18020
  function buildApprovingInWalletScreenProps({
17126
18021
  flow,
17127
18022
  remote,
18023
+ derived,
17128
18024
  handlers
17129
18025
  }) {
17130
18026
  const { state } = flow;
17131
18027
  const setupToken = state.setupDepositToken;
17132
- const actionType = remote.authExecutorCurrentAction?.type ?? null;
18028
+ const currentAction = remote.authExecutorCurrentAction;
18029
+ const actionType = currentAction?.type ?? null;
18030
+ const limit = state.setupSpendingLimit;
18031
+ const spendingLimitLabel = limit == null ? null : "unlimited" in limit ? "Unlimited lifetime spending limit" : `$${formatUsdTwoDecimals3(limit.usd)} lifetime spending limit`;
18032
+ const destinationAddress = remote.authApprovalSmartAccountAddress ?? remote.authExecutorApprovalDestinationAddress ?? readMetadataString(currentAction?.metadata, "smartAccountAddress") ?? readMetadataString(currentAction?.metadata, "ownerPubkey");
17133
18033
  const isSolana = setupToken != null && state.chains.find((c) => c.name === setupToken.chainName)?.chainFamily === "svm";
17134
18034
  const step = actionType === "SIGN_PERMIT2" ? "sign" : actionType === "APPROVE_SPL" ? "spl" : actionType == null && isSolana ? "spl" : "approve";
17135
18035
  const complete = remote.authExecutorCompleted && actionType == null && !remote.authExecutorExecuting;
18036
+ const awaitingApproval = remote.authExecutorAwaitingApproval ?? false;
18037
+ const signing = isApprovingInWalletAction(actionType) && !awaitingApproval && !complete;
17136
18038
  return {
17137
18039
  tokenSymbol: setupToken?.symbol ?? null,
17138
18040
  chainName: setupToken?.chainName ?? null,
17139
18041
  step,
17140
18042
  complete,
18043
+ signing,
18044
+ spendingLimitLabel,
18045
+ destinationAddress,
18046
+ tokenLogoUri: derived.selectedSource?.token.logoURI ?? null,
18047
+ awaitingApproval,
18048
+ onApprove: handlers.onApprove,
17141
18049
  error: flow.state.error || remote.authExecutorError,
17142
18050
  onRetry: handlers.onRetryAuthorization,
17143
18051
  onLogout: flow.isDesktop ? handlers.onLogout : void 0,
@@ -17156,7 +18064,7 @@ function buildLinkTokensScreenProps({
17156
18064
  handlers
17157
18065
  }) {
17158
18066
  const sourceOptions = remote.pendingSelectSource?.metadata?.options ?? [];
17159
- const rowContexts = derived.selectSourceChoices.flatMap(
18067
+ const supportedRows = derived.selectSourceChoices.flatMap(
17160
18068
  (chain) => chain.tokens.map((t) => {
17161
18069
  const rawOption = sourceOptions.find(
17162
18070
  (option) => option.chainName === chain.chainName && option.tokenSymbol === t.tokenSymbol
@@ -17176,6 +18084,10 @@ function buildLinkTokensScreenProps({
17176
18084
  };
17177
18085
  })
17178
18086
  );
18087
+ const rowContexts = [
18088
+ ...supportedRows.filter((row) => isVisibleUsdAmountAtTwoDecimals(row.balanceUsd)),
18089
+ ...supportedRows.filter((row) => !isVisibleUsdAmountAtTwoDecimals(row.balanceUsd))
18090
+ ];
17179
18091
  rowContexts.push(
17180
18092
  ...derived.selectSourceUnsupportedChoices.flatMap(
17181
18093
  (chain) => chain.tokens.map((t) => ({
@@ -17197,6 +18109,17 @@ function buildLinkTokensScreenProps({
17197
18109
  balanceLabel: `${formatNativeAmount(native.amount)} ${native.tokenSymbol}`
17198
18110
  }))
17199
18111
  );
18112
+ const selectedTokenSymbol = flow.state.setupDepositToken?.symbol ?? derived.selectSourceRecommended?.tokenSymbol;
18113
+ const selectedChainName = flow.state.setupDepositToken?.chainName ?? derived.selectSourceRecommended?.chainName;
18114
+ if (selectedTokenSymbol && selectedChainName) {
18115
+ const match = rowContexts.findIndex(
18116
+ (row) => row.symbol === selectedTokenSymbol && row.chainName === selectedChainName
18117
+ );
18118
+ if (match >= 0 && !rowContexts[match].notSupported) {
18119
+ const [pinned] = rowContexts.splice(match, 1);
18120
+ rowContexts.unshift(pinned);
18121
+ }
18122
+ }
17200
18123
  const entries2 = rowContexts.map(
17201
18124
  ({ symbol, chainName, balanceUsd, notSupported, tokenLogoUri, balanceLabel }) => ({
17202
18125
  tokenSymbol: symbol,
@@ -17209,8 +18132,6 @@ function buildLinkTokensScreenProps({
17209
18132
  })
17210
18133
  );
17211
18134
  const firstSupportedIndex = rowContexts.findIndex((row) => !row.notSupported);
17212
- const selectedTokenSymbol = flow.state.setupDepositToken?.symbol ?? derived.selectSourceRecommended?.tokenSymbol;
17213
- const selectedChainName = flow.state.setupDepositToken?.chainName ?? derived.selectSourceRecommended?.chainName;
17214
18135
  const selectedIndex = (() => {
17215
18136
  if (!selectedTokenSymbol || !selectedChainName) {
17216
18137
  return firstSupportedIndex;
@@ -17302,9 +18223,10 @@ function buildDepositScreenProps({
17302
18223
  const registryChainIds = derived.walletConnectChainIdsByAccount[account.id] ?? null;
17303
18224
  const approvedChainIds = account.walletConnect?.approvedChainIds ?? null;
17304
18225
  for (const wallet of account.wallets) {
17305
- for (const source of wallet.sources) {
18226
+ for (const source of wallet.sources ?? []) {
17306
18227
  const balance = source.balance.available.amount;
17307
- if (!isSelectableDepositSourceAmountUsd(balance, minDepositFloor)) continue;
18228
+ const isAuthorized = source.token.status === "AUTHORIZED";
18229
+ if (!isAuthorized && !isSelectableDepositSourceAmountUsd(balance, minDepositFloor)) continue;
17308
18230
  const chain = flow.state.chains.find((c) => c.name === wallet.chain.name);
17309
18231
  const requiresAuth = source.token.status != null && source.token.status !== "AUTHORIZED";
17310
18232
  const notSupported = requiresAuth && chain?.commonId != null && registryChainIds != null && !registryChainIds.includes(chain.commonId) && !(approvedChainIds?.includes(chain.commonId) ?? false);
@@ -17326,7 +18248,10 @@ function buildDepositScreenProps({
17326
18248
  }
17327
18249
  return {
17328
18250
  merchantName,
17329
- availableBalance: selectedSource ? selectedSource.balance.available.amount : selectedAccount ? selectedAccount.wallets.reduce((sum, w) => sum + w.balance.available.amount, 0) : maxSourceBalance,
18251
+ // Wallet balances arrive after the (balance-free) accounts load; while they
18252
+ // do, the source pill / token rows shimmer instead of showing $0.00.
18253
+ balancesLoading: state.balancesLoading,
18254
+ availableBalance: selectedSource ? selectedSource.balance.available.amount : selectedAccount ? selectedAccount.wallets.reduce((sum, w) => sum + (w.balance?.available.amount ?? 0), 0) : maxSourceBalance,
17330
18255
  remainingLimit: selectedSource != null ? selectedSource.remainingAllowance ?? null : selectedAccount?.remainingAllowance ?? null,
17331
18256
  tokenCount,
17332
18257
  initialAmount: parsedAmt,
@@ -17517,6 +18442,28 @@ function StepRendererContent({
17517
18442
  screen
17518
18443
  }) {
17519
18444
  const input = { flow, remote, derived, forms, handlers };
18445
+ const s = flow.state;
18446
+ useEffect(() => {
18447
+ if (screen !== "loading") return;
18448
+ appendDebug("warn", "stuck on loading shimmer", {
18449
+ phaseStep: s.phase.step,
18450
+ privyReady: s.privyReady,
18451
+ privyAuthenticated: s.privyAuthenticated,
18452
+ activeCredentialId: s.activeCredentialId,
18453
+ passkeyConfigLoaded: s.passkeyConfigLoaded,
18454
+ loadingData: s.loadingData,
18455
+ initialDataLoaded: s.initialDataLoaded,
18456
+ loginRequested: s.loginRequested,
18457
+ enableFullWidget: s.enableFullWidget,
18458
+ authenticatedOnOpen: s.authenticatedOnOpen,
18459
+ welcomeBackAcknowledged: s.welcomeBackAcknowledged,
18460
+ mobileFlow: s.mobileFlow,
18461
+ error: s.error,
18462
+ accountsCount: s.accounts.length,
18463
+ hasActiveWallet: s.accounts.some((a) => a.wallets.some((w) => w.status === "ACTIVE")),
18464
+ providersCount: s.providers.length
18465
+ });
18466
+ }, [screen, s]);
17520
18467
  switch (screen) {
17521
18468
  case "loading":
17522
18469
  return /* @__PURE__ */ jsx(BlinkLoadingScreen, {});
@@ -17545,11 +18492,9 @@ function StepRendererContent({
17545
18492
  case "wallet-picker":
17546
18493
  return /* @__PURE__ */ jsx(WalletPickerScreen, { ...buildWalletPickerScreenProps(input) });
17547
18494
  case "open-wallet": {
17548
- const currentActionType = input.remote.authExecutorCurrentAction?.type;
17549
- const inSigningAction = isApprovingInWalletAction(currentActionType);
17550
- const pastPairingTransient = input.remote.authExecutorExecuting && input.remote.pendingSelectSource == null && currentActionType != null && (currentActionType !== "OPEN_PROVIDER" || input.remote.sourceSelectionResolved);
18495
+ const pastSourceSelection = input.remote.authExecutorExecuting && input.remote.pendingSelectSource == null && input.remote.sourceSelectionResolved;
17551
18496
  const settlingPostSign = input.flow.state.linkSettling;
17552
- if (inSigningAction || pastPairingTransient || settlingPostSign) {
18497
+ if (pastSourceSelection || settlingPostSign) {
17553
18498
  return /* @__PURE__ */ jsx(ApprovingInWalletScreen, { ...buildApprovingInWalletScreenProps(input) });
17554
18499
  }
17555
18500
  return /* @__PURE__ */ jsx(OpenWalletScreen, { ...buildOpenWalletScreenProps(input) });
@@ -17559,12 +18504,9 @@ function StepRendererContent({
17559
18504
  if (phase.step === "wallet-setup" && phase.pendingSourceWait) {
17560
18505
  return /* @__PURE__ */ jsx(LinkTokensScreen, { ...buildLinkTokensScreenProps(input) });
17561
18506
  }
17562
- const inSigningAction = isApprovingInWalletAction(
17563
- input.remote.authExecutorCurrentAction?.type
17564
- );
17565
- const justResolvedSelectSource = input.remote.authExecutorExecuting && input.remote.pendingSelectSource == null;
18507
+ const pastSourceSelection = input.remote.authExecutorExecuting && input.remote.pendingSelectSource == null && input.remote.sourceSelectionResolved;
17566
18508
  const settlingPostSign = input.flow.state.linkSettling;
17567
- if (inSigningAction || justResolvedSelectSource || settlingPostSign) {
18509
+ if (pastSourceSelection || settlingPostSign) {
17568
18510
  return /* @__PURE__ */ jsx(ApprovingInWalletScreen, { ...buildApprovingInWalletScreenProps(input) });
17569
18511
  }
17570
18512
  return /* @__PURE__ */ jsx(LinkTokensScreen, { ...buildLinkTokensScreenProps(input) });
@@ -17648,11 +18590,12 @@ var PaymentErrorBoundary = class extends Component {
17648
18590
  };
17649
18591
  function selectedSourceForWallet(selectedWallet, selectedTokenSymbol, depositAmount, priorityContext) {
17650
18592
  if (!selectedWallet) return null;
18593
+ const walletSources = selectedWallet.sources ?? [];
17651
18594
  if (selectedTokenSymbol) {
17652
- return selectedWallet.sources.find((s) => s.token.symbol === selectedTokenSymbol) ?? null;
18595
+ return walletSources.find((s) => s.token.symbol === selectedTokenSymbol) ?? null;
17653
18596
  }
17654
18597
  const walletChainName = selectedWallet.chain.name;
17655
- const ranked = selectedWallet.sources.map((source, index) => {
18598
+ const ranked = walletSources.map((source, index) => {
17656
18599
  return { source, index };
17657
18600
  }).sort((a, b) => {
17658
18601
  const priority = compareDepositSourcePriority(
@@ -17700,7 +18643,7 @@ function computeDerivedState(state, depositAmount = 0, priorityContext) {
17700
18643
  let maxSourceBalance = 0;
17701
18644
  for (const acct of state.accounts) {
17702
18645
  for (const wallet of acct.wallets) {
17703
- for (const source of wallet.sources) {
18646
+ for (const source of wallet.sources ?? []) {
17704
18647
  if (source.balance.available.amount > maxSourceBalance) {
17705
18648
  maxSourceBalance = source.balance.available.amount;
17706
18649
  }
@@ -17710,7 +18653,7 @@ function computeDerivedState(state, depositAmount = 0, priorityContext) {
17710
18653
  let tokenCount = 0;
17711
18654
  for (const acct of state.accounts) {
17712
18655
  for (const wallet of acct.wallets) {
17713
- tokenCount += wallet.sources.filter((s) => s.balance.available.amount > 0).length;
18656
+ tokenCount += (wallet.sources ?? []).filter((s) => s.balance.available.amount > 0).length;
17714
18657
  }
17715
18658
  }
17716
18659
  return {
@@ -17803,6 +18746,7 @@ function useAuthHandlers(dispatch, apiBaseUrl, popupAuthRef) {
17803
18746
  dispatch({ type: "PASSKEY_ACTIVATED", credentialId: result.credentialId, publicKey: result.publicKey });
17804
18747
  dispatch({ type: "SYNC_PRIVY_SESSION", ready: true, authenticated: true });
17805
18748
  window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, result.credentialId);
18749
+ track("login_succeeded", { method: "passkey", popup: true });
17806
18750
  } catch (err) {
17807
18751
  captureException(err);
17808
18752
  dispatch({
@@ -17815,6 +18759,7 @@ function useAuthHandlers(dispatch, apiBaseUrl, popupAuthRef) {
17815
18759
  }, [dispatch, popupAuthRef]);
17816
18760
  const { loginWithPasskey, state: loginState } = useLoginWithPasskey({
17817
18761
  onComplete: (params) => {
18762
+ track("login_succeeded", { method: "passkey", popup: false });
17818
18763
  handleAuthComplete(params.user);
17819
18764
  },
17820
18765
  onError: (error) => {
@@ -17843,6 +18788,7 @@ function useAuthHandlers(dispatch, apiBaseUrl, popupAuthRef) {
17843
18788
  dispatch({ type: "PASSKEY_ACTIVATED", credentialId: result.credentialId, publicKey: result.publicKey });
17844
18789
  dispatch({ type: "SYNC_PRIVY_SESSION", ready: true, authenticated: true });
17845
18790
  window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, result.credentialId);
18791
+ track("signup_succeeded", { method: "passkey", popup: true });
17846
18792
  } catch (err) {
17847
18793
  captureException(err);
17848
18794
  dispatch({
@@ -17855,6 +18801,7 @@ function useAuthHandlers(dispatch, apiBaseUrl, popupAuthRef) {
17855
18801
  }, [dispatch, popupAuthRef]);
17856
18802
  const { signupWithPasskey, state: signupState } = useSignupWithPasskey({
17857
18803
  onComplete: (params) => {
18804
+ track("signup_succeeded", { method: "passkey", popup: false });
17858
18805
  handleAuthComplete(params.user);
17859
18806
  },
17860
18807
  onError: (error) => {
@@ -17926,6 +18873,31 @@ function usePasskeyHandlers(dispatch, apiBaseUrl) {
17926
18873
  };
17927
18874
  }
17928
18875
 
18876
+ // src/balancesLoad.ts
18877
+ async function fetchBalancesByAccountId(apiBaseUrl, token, credentialId, accounts) {
18878
+ const entries2 = await Promise.all(
18879
+ accounts.map(async (account) => {
18880
+ try {
18881
+ const balances = await fetchAccountBalances(
18882
+ apiBaseUrl,
18883
+ token,
18884
+ account.id,
18885
+ credentialId
18886
+ );
18887
+ return [account.id, balances];
18888
+ } catch (err) {
18889
+ captureException(err);
18890
+ return null;
18891
+ }
18892
+ })
18893
+ );
18894
+ const result = {};
18895
+ for (const entry of entries2) {
18896
+ if (entry) result[entry[0]] = entry[1];
18897
+ }
18898
+ return result;
18899
+ }
18900
+
17929
18901
  // src/transferSourceResolution.ts
17930
18902
  var STANDARD_TRANSFER_SOURCE_UNAVAILABLE_ERROR = "Selected payment source is no longer available. Choose or link an active wallet before depositing.";
17931
18903
  function resolveStandardTransferSource({
@@ -17977,14 +18949,14 @@ function useTransferHandlers(deps) {
17977
18949
  const processingStartedAtRef = useRef(null);
17978
18950
  const pollingTransferIdRef = useRef(null);
17979
18951
  const depositSelectionReloadCountRef = useRef(0);
17980
- const reloadAccounts = useCallback(async () => {
18952
+ const reloadAccounts = useCallback(async (opts) => {
17981
18953
  depositSelectionReloadCountRef.current += 1;
17982
18954
  if (depositSelectionReloadCountRef.current === 1) {
17983
18955
  dispatch({ type: "SET_DEPOSIT_SELECTION_REFRESHING", value: true });
17984
18956
  }
17985
18957
  try {
17986
18958
  const token = await getAccessToken();
17987
- if (!token || !activeCredentialId) return;
18959
+ if (!token || !activeCredentialId) return null;
17988
18960
  const [accts, prov] = await Promise.all([
17989
18961
  fetchAccounts(apiBaseUrl, token, activeCredentialId),
17990
18962
  fetchProviders(apiBaseUrl, token)
@@ -18008,6 +18980,20 @@ function useTransferHandlers(deps) {
18008
18980
  defaults,
18009
18981
  resetSelectedTokenSymbol
18010
18982
  });
18983
+ const balancesPromise = fetchBalancesByAccountId(
18984
+ apiBaseUrl,
18985
+ token,
18986
+ activeCredentialId,
18987
+ accts
18988
+ ).then((balancesByAccountId) => {
18989
+ dispatch({ type: "BALANCES_LOADED", balancesByAccountId });
18990
+ return balancesByAccountId;
18991
+ }).catch(() => null);
18992
+ if (opts?.awaitBalances) {
18993
+ return await balancesPromise;
18994
+ }
18995
+ void balancesPromise;
18996
+ return null;
18011
18997
  } finally {
18012
18998
  depositSelectionReloadCountRef.current = Math.max(
18013
18999
  0,
@@ -18203,7 +19189,7 @@ function useSourceSelectionHandlers(_dispatch, orchestrator, options) {
18203
19189
  const selectSourceChoices = useMemo(() => {
18204
19190
  if (!pendingSelectSourceAction) return [];
18205
19191
  const sourceOptions = pendingSelectSourceAction.metadata?.options ?? [];
18206
- return buildSelectSourceChoices(sourceOptions, minTransferAmountUsd);
19192
+ return buildSelectSourceChoices(sourceOptions, minTransferAmountUsd, true);
18207
19193
  }, [pendingSelectSourceAction, minTransferAmountUsd]);
18208
19194
  const selectSourceUnsupportedChoices = useMemo(() => {
18209
19195
  if (!pendingSelectSourceAction) return [];
@@ -18389,13 +19375,6 @@ function buildReauthorizationSessionOptions(params) {
18389
19375
  return options;
18390
19376
  }
18391
19377
 
18392
- // src/tokenAuthorizationRunOptions.ts
18393
- function buildDesktopTokenAuthorizationOnlyRunOptions(chainName, tokenSymbol) {
18394
- return {
18395
- autoResolveSource: { chainName, tokenSymbol }
18396
- };
18397
- }
18398
-
18399
19378
  // src/oneTapDefaults.ts
18400
19379
  var DEFAULT_ONE_TAP_ALLOWANCE_USD = 1e4;
18401
19380
  async function ensureDefaultOneTapAllowance(apiBaseUrl, token) {
@@ -18416,7 +19395,7 @@ function resolveReauthorizationTarget(input) {
18416
19395
  };
18417
19396
  }
18418
19397
  const wallet = input.account?.wallets.find((candidate) => candidate.id === input.selectedWalletId);
18419
- const source = wallet?.sources.find(
19398
+ const source = wallet?.sources?.find(
18420
19399
  (candidate) => input.selectedTokenSymbol ? candidate.token.symbol === input.selectedTokenSymbol : candidate.token.status === "AUTHORIZED"
18421
19400
  );
18422
19401
  return {
@@ -18427,6 +19406,28 @@ function resolveReauthorizationTarget(input) {
18427
19406
  };
18428
19407
  }
18429
19408
 
19409
+ // src/depositTokenSelection.ts
19410
+ function isSourceAuthorized(balancesByAccountId, target) {
19411
+ const account = target.accountId != null ? balancesByAccountId?.[target.accountId] : void 0;
19412
+ if (!account) return false;
19413
+ const wallet = account.wallets.find((w) => w.id === target.walletId);
19414
+ if (!wallet) return false;
19415
+ const source = wallet.sources.find((s) => s.token.symbol === target.tokenSymbol);
19416
+ return source?.token.status === "AUTHORIZED";
19417
+ }
19418
+ var defaultSettleDelay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
19419
+ async function settleUntilTokenAuthorized(reloadAccounts, target, opts = {}) {
19420
+ const maxAttempts = opts.maxAttempts ?? 4;
19421
+ const backoffMs = opts.backoffMs ?? 400;
19422
+ const delay = opts.delay ?? defaultSettleDelay;
19423
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
19424
+ const balances = await reloadAccounts({ awaitBalances: true });
19425
+ if (isSourceAuthorized(balances, target)) return true;
19426
+ if (attempt < maxAttempts - 1) await delay(backoffMs);
19427
+ }
19428
+ return false;
19429
+ }
19430
+
18430
19431
  // src/walletConnectLinks.ts
18431
19432
  function buildWalletConnectDeeplink(links, walletConnectUri) {
18432
19433
  const baseLink = links?.native ?? links?.universal;
@@ -18558,6 +19559,8 @@ function useProviderHandlers(deps) {
18558
19559
  setupDepositToken,
18559
19560
  tryStartExtensionConnectForReownWallet
18560
19561
  } = deps;
19562
+ const setupDepositTokenRef = useRef(setupDepositToken);
19563
+ setupDepositTokenRef.current = setupDepositToken;
18561
19564
  const checkWalletConnectChainSupport = useMemo(
18562
19565
  () => deps.checkWalletConnectChainSupport ?? ((reownWalletId, chainId) => checkReownWalletChainSupport(BLINK_WC_PROJECT_ID, reownWalletId, chainId)),
18563
19566
  [deps.checkWalletConnectChainSupport]
@@ -18625,7 +19628,7 @@ function useProviderHandlers(deps) {
18625
19628
  return null;
18626
19629
  }
18627
19630
  const provider = providers.find((p) => p.id === providerId);
18628
- const providerName = provider?.name ?? "Wallet";
19631
+ const providerName2 = provider?.name ?? "Wallet";
18629
19632
  try {
18630
19633
  const token = await getAccessToken();
18631
19634
  if (!token) throw new Error("Not authenticated");
@@ -18633,7 +19636,7 @@ function useProviderHandlers(deps) {
18633
19636
  const accountId = crypto.randomUUID();
18634
19637
  const account = await createAccount(apiBaseUrl, token, {
18635
19638
  id: accountId,
18636
- name: providerName,
19639
+ name: providerName2,
18637
19640
  credentialId: activeCredentialId,
18638
19641
  providerId,
18639
19642
  ...merchantAuthorization ? {
@@ -18682,7 +19685,7 @@ function useProviderHandlers(deps) {
18682
19685
  return;
18683
19686
  }
18684
19687
  const provider = providers.find((p) => p.id === providerId);
18685
- const providerName = provider?.name ?? "Wallet";
19688
+ const providerName2 = provider?.name ?? "Wallet";
18686
19689
  let shouldRestoreDesktopSelectionOnError = shouldSnapshotDesktopSelection;
18687
19690
  try {
18688
19691
  await resetWalletConnect();
@@ -18705,7 +19708,7 @@ function useProviderHandlers(deps) {
18705
19708
  const newAccountId = crypto.randomUUID();
18706
19709
  const account = await createAccount(apiBaseUrl, token, {
18707
19710
  id: newAccountId,
18708
- name: providerName,
19711
+ name: providerName2,
18709
19712
  credentialId: activeCredentialId,
18710
19713
  providerId,
18711
19714
  ...merchantAuthorization ? {
@@ -18773,7 +19776,7 @@ function useProviderHandlers(deps) {
18773
19776
  }
18774
19777
  dispatch({ type: "BEGIN_LINK_SETTLING" });
18775
19778
  try {
18776
- await reloadAccounts();
19779
+ await reloadAccounts({ awaitBalances: true });
18777
19780
  } finally {
18778
19781
  dispatch({ type: "END_LINK_SETTLING" });
18779
19782
  }
@@ -18900,7 +19903,7 @@ function useProviderHandlers(deps) {
18900
19903
  }
18901
19904
  dispatch({ type: "BEGIN_LINK_SETTLING" });
18902
19905
  try {
18903
- await reloadAccounts();
19906
+ await reloadAccounts({ awaitBalances: true });
18904
19907
  } finally {
18905
19908
  dispatch({ type: "END_LINK_SETTLING" });
18906
19909
  }
@@ -19124,7 +20127,7 @@ function useProviderHandlers(deps) {
19124
20127
  dispatch({ type: "SET_STANDARD_DESKTOP_INLINE_OPEN_WALLET", value: false });
19125
20128
  dispatch({ type: "BEGIN_LINK_SETTLING" });
19126
20129
  try {
19127
- await reloadAccounts();
20130
+ await reloadAccounts({ awaitBalances: true });
19128
20131
  } finally {
19129
20132
  dispatch({ type: "END_LINK_SETTLING" });
19130
20133
  }
@@ -19226,7 +20229,9 @@ function useProviderHandlers(deps) {
19226
20229
  const result = await orchestrator.run(
19227
20230
  sessionId,
19228
20231
  {
19229
- ...buildDesktopTokenAuthorizationOnlyRunOptions(inlineChain.name, tokenSymbol),
20232
+ // No autoResolveSource: pause at SELECT_SOURCE so LinkTokensScreen
20233
+ // renders with the token preselected (SET_SETUP_DEPOSIT_TOKEN pin
20234
+ // above) and the user can set a spending limit before approving.
19230
20235
  // Pin the existing account id so pairing and signing share one
19231
20236
  // runtime (see handleSelectWalletConnectWallet).
19232
20237
  walletConnectRuntimeKey: runtimeAccountId,
@@ -19234,10 +20239,11 @@ function useProviderHandlers(deps) {
19234
20239
  // Require the target token's chain at pairing. A reused session
19235
20240
  // that doesn't approve it re-pairs with the chain required; a
19236
20241
  // wallet that approves the connection without it fails
19237
- // OPEN_PROVIDER with a clear error instead of the auto-resolved
19238
- // SELECT_SOURCE 422 (INVALID_SELECT_SOURCE_SELECTION) — e.g.
19239
- // authorizing USDC on HyperEVM (999, a WC-optional chain) with a
19240
- // wallet that dropped or doesn't support that chain.
20242
+ // OPEN_PROVIDER with a clear error up front instead of stranding the
20243
+ // user on LinkTokensScreen whose only option (SELECT_SOURCE) would
20244
+ // 422 (INVALID_SELECT_SOURCE_SELECTION) on Approve e.g. authorizing
20245
+ // USDC on HyperEVM (999, a WC-optional chain) with a wallet that
20246
+ // dropped or doesn't support that chain.
19241
20247
  walletConnectRequiredChainId: chainId,
19242
20248
  walletConnectRequiredChainName: inlineChain.name,
19243
20249
  onWalletConnectDisplayUri: (uri) => {
@@ -19273,11 +20279,17 @@ function useProviderHandlers(deps) {
19273
20279
  dispatch({ type: "SET_STANDARD_DESKTOP_INLINE_OPEN_WALLET", value: false });
19274
20280
  dispatch({ type: "BEGIN_LINK_SETTLING" });
19275
20281
  try {
19276
- await reloadAccounts();
20282
+ await reloadAccounts({ awaitBalances: true });
19277
20283
  } finally {
19278
20284
  dispatch({ type: "END_LINK_SETTLING" });
19279
20285
  }
19280
- dispatch({ type: "SELECT_TOKEN", walletId, tokenSymbol, accountId: selectTokenAccountId });
20286
+ const confirmed = setupDepositTokenRef.current;
20287
+ dispatch({
20288
+ type: "SELECT_TOKEN",
20289
+ walletId: confirmed?.walletId ?? walletId,
20290
+ tokenSymbol: confirmed?.symbol ?? tokenSymbol,
20291
+ accountId: selectTokenAccountId
20292
+ });
19281
20293
  dispatch({ type: "DISCARD_SAVED_SELECTION" });
19282
20294
  } catch (err) {
19283
20295
  captureException(err);
@@ -19592,10 +20604,7 @@ function useProviderHandlers(deps) {
19592
20604
  walletDeeplinks: walletDeeplinks ?? null,
19593
20605
  providerId: matchedProvider?.id ?? null
19594
20606
  });
19595
- const result = await orchestrator.run(
19596
- session.id,
19597
- buildDesktopTokenAuthorizationOnlyRunOptions(inlineChain.name, tokenSymbol)
19598
- );
20607
+ const result = await orchestrator.run(session.id);
19599
20608
  if (result.status === "cancelled") {
19600
20609
  dispatch({ type: "DESKTOP_WAIT_CLEARED" });
19601
20610
  dispatch({ type: "SET_STANDARD_DESKTOP_INLINE_OPEN_WALLET", value: false });
@@ -19607,14 +20616,25 @@ function useProviderHandlers(deps) {
19607
20616
  }
19608
20617
  dispatch({ type: "DESKTOP_WAIT_CLEARED" });
19609
20618
  dispatch({ type: "SET_STANDARD_DESKTOP_INLINE_OPEN_WALLET", value: false });
20619
+ const confirmed = setupDepositTokenRef.current;
20620
+ const settleTarget = {
20621
+ accountId: _accountId,
20622
+ walletId: confirmed?.walletId ?? _walletId,
20623
+ tokenSymbol: confirmed?.symbol ?? tokenSymbol
20624
+ };
19610
20625
  dispatch({ type: "BEGIN_LINK_SETTLING" });
19611
20626
  try {
19612
- await reloadAccounts();
20627
+ await settleUntilTokenAuthorized(reloadAccounts, settleTarget);
20628
+ dispatch({
20629
+ type: "SELECT_TOKEN",
20630
+ walletId: settleTarget.walletId,
20631
+ tokenSymbol: settleTarget.tokenSymbol,
20632
+ accountId: _accountId
20633
+ });
20634
+ dispatch({ type: "DISCARD_SAVED_SELECTION" });
19613
20635
  } finally {
19614
20636
  dispatch({ type: "END_LINK_SETTLING" });
19615
20637
  }
19616
- dispatch({ type: "SELECT_TOKEN", walletId: _walletId, tokenSymbol, accountId: _accountId });
19617
- dispatch({ type: "DISCARD_SAVED_SELECTION" });
19618
20638
  } catch (err) {
19619
20639
  captureException(err);
19620
20640
  dispatch({ type: "DESKTOP_WAIT_CLEARED" });
@@ -20074,6 +21094,15 @@ function useDataLoadEffect(deps) {
20074
21094
  hasActiveCredential: !!state.activeCredentialId,
20075
21095
  loading: loadingDataRef.current
20076
21096
  });
21097
+ appendDebug("warn", "dataLoadEffect run", {
21098
+ loadAction,
21099
+ authenticated,
21100
+ accountsCount: state.accounts.length,
21101
+ hasActiveCredential: !!state.activeCredentialId,
21102
+ loadingRef: loadingDataRef.current,
21103
+ privyAuthenticated: state.privyAuthenticated,
21104
+ activeCredentialId: state.activeCredentialId ? `${state.activeCredentialId.slice(0, 8)}\u2026` : null
21105
+ });
20077
21106
  if (loadAction === "reset") {
20078
21107
  loadingDataRef.current = false;
20079
21108
  dispatch({ type: "DATA_LOAD_END" });
@@ -20091,13 +21120,21 @@ function useDataLoadEffect(deps) {
20091
21120
  const load = async () => {
20092
21121
  dispatch({ type: "DATA_LOAD_START" });
20093
21122
  try {
21123
+ appendDebug("warn", "dataLoad: requesting access token");
20094
21124
  const token = await getAccessTokenRef.current();
21125
+ appendDebug("warn", "dataLoad: token result", { hasToken: !!token, cancelled });
20095
21126
  if (!token) throw new Error("Not authenticated");
20096
21127
  const [prov, accts, chn] = await Promise.all([
20097
21128
  fetchProviders(apiBaseUrl, token),
20098
21129
  fetchAccounts(apiBaseUrl, token, credentialId),
20099
21130
  fetchChains(apiBaseUrl, token)
20100
21131
  ]);
21132
+ appendDebug("warn", "dataLoad: fetch resolved", {
21133
+ cancelled,
21134
+ providers: prov.length,
21135
+ accounts: accts.length,
21136
+ chains: chn.length
21137
+ });
20101
21138
  if (cancelled) return;
20102
21139
  const parsedAmt = depositAmountRef.current != null ? depositAmountRef.current : 0;
20103
21140
  const priorityContext = resolveDepositPriorityContext(accts, chn, destination);
@@ -20123,7 +21160,16 @@ function useDataLoadEffect(deps) {
20123
21160
  resetSelectedTokenSymbol
20124
21161
  });
20125
21162
  if (clearMobile) clearMobileFlowState();
21163
+ void fetchBalancesByAccountId(apiBaseUrl, token, credentialId, accts).then(
21164
+ (balancesByAccountId) => {
21165
+ dispatch({ type: "BALANCES_LOADED", balancesByAccountId });
21166
+ }
21167
+ );
20126
21168
  } catch (err) {
21169
+ appendDebug("error", "dataLoad: threw", {
21170
+ cancelled,
21171
+ message: err instanceof Error ? err.message : String(err)
21172
+ });
20127
21173
  if (!cancelled) {
20128
21174
  captureException(err);
20129
21175
  dispatch({
@@ -20140,6 +21186,7 @@ function useDataLoadEffect(deps) {
20140
21186
  };
20141
21187
  load();
20142
21188
  return () => {
21189
+ appendDebug("warn", "dataLoadEffect cleanup (cancelling in-flight load)");
20143
21190
  cancelled = true;
20144
21191
  loadingDataRef.current = false;
20145
21192
  dispatch({ type: "DATA_LOAD_END" });
@@ -20883,7 +21930,7 @@ function BlinkPaymentInner({
20883
21930
  useWalletConnector: useWalletConnectorProp,
20884
21931
  userAgent: typeof navigator === "undefined" ? void 0 : navigator.userAgent
20885
21932
  });
20886
- const [state, dispatch] = useReducer(
21933
+ const [state, rawDispatch] = useReducer(
20887
21934
  paymentReducer,
20888
21935
  {
20889
21936
  depositAmount,
@@ -20893,6 +21940,30 @@ function BlinkPaymentInner({
20893
21940
  },
20894
21941
  createInitialState
20895
21942
  );
21943
+ const device = isDesktop ? "desktop" : "mobile";
21944
+ const stateRef = useRef(state);
21945
+ stateRef.current = state;
21946
+ const dispatch = useCallback(
21947
+ (action) => {
21948
+ trackAction(action, stateRef.current, { device });
21949
+ rawDispatch(action);
21950
+ },
21951
+ [device]
21952
+ );
21953
+ usePostHogIdentify();
21954
+ useAnalyticsScreenView(state.phase, device);
21955
+ const flowOpenedRef = useRef(false);
21956
+ useEffect(() => {
21957
+ if (flowOpenedRef.current) return;
21958
+ flowOpenedRef.current = true;
21959
+ registerSuperProps({ device });
21960
+ track("flow_opened", {
21961
+ device,
21962
+ isMobileApp: isMobileApp ?? false,
21963
+ fullWidget: enableFullWidget ?? false,
21964
+ hasAmount: depositAmount != null
21965
+ });
21966
+ }, []);
20896
21967
  const polling = useTransferPolling(3e3, effectiveGetAccessToken);
20897
21968
  const transferSigning = useTransferSigning(2e3, { getAccessToken: effectiveGetAccessToken });
20898
21969
  const mobileFlowRefs = {
@@ -21251,7 +22322,7 @@ function BlinkPaymentInner({
21251
22322
  const handleSetDepositToken = useCallback((symbol, chainName, walletId, tokenAddress, chainId) => {
21252
22323
  dispatch({ type: "SET_SETUP_DEPOSIT_TOKEN", symbol, chainName, walletId, tokenAddress, chainId });
21253
22324
  }, []);
21254
- const handleConfirmSetupDeposit = useCallback(async () => {
22325
+ const handleConfirmSetupDeposit = useCallback(async (limit) => {
21255
22326
  const plan = planConfirmSetupDeposit({
21256
22327
  setupDepositToken: state.setupDepositToken,
21257
22328
  setupSelectedSourceOption,
@@ -21261,6 +22332,19 @@ function BlinkPaymentInner({
21261
22332
  dispatch({ type: "SET_ERROR", error: plan.error });
21262
22333
  return;
21263
22334
  }
22335
+ const token = await effectiveGetAccessToken();
22336
+ if (!token) {
22337
+ dispatch({ type: "SET_ERROR", error: "Your session expired. Please try again." });
22338
+ return;
22339
+ }
22340
+ const config = "unlimited" in limit ? { unlimitedAllowance: true } : { defaultAllowance: limit.usd, unlimitedAllowance: false };
22341
+ try {
22342
+ await updateUserConfig(apiBaseUrl, token, config);
22343
+ } catch {
22344
+ dispatch({ type: "SET_ERROR", error: "Could not save your spending limit. Please try again." });
22345
+ return;
22346
+ }
22347
+ dispatch({ type: "SET_SETUP_SPENDING_LIMIT", limit });
21264
22348
  for (const action of plan.actions) {
21265
22349
  dispatch(action);
21266
22350
  }
@@ -21269,6 +22353,8 @@ function BlinkPaymentInner({
21269
22353
  state.selectedAccountId,
21270
22354
  state.setupDepositToken,
21271
22355
  setupSelectedSourceOption,
22356
+ effectiveGetAccessToken,
22357
+ apiBaseUrl,
21272
22358
  dispatch,
21273
22359
  orchestrator
21274
22360
  ]);
@@ -21329,6 +22415,9 @@ function BlinkPaymentInner({
21329
22415
  }
21330
22416
  })();
21331
22417
  }, [orchestrator]);
22418
+ const handleApprove = useCallback(() => {
22419
+ authExecutor.approveAuthorization();
22420
+ }, [authExecutor]);
21332
22421
  const handleSetPhase = useCallback((phase) => {
21333
22422
  clearScreenErrors();
21334
22423
  if (phase.step === "deposit") {
@@ -21350,6 +22439,7 @@ function BlinkPaymentInner({
21350
22439
  onConfirmSign: transfer.handleConfirmSign,
21351
22440
  onRetryMobileStatus: mobileFlow.handleRetryMobileStatus,
21352
22441
  onRetryAuthorization: handleAuthorizationRetry,
22442
+ onApprove: handleApprove,
21353
22443
  onRetryTransferSigning: transfer.handleRetryTransferSigning,
21354
22444
  onBackFromOpenWallet: handleBackFromOpenWallet,
21355
22445
  onLogout: handleLogout,
@@ -21420,6 +22510,7 @@ function BlinkPaymentInner({
21420
22510
  handleConfirmSetupDeposit,
21421
22511
  handleBackFromSetupDeposit,
21422
22512
  handleAuthorizationRetry,
22513
+ handleApprove,
21423
22514
  disconnectWallets,
21424
22515
  state.desktopWait?.walletForegroundLink
21425
22516
  ]);
@@ -21459,6 +22550,9 @@ function BlinkPaymentInner({
21459
22550
  authExecutorError: authExecutor.error,
21460
22551
  authExecutorExecuting: authExecutor.executing,
21461
22552
  authExecutorCurrentAction: authExecutor.currentAction,
22553
+ authExecutorAwaitingApproval: authExecutor.awaitingApproval,
22554
+ authExecutorApprovalDestinationAddress: authExecutor.approvalDestinationAddress,
22555
+ authApprovalSmartAccountAddress: orchestrator.approvalSmartAccountAddress,
21462
22556
  authExecutorCompleted: orchestrator.orchestratorCompleted,
21463
22557
  transferSigningSigning: transferSigning.signing,
21464
22558
  transferSigningError: transferSigning.error,
@@ -21496,6 +22590,6 @@ function BlinkPaymentInner({
21496
22590
  ) });
21497
22591
  }
21498
22592
 
21499
- export { ACCOUNT_SWITCH_CONFLICT_MESSAGE, AdvancedSourceScreen, ApprovingInWalletScreen, AuthorizationSessionCancelledError, BLINK_ERROR_ILLUSTRATION, BLINK_LOGO, BLINK_MASCOT, BLINK_PASSKEY_ILLUSTRATION, BLINK_SUCCESS_ILLUSTRATION, BlinkDepositButton, BlinkErrorScreen, BlinkInitialLoadingScreen, BlinkLoadingScreen, BlinkPayment, BlinkProvider, ConfirmSignScreen, DepositCompleteScreen, DepositOptionsScreen, DepositScreen, DepositTransferStatusScreen, GuestTokenPickerScreen, IconCircle, InfoBanner, LOGIN_KEY_ILLUSTRATION, LinkTokensScreen, LoginScreen, ManualTransferPasskeyScreen, OpenWalletScreen, OtpVerifyScreen, OutlineButton, PasskeyIframeBlockedError, PasskeyPopupWelcomeScreen, PasskeyScreen, PoweredByFooter, PrimaryButton, ScreenHeader, ScreenLayout, SecondaryButton, SelectDepositSourceScreen, SelectSourceScreen, SettingsMenu, Spinner, StepList, StepRenderer, SuccessScreen, TokenPickerScreen, VerifyPasskeyScreen, WalletPickerScreen, WelcomeBackScreen, appendDebug, api_exports as blinkApi, buildNativeUnsupportedEntries, clearDebugEntries, createInitialState, credentialIdBase64ToBytes, darkTheme, darkThemeNew, darkTransparentTheme, darkTransparentThemeNew, deviceHasPasskey, encodePermit2ApproveCalldata, findDevicePasskey, findDevicePasskeyViaPopup, formatNativeAmount, getAtomicBatchSupportDebugInfo, getDebugEntries, getDeviceBiometricUnlockText, getTheme, getThemeBase, getWalletCapabilities, isAuthorizationSessionCancelled, isExpectedAuthorizationCancellation, isTerminalTransferStatus, isTransferAwaitingCompletion, isTransparentTheme, isUserDismissedAuthorizationError, isVisibleUsdAmountAtTwoDecimals, lightTheme, lightThemeNew, lightTransparentTheme, lightTransparentThemeNew, mapGuestPickerEntries, replaceOpenProviderForAccountSwitch, resolvePasskeyRpId, screenForPhase, subscribeDebug, supportsAtomicBatch, supportsPaymasterService, useAuthorizationExecutor, useAuthorizationOrchestrator, useBlinkConfig, useBlinkDebugLog, useBlinkDepositAmount, useSolanaAccountSwitchEffect, useTransferPolling, useTransferSigning };
22593
+ export { ACCOUNT_SWITCH_CONFLICT_MESSAGE, AdvancedSourceScreen, ApprovingInWalletScreen, AuthorizationSessionCancelledError, BLINK_ERROR_ILLUSTRATION, BLINK_LOGO, BLINK_MASCOT, BLINK_PASSKEY_ILLUSTRATION, BLINK_SUCCESS_ILLUSTRATION, BlinkDepositButton, BlinkErrorScreen, BlinkInitialLoadingScreen, BlinkLoadingScreen, BlinkPayment, BlinkProvider, ConfirmSignScreen, DepositCompleteScreen, DepositOptionsScreen, DepositScreen, DepositTransferStatusScreen, GuestTokenPickerScreen, IconCircle, InfoBanner, LOGIN_KEY_ILLUSTRATION, LinkTokensScreen, LoginScreen, ManualTransferPasskeyScreen, OpenWalletScreen, OtpVerifyScreen, OutlineButton, PasskeyIframeBlockedError, PasskeyPopupWelcomeScreen, PasskeyScreen, PoweredByFooter, PrimaryButton, ScreenHeader, ScreenLayout, SecondaryButton, SelectDepositSourceScreen, SelectSourceScreen, SettingsMenu, Spinner, StepList, StepRenderer, SuccessScreen, TokenPickerScreen, VerifyPasskeyScreen, WalletPickerScreen, WelcomeBackScreen, appendDebug, api_exports as blinkApi, buildNativeUnsupportedEntries, clearDebugEntries, createInitialState, credentialIdBase64ToBytes, darkTheme, darkThemeNew, darkTransparentTheme, darkTransparentThemeNew, deviceHasPasskey, encodePermit2ApproveCalldata, findDevicePasskey, findDevicePasskeyViaPopup, formatNativeAmount, getAtomicBatchSupportDebugInfo, getDebugEntries, getDeviceBiometricUnlockText, getTheme, getThemeBase, getWalletCapabilities, identifyUser, isAuthorizationSessionCancelled, isExpectedAuthorizationCancellation, isTerminalTransferStatus, isTransferAwaitingCompletion, isTransparentTheme, isUserDismissedAuthorizationError, isVisibleUsdAmountAtTwoDecimals, lightTheme, lightThemeNew, lightTransparentTheme, lightTransparentThemeNew, mapGuestPickerEntries, replaceOpenProviderForAccountSwitch, resetUser, resolveAnalyticsEnvironment, resolvePasskeyRpId, sanitizeProps, screenForPhase, subscribeDebug, supportsAtomicBatch, supportsPaymasterService, track, useAuthorizationExecutor, useAuthorizationOrchestrator, useBlinkConfig, useBlinkDebugLog, useBlinkDepositAmount, useSolanaAccountSwitchEffect, useTransferPolling, useTransferSigning };
21500
22594
  //# sourceMappingURL=index.js.map
21501
22595
  //# sourceMappingURL=index.js.map