@stableflow/pay-widgets 0.0.1 → 0.2.0-beta-e820becee0-20261012

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.
@@ -1,35 +1,26 @@
1
1
  // src/token-select/context.tsx
2
- import { createContext, useContext, useEffect as useEffect2, useMemo } from "react";
2
+ import { useEffect } from "react";
3
+ import { createContext, useContext } from "react";
3
4
 
4
5
  // src/token-select/balances.ts
5
6
  import { useSyncExternalStore } from "react";
6
7
 
7
8
  // src/token-select/config.ts
8
9
  var TOKEN_BALANCE_POLL_MS = 6e4;
9
- var CONFIG_STALE_MS = 30 * 60 * 1e3;
10
10
  var ALL_CHAIN_FILTER = "all";
11
- var DEFAULT_API_HOST = "https://api.stableflow.ai";
11
+ var DEFAULT_STABLE_SYMBOLS = ["USDT", "USDC"];
12
12
  var POPULAR_SECTION_TITLE = "Popular on your network";
13
13
  var TOKEN_SELECT_PREFS_STORAGE_KEY = "stableflow-pay:token-select-prefs:v1";
14
14
  var TOKEN_SELECT_PREFS_VERSION = 2;
15
15
  var MAX_RECENT_BLOCKCHAINS = 16;
16
16
  var MAX_RECENT_ASSETS = 3;
17
- var DISCONNECTED_CHAIN_ORDER = [
18
- "near",
19
- "sol",
20
- "tron",
21
- "eth",
22
- "bsc",
23
- "arb",
24
- "base",
25
- "pol",
26
- "avax",
27
- "op",
28
- "bera",
29
- "gnosis",
30
- "xlayer",
31
- "scroll"
32
- ];
17
+ var CHAIN_KIND_LABELS = {
18
+ evm: "EVM",
19
+ near: "Near",
20
+ solana: "Solana",
21
+ tron: "Tron",
22
+ zec: "Zcash"
23
+ };
33
24
  var DEFAULT_CHAIN_KINDS = {
34
25
  eth: "evm",
35
26
  base: "evm",
@@ -52,7 +43,23 @@ var DEFAULT_CHAIN_KINDS = {
52
43
  var listeners = /* @__PURE__ */ new Set();
53
44
  var balances = {};
54
45
  var reader = null;
46
+ var syncReader = null;
47
+ var tokensById = /* @__PURE__ */ new Map();
55
48
  var chainInflight = /* @__PURE__ */ new Map();
49
+ function configureBalanceReaders(options) {
50
+ reader = options.readBalances ?? null;
51
+ if (!options.readBalances && options.getBalance) {
52
+ syncReader = {
53
+ getBalance: options.getBalance,
54
+ isBalanceLoading: options.isBalanceLoading ?? (() => false)
55
+ };
56
+ return;
57
+ }
58
+ syncReader = null;
59
+ }
60
+ function setBalanceTokens(tokens) {
61
+ tokensById = new Map(tokens.map((token) => [token.assetId, token]));
62
+ }
56
63
  function emit() {
57
64
  balances = { ...balances };
58
65
  for (const listener of listeners) listener();
@@ -87,15 +94,25 @@ function withStaleOnError(prev, next) {
87
94
  if (prev?.raw == null && prev?.formatted == null) return next;
88
95
  return { ...next, raw: prev.raw, formatted: prev.formatted, updatedAt: prev.updatedAt };
89
96
  }
90
- function setBalanceReader(next) {
91
- reader = next;
92
- }
93
97
  function clearPayTokenBalances() {
94
98
  balances = {};
95
99
  emit();
96
100
  }
101
+ function syncBalance(owner, assetId) {
102
+ if (!syncReader) return void 0;
103
+ const token = tokensById.get(assetId);
104
+ if (!token) return void 0;
105
+ const loading = syncReader.isBalanceLoading(token, owner);
106
+ const formatted = syncReader.getBalance(token, owner);
107
+ if (loading && (formatted == null || formatted === "")) {
108
+ return { raw: null, formatted: null, status: "loading", error: null, updatedAt: null };
109
+ }
110
+ if (formatted == null || formatted === "") return void 0;
111
+ return { raw: null, formatted, status: "success", error: null, updatedAt: null };
112
+ }
97
113
  function getPayTokenBalance(owner, assetId) {
98
114
  if (!owner || !assetId) return void 0;
115
+ if (syncReader) return syncBalance(owner, assetId);
99
116
  return balances[`${owner}:${assetId}`] ?? balances[`${owner.toLowerCase()}:${assetId}`];
100
117
  }
101
118
  function usePayTokenBalance(owner, assetId) {
@@ -198,6 +215,7 @@ async function fetchChain(owner, tokens, force) {
198
215
  });
199
216
  }
200
217
  async function ensurePayTokenBalances(owners, tokens, opts) {
218
+ if (syncReader || !reader) return;
201
219
  const force = Boolean(opts?.force);
202
220
  const groups = groupByChain(tokens);
203
221
  await Promise.all(Array.from(groups.values()).map((group) => {
@@ -208,306 +226,66 @@ async function ensurePayTokenBalances(owners, tokens, opts) {
208
226
  }));
209
227
  }
210
228
 
211
- // src/token-select/pay-config.ts
212
- import { useCallback, useEffect, useSyncExternalStore as useSyncExternalStore2 } from "react";
213
-
214
- // src/token-select/logos.ts
215
- var LOGO_HOST = "https://assets.dapdap.net";
216
- var CHAIN_LOGO_ALIAS = {
217
- eth: "ethereum",
218
- ethereum: "ethereum",
219
- base: "base",
220
- arb: "arbitrum",
221
- arbitrum: "arbitrum",
222
- op: "optimism",
223
- optimism: "optimism",
224
- pol: "polygon",
225
- polygon: "polygon",
226
- bsc: "bsc",
227
- avax: "avalanche",
228
- avalanche: "avalanche",
229
- gnosis: "gnosis",
230
- scroll: "scroll",
231
- xlayer: "xlayer",
232
- bera: "berachain",
233
- berachain: "berachain",
234
- near: "near",
235
- sol: "solana",
236
- solana: "solana",
237
- tron: "tron",
238
- zec: "zcash",
239
- zcash: "zcash"
240
- };
241
- function logoPath(path) {
242
- const normalized = path.startsWith("/") ? path : `/${path}`;
243
- return `${LOGO_HOST}${normalized}`;
244
- }
245
- function evmTypeLogoUrl() {
246
- return logoPath("/stableflow/networks/type-evm.svg");
247
- }
248
- function chainLogoUrl(blockchainOrNetwork) {
249
- const key = String(blockchainOrNetwork || "").toLowerCase();
250
- const file = CHAIN_LOGO_ALIAS[key] || key.replace(/\s+/g, "");
251
- return logoPath(`/stableflow/networks/${file}.png`);
252
- }
253
- function tokenLogoUrl(symbol) {
254
- const upper = String(symbol || "").toUpperCase();
255
- const file = upper === "USDT0" ? "usdt" : upper.toLowerCase();
256
- return logoPath(`/stableflow/tokens/${file}.png`);
257
- }
258
-
259
- // src/token-select/pay-config.ts
260
- var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
261
- var WRAP_NEAR_CONTRACT = "wrap.near";
262
- var listeners2 = /* @__PURE__ */ new Set();
263
- var chainKindOverrides = {};
264
- var snapshot = {
265
- apiHost: DEFAULT_API_HOST,
266
- chains: [],
267
- tokens: [],
268
- source: null,
269
- status: "idle",
270
- error: null,
271
- fetchedAt: null
272
- };
273
- var inflight = null;
274
- function emit2(next) {
275
- snapshot = next;
276
- for (const listener of listeners2) listener();
277
- }
278
- function subscribe2(listener) {
279
- listeners2.add(listener);
280
- return () => listeners2.delete(listener);
281
- }
282
- function getPayConfigSnapshot() {
283
- return snapshot;
284
- }
285
- function setChainKindOverrides(overrides) {
286
- chainKindOverrides = overrides ?? {};
287
- if (snapshot.source) {
288
- const normalized = normalizePayConfig(snapshot.source, chainKindOverrides);
289
- emit2({ ...snapshot, chains: normalized.chains, tokens: normalized.tokens });
290
- }
291
- }
292
- function asRecord(value) {
293
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
294
- return value;
295
- }
296
- function apiText(value) {
297
- if (typeof value === "string") return value.trim();
298
- if (typeof value === "number" && Number.isFinite(value)) return String(value);
299
- return "";
300
- }
301
- function apiNumber(value) {
302
- if (typeof value === "number" && Number.isFinite(value)) return value;
303
- if (typeof value === "string" && value.trim()) {
304
- const parsed = Number(value);
305
- if (Number.isFinite(parsed)) return parsed;
306
- }
307
- return null;
308
- }
309
- function asBoolean(value, fallback = true) {
310
- if (typeof value === "boolean") return value;
311
- if (typeof value === "string") {
312
- const lower = value.trim().toLowerCase();
313
- if (lower === "true") return true;
314
- if (lower === "false") return false;
315
- }
316
- return fallback;
317
- }
318
- function mapSourceChain(raw) {
319
- const row = asRecord(raw) ?? {};
320
- return {
321
- network: apiText(row.network),
322
- chainId: apiText(row.chain_id ?? row.chainId),
323
- chainName: apiText(row.chain_name ?? row.chainName),
324
- logo: apiText(row.logo),
325
- explorer: apiText(row.explorer)
326
- };
327
- }
328
- function mapSourceToken(raw) {
329
- const row = asRecord(raw) ?? {};
330
- return {
331
- symbol: apiText(row.symbol),
332
- network: apiText(row.network),
333
- decimals: apiNumber(row.decimals) ?? 0,
334
- contractAddress: apiText(row.contract_address ?? row.contractAddress),
335
- price: apiText(row.price),
336
- supportPayment: asBoolean(row.support_payment ?? row.supportPayment),
337
- supportReceive: asBoolean(row.support_receive ?? row.supportReceive)
338
- };
339
- }
340
- function configBody(raw) {
341
- const row = asRecord(raw) ?? {};
342
- const data = asRecord(row.data);
343
- if (typeof row.code === "number" && data) return data;
344
- return row;
345
- }
346
- function mapPayConfigSource(raw) {
347
- const row = configBody(raw);
348
- const chains = Array.isArray(row.chains) ? row.chains.map(mapSourceChain) : [];
349
- const tokens = Array.isArray(row.tokens) ? row.tokens.map(mapSourceToken) : [];
350
- return { chains, tokens };
351
- }
352
- function chainKindFor(network, overrides = chainKindOverrides) {
353
- const key = network.trim().toLowerCase();
354
- if (!key) return null;
355
- return overrides[key] ?? DEFAULT_CHAIN_KINDS[key] ?? null;
356
- }
357
- function normalizeSymbol(symbol) {
358
- const upper = symbol.trim().toUpperCase();
359
- if (!upper) return null;
360
- if (upper === "USDT0") return "USDT";
361
- if (upper === "WNEAR") return "NEAR";
362
- return upper;
363
- }
364
- function tokenAssetId(network, symbol, contractAddress) {
365
- const addr = String(contractAddress || "").trim();
366
- return `${network}:${symbol}:${addr || "native"}`;
367
- }
368
- function isNativeAddress(contractAddress) {
369
- const addr = String(contractAddress || "").trim();
370
- if (!addr) return true;
371
- const lower = addr.toLowerCase();
372
- return lower === "native" || lower === ZERO_ADDRESS;
373
- }
374
- function normalizePayConfig(source, overrides = chainKindOverrides) {
375
- const chains = [];
376
- for (const chain of source.chains) {
377
- const chainKind = chainKindFor(chain.network, overrides);
378
- if (!chainKind || !chain.network) continue;
379
- chains.push({
380
- blockchain: chain.network,
381
- chainName: chain.chainName || chain.network,
382
- chainKind,
383
- chainId: chain.chainId || void 0,
384
- logo: chain.logo || chainLogoUrl(chain.network),
385
- explorer: chain.explorer
386
- });
387
- }
388
- const chainByCode = new Map(chains.map((chain) => [chain.blockchain, chain]));
389
- const tokens = [];
390
- for (const token of source.tokens) {
391
- const chain = chainByCode.get(token.network);
392
- if (!chain) continue;
393
- const symbol = normalizeSymbol(token.symbol);
394
- if (!symbol) continue;
395
- if (!Number.isInteger(token.decimals) || token.decimals < 0) continue;
396
- const contractAddress = token.contractAddress.trim() || null;
397
- tokens.push({
398
- assetId: tokenAssetId(token.network, symbol, contractAddress),
399
- symbol,
400
- providerSymbol: token.symbol,
401
- blockchain: token.network,
402
- decimals: token.decimals,
403
- contractAddress,
404
- price: Number(token.price || 1),
405
- logo: tokenLogoUrl(symbol),
406
- supportPayment: token.supportPayment,
407
- supportReceive: token.supportReceive,
408
- chain
409
- });
410
- }
411
- const hasNearWrap = tokens.some((token) => token.symbol === "NEAR" && token.blockchain === "near" && token.contractAddress?.toLowerCase() === WRAP_NEAR_CONTRACT);
412
- const visible = hasNearWrap ? tokens.filter((token) => !(token.symbol === "NEAR" && token.blockchain === "near" && isNativeAddress(token.contractAddress))) : tokens;
413
- return { chains, tokens: visible };
414
- }
415
- function configUrl(apiHost) {
416
- return `${apiHost.replace(/\/$/, "")}/v1/pay/config`;
417
- }
418
- async function load(apiHost, force) {
419
- const fresh = snapshot.apiHost === apiHost && snapshot.status === "success" && snapshot.fetchedAt != null && Date.now() - snapshot.fetchedAt < CONFIG_STALE_MS;
420
- if (!force && fresh) return;
421
- if (inflight && !force) return inflight;
422
- emit2({ ...snapshot, apiHost, status: snapshot.tokens.length > 0 ? snapshot.status : "loading", error: null });
423
- const run = (async () => {
424
- try {
425
- const response = await fetch(configUrl(apiHost));
426
- if (!response.ok) throw new Error(`Config request failed (${response.status})`);
427
- const source = mapPayConfigSource(await response.json());
428
- const normalized = normalizePayConfig(source, chainKindOverrides);
429
- emit2({
430
- apiHost,
431
- source,
432
- chains: normalized.chains,
433
- tokens: normalized.tokens,
434
- status: "success",
435
- error: null,
436
- fetchedAt: Date.now()
437
- });
438
- } catch (cause) {
439
- const message = cause instanceof Error ? cause.message : "Failed to load config";
440
- emit2({
441
- ...snapshot,
442
- apiHost,
443
- status: snapshot.source ? "success" : "error",
444
- error: message
445
- });
446
- } finally {
447
- inflight = null;
448
- }
449
- })();
450
- inflight = run;
451
- return run;
452
- }
453
- function refetchPayConfig(apiHost = snapshot.apiHost) {
454
- return load(apiHost, true);
455
- }
456
- function usePayConfigSnapshot() {
457
- return useSyncExternalStore2(subscribe2, getPayConfigSnapshot, getPayConfigSnapshot);
458
- }
459
- function usePayConfig(options) {
460
- const apiHost = options?.apiHost?.trim() || DEFAULT_API_HOST;
461
- const current = useSyncExternalStore2(subscribe2, getPayConfigSnapshot, getPayConfigSnapshot);
462
- const refetch = useCallback(() => {
463
- void refetchPayConfig(apiHost);
464
- }, [apiHost]);
465
- useEffect(() => {
466
- void load(apiHost, false);
467
- }, [apiHost]);
468
- return {
469
- chains: current.apiHost === apiHost ? current.chains : [],
470
- tokens: current.apiHost === apiHost ? current.tokens : [],
471
- source: current.apiHost === apiHost ? current.source : null,
472
- status: current.apiHost === apiHost ? current.status : "loading",
473
- error: current.apiHost === apiHost ? current.error : null,
474
- refetch
475
- };
476
- }
477
-
478
229
  // src/token-select/context.tsx
479
230
  import { jsx } from "react/jsx-runtime";
480
231
  var PayWidgetsContext = createContext(null);
232
+ function isProduction() {
233
+ const env = globalThis.process?.env?.NODE_ENV;
234
+ return env === "production";
235
+ }
236
+ function warnBalanceMode(readBalances, getBalance) {
237
+ if (isProduction()) return;
238
+ if (readBalances && getBalance) {
239
+ console.warn("PayWidgetsProvider: both readBalances and getBalance were passed; using readBalances.");
240
+ return;
241
+ }
242
+ if (!readBalances && !getBalance) {
243
+ console.warn("PayWidgetsProvider: pass readBalances or getBalance so token balances can load.");
244
+ }
245
+ }
481
246
  function PayWidgetsProvider(props) {
482
247
  const {
483
- apiHost,
248
+ tokens,
249
+ loading = false,
484
250
  popularTokens = [],
485
251
  wallet,
486
252
  readBalances,
487
- onConfig,
253
+ getBalance,
254
+ isBalanceLoading,
488
255
  onCopyAddress,
489
- chainKinds,
256
+ stableSymbols = [...DEFAULT_STABLE_SYMBOLS],
257
+ chainKindLabels = {},
490
258
  children
491
259
  } = props;
492
- const config = usePayConfig({ apiHost });
493
- useEffect2(() => {
494
- setChainKindOverrides(chainKinds);
495
- }, [chainKinds]);
496
- useEffect2(() => {
497
- setBalanceReader(readBalances);
498
- return () => setBalanceReader(null);
499
- }, [readBalances]);
500
- useEffect2(() => {
501
- if (config.status === "success" && config.source) onConfig?.(config.source);
502
- }, [config.source, config.status, onConfig]);
260
+ useEffect(() => {
261
+ warnBalanceMode(readBalances, getBalance);
262
+ configureBalanceReaders({
263
+ readBalances: readBalances ?? null,
264
+ getBalance: getBalance ?? null,
265
+ isBalanceLoading: isBalanceLoading ?? null
266
+ });
267
+ return () => configureBalanceReaders({});
268
+ }, [readBalances, getBalance, isBalanceLoading]);
269
+ useEffect(() => {
270
+ setBalanceTokens(tokens);
271
+ }, [tokens]);
503
272
  const connected = Object.values(wallet.accounts).some((account) => Boolean(account?.address?.trim()));
504
- useEffect2(() => {
273
+ useEffect(() => {
505
274
  if (!connected) clearPayTokenBalances();
506
275
  }, [connected]);
507
- const value = useMemo(
508
- () => ({ wallet, popularTokens, onCopyAddress }),
509
- [wallet, popularTokens, onCopyAddress]
510
- );
276
+ const balanceMode = readBalances ? "async" : getBalance ? "sync" : "none";
277
+ const value = {
278
+ tokens,
279
+ loading,
280
+ wallet,
281
+ popularTokens,
282
+ stableSymbols,
283
+ chainKindLabels,
284
+ onCopyAddress,
285
+ balanceMode,
286
+ getBalance: balanceMode === "sync" ? getBalance ?? null : null,
287
+ isBalanceLoading: balanceMode === "sync" ? isBalanceLoading ?? null : null
288
+ };
511
289
  return /* @__PURE__ */ jsx(PayWidgetsContext.Provider, { value, children });
512
290
  }
513
291
  function usePayWidgets() {
@@ -519,83 +297,23 @@ function usePayWidgets() {
519
297
  }
520
298
 
521
299
  // src/token-select/TokenSelectDialog.tsx
522
- import { useEffect as useEffect3, useMemo as useMemo2, useRef, useState as useState2, useSyncExternalStore as useSyncExternalStore4 } from "react";
300
+ import { useEffect as useEffect2, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore as useSyncExternalStore3 } from "react";
523
301
  import { Dialog } from "@stableflow/pay-ui/dialog";
524
- import { SearchInput as SearchInput2 } from "@stableflow/pay-ui/search-input";
302
+ import { SearchInput } from "@stableflow/pay-ui/search-input";
525
303
 
526
- // src/token-select/prefs.ts
527
- import { useSyncExternalStore as useSyncExternalStore3 } from "react";
528
- var EMPTY_PREFS = { recentAssetIds: [], recentBlockchains: [] };
529
- function rememberRecentAsset(recent, assetId) {
530
- const id = assetId.trim();
531
- if (!id) return recent;
532
- return [id, ...recent.filter((item) => item !== id)].slice(0, MAX_RECENT_ASSETS);
533
- }
534
- function rememberRecentBlockchain(recent, blockchain) {
535
- const code = blockchain.trim();
536
- if (!code) return recent;
537
- return [code, ...recent.filter((item) => item !== code)].slice(0, MAX_RECENT_BLOCKCHAINS);
538
- }
539
- function migrateTokenSelectPrefs(persisted, version) {
540
- const state = persisted ?? {};
541
- const recentBlockchains = version >= 1 ? Array.isArray(state.recentBlockchains) ? state.recentBlockchains : [] : typeof state.lastBlockchain === "string" && state.lastBlockchain ? [state.lastBlockchain] : [];
542
- const recentAssetIds = version >= TOKEN_SELECT_PREFS_VERSION && Array.isArray(state.recentAssetIds) ? state.recentAssetIds.filter((id) => typeof id === "string" && id.trim()).slice(0, MAX_RECENT_ASSETS) : typeof state.lastAssetId === "string" && state.lastAssetId ? [state.lastAssetId] : [];
543
- return { recentAssetIds, recentBlockchains };
544
- }
545
- function readStorage() {
546
- if (typeof localStorage === "undefined") return EMPTY_PREFS;
547
- try {
548
- const raw = localStorage.getItem(TOKEN_SELECT_PREFS_STORAGE_KEY);
549
- if (!raw) return EMPTY_PREFS;
550
- const parsed = JSON.parse(raw);
551
- const version = typeof parsed.version === "number" ? parsed.version : 0;
552
- const payload = parsed && typeof parsed === "object" && "state" in parsed ? parsed.state : parsed;
553
- return migrateTokenSelectPrefs(payload, version);
554
- } catch {
555
- return EMPTY_PREFS;
556
- }
557
- }
558
- function writeStorage(state) {
559
- if (typeof localStorage === "undefined") return;
560
- localStorage.setItem(TOKEN_SELECT_PREFS_STORAGE_KEY, JSON.stringify({
561
- state,
562
- version: TOKEN_SELECT_PREFS_VERSION
563
- }));
564
- }
565
- var prefs = readStorage();
566
- var listeners3 = /* @__PURE__ */ new Set();
567
- function emit3(next) {
568
- prefs = next;
569
- writeStorage(next);
570
- for (const listener of listeners3) listener();
571
- }
572
- function subscribe3(listener) {
573
- listeners3.add(listener);
574
- return () => listeners3.delete(listener);
575
- }
576
- function getTokenSelectPrefs() {
577
- return prefs;
578
- }
579
- function setLastToken(assetId, blockchain) {
580
- emit3({
581
- recentAssetIds: rememberRecentAsset(prefs.recentAssetIds, assetId),
582
- recentBlockchains: rememberRecentBlockchain(prefs.recentBlockchains, blockchain)
583
- });
584
- }
585
- function setLastBlockchain(blockchain) {
586
- emit3({
587
- ...prefs,
588
- recentBlockchains: rememberRecentBlockchain(prefs.recentBlockchains, blockchain)
589
- });
590
- }
591
- function useTokenSelectPrefs() {
592
- return useSyncExternalStore3(subscribe3, getTokenSelectPrefs, getTokenSelectPrefs);
593
- }
304
+ // src/token-select/chain-pane.tsx
305
+ import { IconChevron } from "@stableflow/pay-ui/icons/chevron";
306
+ import { Tooltip } from "@stableflow/pay-ui/tooltip";
307
+
308
+ // src/token-select/chain-wallet-status.tsx
309
+ import { IconCopy } from "@stableflow/pay-ui/icons/copy";
310
+ import { IconLogout } from "@stableflow/pay-ui/icons/logout";
594
311
 
595
312
  // src/token-select/format.ts
596
313
  import { clsx } from "clsx";
597
314
  import { twMerge } from "tailwind-merge";
598
315
  var fontSans = "[font-family:Montserrat,PingFang_SC,system-ui,sans-serif]";
316
+ var thinScrollbar = "[&::-webkit-scrollbar]:w-[4px] [&::-webkit-scrollbar-thumb]:rounded-[12px] [&::-webkit-scrollbar-thumb]:bg-[#d9d9d9] [&::-webkit-scrollbar-track]:bg-transparent supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-width:thin] [scrollbar-color:#d9d9d9_transparent]";
599
317
  function cx(...inputs) {
600
318
  return twMerge(clsx(inputs));
601
319
  }
@@ -605,13 +323,8 @@ function formatAddress(address, prefix = 4, suffix = 5) {
605
323
  if (address.length <= prefix + suffix) return address;
606
324
  return `${address.slice(0, prefix)}...${address.slice(-suffix)}`;
607
325
  }
608
- function chainKindLabel(kind) {
609
- if (kind === "evm") return "EVM";
610
- if (kind === "near") return "Near";
611
- if (kind === "solana") return "Solana";
612
- if (kind === "tron") return "Tron";
613
- if (kind === "zec") return "Zcash";
614
- return kind;
326
+ function chainKindLabel(kind, labels) {
327
+ return labels?.[kind] ?? CHAIN_KIND_LABELS[kind] ?? kind;
615
328
  }
616
329
  function formatDisplayAmount(value, options = {}) {
617
330
  const maxDecimals = options.maxDecimals ?? 2;
@@ -633,14 +346,7 @@ function formatDisplayAmount(value, options = {}) {
633
346
  return `${prefix}${negative ? "-" : ""}${body}`;
634
347
  }
635
348
 
636
- // src/token-select/chain-pane.tsx
637
- import { useState } from "react";
638
- import { IconArrowDown } from "@stableflow/pay-ui/icons/arrow-down";
639
- import { Tooltip } from "@stableflow/pay-ui/tooltip";
640
-
641
349
  // src/token-select/chain-wallet-status.tsx
642
- import { IconCopy } from "@stableflow/pay-ui/icons/copy";
643
- import { IconLogout } from "@stableflow/pay-ui/icons/logout";
644
350
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
645
351
  function stop(event) {
646
352
  event.preventDefault();
@@ -710,22 +416,14 @@ function ChainWalletStatus({ kind }) {
710
416
  }
711
417
 
712
418
  // src/token-select/utils.ts
713
- function groupEvmChains(chains) {
714
- const evm = chains.filter((chain) => chain.chainKind === "evm");
715
- if (evm.length === 0) return chains.map((chain) => ({ kind: "chain", chain }));
716
- let placed = false;
717
- const entries = [];
718
- for (const chain of chains) {
719
- if (chain.chainKind === "evm") {
720
- if (!placed) {
721
- entries.push({ kind: "evm", chains: evm });
722
- placed = true;
723
- }
724
- continue;
725
- }
726
- entries.push({ kind: "chain", chain });
419
+ var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
420
+ function chainsFromTokens(tokens) {
421
+ const byCode = /* @__PURE__ */ new Map();
422
+ for (const token of tokens) {
423
+ const code = token.chain.blockchain || token.blockchain;
424
+ if (!byCode.has(code)) byCode.set(code, token.chain);
727
425
  }
728
- return entries;
426
+ return Array.from(byCode.values());
729
427
  }
730
428
  function tokenBalanceUsd(token, formatted) {
731
429
  if (formatted == null || formatted === "") return -1;
@@ -741,10 +439,6 @@ function tokenMatchesSearch(token, query) {
741
439
  if (token.providerSymbol.toLowerCase().includes(q)) return true;
742
440
  return Boolean(token.contractAddress?.toLowerCase().includes(q));
743
441
  }
744
- function matchesChainFilter(token, chainFilter) {
745
- if (chainFilter === ALL_CHAIN_FILTER) return true;
746
- return token.blockchain === chainFilter;
747
- }
748
442
  function formattedBalancePositive(formatted) {
749
443
  if (formatted == null || formatted === "") return false;
750
444
  const amount = Number(formatted);
@@ -764,9 +458,6 @@ function chainBalanceUsd(blockchain, tokens, getBalanceUsd) {
764
458
  }
765
459
  return total;
766
460
  }
767
- var DISCONNECTED_CHAIN_RANK = new Map(
768
- DISCONNECTED_CHAIN_ORDER.map((code, index) => [code, index])
769
- );
770
461
  function parsePopularTokens(raw) {
771
462
  if (!raw?.trim()) return [];
772
463
  const refs = [];
@@ -790,16 +481,10 @@ function popularTokensForWallets(tokens, configured, isChainConnected) {
790
481
  }
791
482
  return result;
792
483
  }
484
+ function sortChainsAlpha(chains) {
485
+ return chains.slice().sort((left, right) => left.chainName.localeCompare(right.chainName));
486
+ }
793
487
  function sortChainsForSidebar(chains, tokens, options) {
794
- if (!options.walletConnected) {
795
- const tail = DISCONNECTED_CHAIN_ORDER.length;
796
- return chains.map((chain, index) => ({ chain, index })).sort((left, right) => {
797
- const leftRank = DISCONNECTED_CHAIN_RANK.get(left.chain.blockchain) ?? tail;
798
- const rightRank = DISCONNECTED_CHAIN_RANK.get(right.chain.blockchain) ?? tail;
799
- if (leftRank !== rightRank) return leftRank - rightRank;
800
- return left.index - right.index;
801
- }).map((item) => item.chain);
802
- }
803
488
  if (!options.showBalances) return chains.slice();
804
489
  return chains.slice().sort((left, right) => {
805
490
  const leftUsd = chainBalanceUsd(left.blockchain, tokens, options.getBalanceUsd);
@@ -808,6 +493,13 @@ function sortChainsForSidebar(chains, tokens, options) {
808
493
  return left.chainName.localeCompare(right.chainName);
809
494
  });
810
495
  }
496
+ function payerRailChains(chains, owners, fundedSet) {
497
+ return chains.filter((chain) => {
498
+ if (!owners[chain.chainKind]) return false;
499
+ if (chain.chainKind === "evm" && !fundedSet.has(chain.blockchain)) return false;
500
+ return true;
501
+ });
502
+ }
811
503
  function initialChainFilter(lockChainKind, recentBlockchains, chains) {
812
504
  if (!lockChainKind) return ALL_CHAIN_FILTER;
813
505
  const matching = chains.filter((chain) => chain.chainKind === lockChainKind);
@@ -848,9 +540,6 @@ function sortTokensByBalance(tokens, getBalanceUsd, isLoading) {
848
540
  return compareTokensBySymbol(left, right);
849
541
  });
850
542
  }
851
- function sortTokensBySymbol(tokens) {
852
- return tokens.slice().sort(compareTokensBySymbol);
853
- }
854
543
  function positiveBalanceTokens(tokens, getBalanceUsd) {
855
544
  return sortTokensByBalance(
856
545
  tokens.filter((token) => getBalanceUsd(token) > 0),
@@ -858,6 +547,90 @@ function positiveBalanceTokens(tokens, getBalanceUsd) {
858
547
  () => false
859
548
  );
860
549
  }
550
+ function isGasToken(token) {
551
+ const addr = String(token.contractAddress || "").trim().toLowerCase();
552
+ if (!addr || addr === "native" || addr === ZERO_ADDRESS) return true;
553
+ return token.blockchain === "near" && token.symbol.toUpperCase() === "NEAR";
554
+ }
555
+ function stableSymbolIndex(symbol, stableSymbols) {
556
+ const upper = symbol.toUpperCase();
557
+ return stableSymbols.findIndex((item) => item.toUpperCase() === upper);
558
+ }
559
+ function tokenPriority(token, stableSymbols = DEFAULT_STABLE_SYMBOLS) {
560
+ if (stableSymbolIndex(token.symbol, stableSymbols) >= 0) return 0;
561
+ if (isGasToken(token)) return 1;
562
+ return 2;
563
+ }
564
+ function compareTokensByPriority(left, right, stableSymbols = DEFAULT_STABLE_SYMBOLS) {
565
+ const leftPriority = tokenPriority(left, stableSymbols);
566
+ const rightPriority = tokenPriority(right, stableSymbols);
567
+ if (leftPriority !== rightPriority) return leftPriority - rightPriority;
568
+ if (leftPriority === 0) {
569
+ const leftIndex = stableSymbolIndex(left.symbol, stableSymbols);
570
+ const rightIndex = stableSymbolIndex(right.symbol, stableSymbols);
571
+ if (leftIndex !== rightIndex) return leftIndex - rightIndex;
572
+ }
573
+ return left.symbol.localeCompare(right.symbol) || left.chain.chainName.localeCompare(right.chain.chainName);
574
+ }
575
+ function sortTokensForChain(tokens, options) {
576
+ const stableSymbols = options.stableSymbols ?? DEFAULT_STABLE_SYMBOLS;
577
+ if (!options.withBalances) {
578
+ return tokens.slice().sort((left, right) => compareTokensByPriority(left, right, stableSymbols));
579
+ }
580
+ return tokens.slice().sort((left, right) => {
581
+ const leftRank = tokenBalanceRank(options.usdOf(left), options.loadingOf(left));
582
+ const rightRank = tokenBalanceRank(options.usdOf(right), options.loadingOf(right));
583
+ if (leftRank !== rightRank) return leftRank - rightRank;
584
+ if (leftRank === BALANCE_RANK_FUNDED) {
585
+ const byUsd = options.usdOf(right) - options.usdOf(left);
586
+ if (byUsd !== 0) return byUsd;
587
+ }
588
+ return compareTokensByPriority(left, right, stableSymbols);
589
+ });
590
+ }
591
+ var EVM_SEARCH_GROUP = "evm";
592
+ var EVM_SEARCH_GROUP_NAME = "EVM-based";
593
+ function groupSearchResults(tokens, query, options) {
594
+ const matched = tokens.filter((token) => tokenMatchesSearch(token, query));
595
+ const groups = /* @__PURE__ */ new Map();
596
+ for (const token of matched) {
597
+ const key = token.chain.chainKind === EVM_SEARCH_GROUP ? EVM_SEARCH_GROUP : token.blockchain;
598
+ const list = groups.get(key) ?? [];
599
+ list.push(token);
600
+ groups.set(key, list);
601
+ }
602
+ const stableSymbols = options.stableSymbols ?? DEFAULT_STABLE_SYMBOLS;
603
+ const built = Array.from(groups.entries()).map(([key, groupTokens]) => {
604
+ const chain = groupTokens[0].chain;
605
+ const folded = key === EVM_SEARCH_GROUP;
606
+ const sorted = options.role === "payer" ? sortTokensForChain(groupTokens, {
607
+ withBalances: options.hasOwner(chain.chainKind),
608
+ usdOf: options.usdOf,
609
+ loadingOf: options.loadingOf,
610
+ stableSymbols
611
+ }) : groupTokens.slice().sort((left, right) => compareTokensByPriority(left, right, stableSymbols));
612
+ return {
613
+ blockchain: folded ? EVM_SEARCH_GROUP : key,
614
+ chainName: folded ? EVM_SEARCH_GROUP_NAME : chain.chainName,
615
+ chainKind: chain.chainKind,
616
+ tokens: sorted,
617
+ usd: sorted.reduce((total, token) => {
618
+ const usd = options.usdOf(token);
619
+ return usd > 0 ? total + usd : total;
620
+ }, 0)
621
+ };
622
+ });
623
+ built.sort((left, right) => {
624
+ if (options.role === "payer" && right.usd !== left.usd) return right.usd - left.usd;
625
+ return left.chainName.localeCompare(right.chainName);
626
+ });
627
+ return built.map(({ blockchain, chainName, chainKind, tokens: groupTokens }) => ({
628
+ blockchain,
629
+ chainName,
630
+ chainKind,
631
+ tokens: groupTokens
632
+ }));
633
+ }
861
634
  function recentTokensInOrder(tokens, recentAssetIds) {
862
635
  const byId = new Map(tokens.map((token) => [token.assetId, token]));
863
636
  const result = [];
@@ -870,109 +643,86 @@ function recentTokensInOrder(tokens, recentAssetIds) {
870
643
 
871
644
  // src/token-select/chain-pane.tsx
872
645
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
873
- function lockReason(lockChainKind) {
874
- return `Recipient address is on ${chainKindLabel(lockChainKind)}; edit the recipient to change chain`;
646
+ function lockReason(lockChainKind, labels) {
647
+ return `Recipient address is on ${chainKindLabel(lockChainKind, labels)}; edit the recipient to change chain`;
875
648
  }
876
649
  function ChainPane(props) {
877
650
  const {
878
651
  chains,
879
- fundedBlockchains,
880
652
  showWalletStatus = true,
881
- onSelectFilter,
653
+ onSelectChain,
882
654
  lockChainKind = null,
883
655
  disabledBlockchains = null,
884
- disabledReason
656
+ disabledReason,
657
+ scrollRef
885
658
  } = props;
886
- const [evmExpanded, setEvmExpanded] = useState(true);
659
+ const { chainKindLabels } = usePayWidgets();
887
660
  function wrapDisabled(disabled, reason, content) {
888
661
  if (!disabled || !reason) return content;
889
662
  return /* @__PURE__ */ jsx3(Tooltip, { side: "right", triggerClassName: "min-w-0 flex-1", content: reason, children: content });
890
663
  }
891
- function chainRow(chain, nested) {
892
- const locked = isChainKindLocked(chain.chainKind, lockChainKind);
893
- const chainDisabled = isBlockchainDisabled(chain.blockchain, disabledBlockchains);
894
- const disabled = locked || chainDisabled;
895
- const reason = chainDisabled ? disabledReason : locked && lockChainKind ? lockReason(lockChainKind) : void 0;
896
- return /* @__PURE__ */ jsxs2(
897
- "div",
898
- {
899
- className: cx(
900
- "flex min-h-14 w-full items-center gap-2 rounded-[12px] px-2 md:min-h-[66px] md:gap-3 md:px-3",
901
- disabled ? "opacity-40" : "hover:bg-[#F6F6F6]",
902
- nested ? "min-h-12 md:min-h-14" : ""
903
- ),
904
- children: [
905
- wrapDisabled(
906
- disabled,
907
- reason,
908
- /* @__PURE__ */ jsxs2(
664
+ return /* @__PURE__ */ jsx3("div", { className: "flex h-full min-h-0 flex-col", children: /* @__PURE__ */ jsx3("div", { ref: scrollRef, className: cx("min-h-0 flex-1 overflow-y-auto", thinScrollbar), children: /* @__PURE__ */ jsxs2("div", { className: "px-2.5 pt-[17px] pb-3", children: [
665
+ /* @__PURE__ */ jsx3("p", { className: cx("px-3.5 text-sm font-medium text-[#aaa]", fontSans), children: "All Networks" }),
666
+ /* @__PURE__ */ jsx3("div", { className: "mt-1", children: chains.map((chain) => {
667
+ const locked = isChainKindLocked(chain.chainKind, lockChainKind);
668
+ const chainDisabled = isBlockchainDisabled(chain.blockchain, disabledBlockchains);
669
+ const disabled = locked || chainDisabled;
670
+ const reason = chainDisabled ? disabledReason : locked && lockChainKind ? lockReason(lockChainKind, chainKindLabels) : void 0;
671
+ return /* @__PURE__ */ jsxs2(
672
+ "div",
673
+ {
674
+ className: cx(
675
+ "flex h-[66px] w-full items-center gap-4 rounded-[12px] px-3.5",
676
+ disabled ? "opacity-40" : "hover:bg-[#f6f6f6]"
677
+ ),
678
+ children: [
679
+ wrapDisabled(
680
+ disabled,
681
+ reason,
682
+ /* @__PURE__ */ jsxs2(
683
+ "button",
684
+ {
685
+ type: "button",
686
+ disabled,
687
+ onClick: () => {
688
+ if (disabled) return;
689
+ onSelectChain(chain.blockchain);
690
+ },
691
+ className: cx(
692
+ "flex min-w-0 flex-1 items-center gap-3 border-0 bg-transparent text-left",
693
+ disabled ? "cursor-not-allowed" : "cursor-pointer"
694
+ ),
695
+ children: [
696
+ /* @__PURE__ */ jsx3("img", { src: chain.logo, alt: "", className: "size-8 shrink-0 object-cover" }),
697
+ /* @__PURE__ */ jsx3("span", { className: cx("truncate text-sm font-medium text-black", fontSans), children: chain.chainName })
698
+ ]
699
+ }
700
+ )
701
+ ),
702
+ showWalletStatus ? /* @__PURE__ */ jsx3(ChainWalletStatus, { kind: chain.chainKind }) : /* @__PURE__ */ jsx3("span", { className: "min-w-0 flex-1" }),
703
+ /* @__PURE__ */ jsx3(
909
704
  "button",
910
705
  {
911
706
  type: "button",
707
+ "aria-label": `Open ${chain.chainName}`,
912
708
  disabled,
913
709
  onClick: () => {
914
710
  if (disabled) return;
915
- onSelectFilter(chain.blockchain);
711
+ onSelectChain(chain.blockchain);
916
712
  },
917
713
  className: cx(
918
- "flex min-w-0 flex-1 items-center gap-3 border-0 bg-transparent text-left",
714
+ "inline-flex shrink-0 border-0 bg-transparent p-0 text-[#606060]",
919
715
  disabled ? "cursor-not-allowed" : "cursor-pointer"
920
716
  ),
921
- children: [
922
- /* @__PURE__ */ jsxs2("span", { className: "relative size-8 shrink-0", children: [
923
- /* @__PURE__ */ jsx3("img", { src: chain.logo, alt: "", className: "size-8 object-cover" }),
924
- fundedBlockchains.has(chain.blockchain) ? /* @__PURE__ */ jsx3("span", { className: "absolute -top-0.5 -right-0.5 size-2 rounded-full border border-white bg-[#06f] md:size-2.5" }) : null
925
- ] }),
926
- /* @__PURE__ */ jsx3("span", { className: cx("truncate text-sm font-medium text-black md:text-base", fontSans), children: chain.chainName })
927
- ]
717
+ children: /* @__PURE__ */ jsx3(IconChevron, {})
928
718
  }
929
719
  )
930
- ),
931
- showWalletStatus && !nested ? /* @__PURE__ */ jsx3(ChainWalletStatus, { kind: chain.chainKind }) : null
932
- ]
933
- },
934
- chain.blockchain
935
- );
936
- }
937
- return /* @__PURE__ */ jsxs2("div", { className: "flex min-h-0 flex-1 flex-col", children: [
938
- /* @__PURE__ */ jsx3("p", { className: cx("shrink-0 text-sm font-medium text-[#aaa]", fontSans), children: "All Networks" }),
939
- /* @__PURE__ */ jsx3("div", { className: "mt-3 flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto", children: groupEvmChains(chains).map((entry) => {
940
- if (entry.kind === "chain") return chainRow(entry.chain, false);
941
- return /* @__PURE__ */ jsxs2("div", { className: "rounded-[12px] px-2 md:px-3 py-2 hover:bg-[rgba(185,215,255,0.1)]", children: [
942
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between", children: [
943
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-3", children: [
944
- /* @__PURE__ */ jsx3("span", { className: "flex size-8 shrink-0 items-center justify-center rounded-[10px] bg-[#627EEA]", children: /* @__PURE__ */ jsx3("img", { src: evmTypeLogoUrl(), alt: "", className: "size-5 object-contain" }) }),
945
- /* @__PURE__ */ jsx3("span", { className: cx("truncate text-sm font-medium text-black md:text-base", fontSans), children: "EVM-based" })
946
- ] }),
947
- showWalletStatus ? /* @__PURE__ */ jsx3(ChainWalletStatus, { kind: "evm" }) : /* @__PURE__ */ jsx3("span", { className: "min-w-0 flex-1" })
948
- ] }),
949
- /* @__PURE__ */ jsxs2("div", { className: "mt-2 flex items-center gap-2", children: [
950
- evmExpanded ? /* @__PURE__ */ jsx3("span", { className: "min-w-0 flex-1" }) : /* @__PURE__ */ jsx3("span", { className: "flex min-w-0 flex-1 overflow-hidden", children: entry.chains.map((chain, index) => /* @__PURE__ */ jsx3(
951
- "img",
952
- {
953
- src: chain.logo,
954
- alt: "",
955
- style: { zIndex: entry.chains.length - index },
956
- className: index === 0 ? "relative size-4 shrink-0 rounded-[4px] border border-white object-cover" : "relative -ml-1.5 size-4 shrink-0 rounded-[4px] border border-white object-cover"
957
- },
958
- chain.blockchain
959
- )) }),
960
- /* @__PURE__ */ jsx3(
961
- "button",
962
- {
963
- type: "button",
964
- "aria-expanded": evmExpanded,
965
- "aria-label": evmExpanded ? "Collapse EVM-based" : "Expand EVM-based",
966
- onClick: () => setEvmExpanded((open) => !open),
967
- className: "ml-auto inline-flex shrink-0 cursor-pointer border-0 bg-transparent p-1 text-[#606060]",
968
- children: /* @__PURE__ */ jsx3(IconArrowDown, { className: cx("size-3 transition-transform", evmExpanded ? "rotate-180" : "") })
969
- }
970
- )
971
- ] }),
972
- evmExpanded ? /* @__PURE__ */ jsx3("div", { className: "mt-2 border-t border-[#e3e3e3] pt-1", children: entry.chains.map((chain) => chainRow(chain, true)) }) : null
973
- ] }, "evm");
720
+ ]
721
+ },
722
+ chain.blockchain
723
+ );
974
724
  }) })
975
- ] });
725
+ ] }) }) });
976
726
  }
977
727
 
978
728
  // src/token-select/network-rail.tsx
@@ -982,99 +732,184 @@ import { Tooltip as Tooltip2 } from "@stableflow/pay-ui/tooltip";
982
732
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
983
733
  function chipClass(selected, disabled) {
984
734
  return cx(
985
- "relative flex size-10 shrink-0 items-center justify-center rounded-[12px] border border-solid md:size-[50px]",
735
+ "relative flex size-[50px] shrink-0 items-center justify-center rounded-[12px] border border-solid",
986
736
  selected ? "border-[#06f] bg-[rgba(0,102,255,0.1)]" : "border-transparent bg-transparent",
987
737
  disabled ? "cursor-not-allowed opacity-40" : "cursor-pointer hover:bg-black/5"
988
738
  );
989
739
  }
990
740
  function NetworkRail(props) {
991
741
  const {
992
- view,
993
- chainFilter,
994
- chips,
995
- fundedBlockchains,
742
+ role,
996
743
  walletConnected,
997
- showAllLabel = false,
744
+ selection,
745
+ chains,
746
+ fundedBlockchains,
998
747
  lockChainKind = null,
999
748
  disabledBlockchains = null,
1000
749
  disabledReason,
1001
- onSelectFilter,
1002
- onOpenNetworks,
750
+ onSelectWallet,
751
+ onSelectAll,
752
+ onSelectNetworks,
753
+ onSelectChain,
1003
754
  className
1004
755
  } = props;
756
+ const { chainKindLabels } = usePayWidgets();
757
+ const payer = role === "payer";
758
+ const networksSelected = selection.kind === "networks";
759
+ const allSelected = selection.kind === "all";
760
+ const walletSelected = selection.kind === "wallet";
1005
761
  function wrapDisabled(disabled, reason, content) {
1006
762
  if (!disabled || !reason) return content;
1007
763
  return /* @__PURE__ */ jsx4(Tooltip2, { side: "right", content: reason, children: content });
1008
764
  }
1009
- return /* @__PURE__ */ jsxs3("div", { className: cx("flex h-full w-[52px] shrink-0 flex-col items-center border-r border-[#e3e3e3] pr-2 md:w-[72px] md:pr-3", className), children: [
1010
- showAllLabel ? /* @__PURE__ */ jsx4(
1011
- "button",
1012
- {
1013
- type: "button",
1014
- "aria-label": "All",
1015
- onClick: () => onSelectFilter(ALL_CHAIN_FILTER),
1016
- className: chipClass(view === "token" && chainFilter === ALL_CHAIN_FILTER, false),
1017
- children: /* @__PURE__ */ jsx4("span", { className: cx("text-xs font-medium text-black md:text-sm", fontSans), children: "All" })
1018
- }
1019
- ) : /* @__PURE__ */ jsx4(
1020
- "button",
1021
- {
1022
- type: "button",
1023
- "aria-label": "Wallet",
1024
- disabled: !walletConnected,
1025
- onClick: () => {
1026
- if (!walletConnected) return;
1027
- onSelectFilter(ALL_CHAIN_FILTER);
1028
- },
1029
- className: walletConnected ? chipClass(view === "token" && chainFilter === ALL_CHAIN_FILTER, false) : "relative flex size-10 shrink-0 cursor-not-allowed items-center justify-center rounded-[12px] border border-solid border-transparent md:size-[50px]",
1030
- children: walletConnected ? /* @__PURE__ */ jsx4(IconWallet, { className: "size-4 text-black" }) : /* @__PURE__ */ jsx4("span", { className: "flex size-8 items-center justify-center rounded-[8px] bg-[#EFEFEF]", children: /* @__PURE__ */ jsx4(IconWallet, { className: "size-4 text-[#9FA7BA]" }) })
1031
- }
1032
- ),
1033
- /* @__PURE__ */ jsx4("div", { className: "mt-2 flex min-h-0 w-full flex-1 flex-col items-center gap-2 overflow-y-auto", children: chips.map((chain) => {
1034
- const locked = isChainKindLocked(chain.chainKind, lockChainKind);
1035
- const chainDisabled = isBlockchainDisabled(chain.blockchain, disabledBlockchains);
1036
- const disabled = locked || chainDisabled;
1037
- const reason = chainDisabled ? disabledReason : locked ? `Recipient address is on ${chainKindLabel(lockChainKind)}; edit the recipient to change chain` : void 0;
1038
- const selected = view === "token" && chainFilter === chain.blockchain;
1039
- return /* @__PURE__ */ jsx4("span", { children: wrapDisabled(
1040
- disabled,
1041
- reason,
1042
- /* @__PURE__ */ jsxs3(
1043
- "button",
1044
- {
1045
- type: "button",
1046
- "aria-label": chain.chainName,
1047
- disabled,
1048
- onClick: () => {
1049
- if (disabled) return;
1050
- onSelectFilter(chain.blockchain);
1051
- },
1052
- className: chipClass(selected, disabled),
1053
- children: [
1054
- /* @__PURE__ */ jsx4("img", { src: chain.logo, alt: "", className: "size-6 object-cover md:size-8" }),
1055
- fundedBlockchains.has(chain.blockchain) ? /* @__PURE__ */ jsx4("span", { className: "absolute top-1 right-1 size-2 rounded-full border border-white bg-[#06f] md:size-2.5" }) : null
1056
- ]
1057
- }
1058
- )
1059
- ) }, chain.blockchain);
1060
- }) }),
1061
- /* @__PURE__ */ jsx4("div", { className: "mt-2 flex w-full shrink-0 justify-center border-t border-[#e3e3e3] pt-2", children: /* @__PURE__ */ jsx4(
1062
- "button",
1063
- {
1064
- type: "button",
1065
- "aria-label": "All networks",
1066
- onClick: onOpenNetworks,
1067
- className: chipClass(view === "network", false),
1068
- children: /* @__PURE__ */ jsx4(IconAllNetworks, { className: "size-4 text-black" })
1069
- }
1070
- ) })
765
+ function chainButton(chain) {
766
+ const locked = isChainKindLocked(chain.chainKind, lockChainKind);
767
+ const chainDisabled = isBlockchainDisabled(chain.blockchain, disabledBlockchains);
768
+ const disabled = locked || chainDisabled;
769
+ const reason = chainDisabled ? disabledReason : locked ? `Recipient address is on ${chainKindLabel(lockChainKind, chainKindLabels)}; edit the recipient to change chain` : void 0;
770
+ const selected = selection.kind === "chain" && selection.blockchain === chain.blockchain;
771
+ return /* @__PURE__ */ jsx4("span", { children: wrapDisabled(
772
+ disabled,
773
+ reason,
774
+ /* @__PURE__ */ jsxs3(
775
+ "button",
776
+ {
777
+ type: "button",
778
+ "aria-label": chain.chainName,
779
+ disabled,
780
+ onClick: () => {
781
+ if (disabled) return;
782
+ onSelectChain(chain.blockchain);
783
+ },
784
+ className: chipClass(selected, disabled),
785
+ children: [
786
+ /* @__PURE__ */ jsx4("img", { src: chain.logo, alt: "", className: "size-8 object-cover" }),
787
+ fundedBlockchains.has(chain.blockchain) ? /* @__PURE__ */ jsx4("span", { className: "absolute top-1 right-1 size-2.5 rounded-full border border-white bg-[#06f]" }) : null
788
+ ]
789
+ }
790
+ )
791
+ ) }, chain.blockchain);
792
+ }
793
+ return /* @__PURE__ */ jsxs3("div", { className: cx("box-border flex h-full w-[76px] shrink-0 flex-col items-center border-r border-[#e3e3e3] pt-3", className), children: [
794
+ payer && walletConnected ? /* @__PURE__ */ jsx4("button", { type: "button", "aria-label": "Wallet", onClick: onSelectWallet, className: chipClass(walletSelected, false), children: /* @__PURE__ */ jsx4(IconWallet, { className: walletSelected ? "size-5 text-[#06f]" : "size-5 text-black" }) }) : null,
795
+ payer && !walletConnected ? /* @__PURE__ */ jsx4("button", { type: "button", "aria-label": "All networks", onClick: onSelectNetworks, className: chipClass(networksSelected, false), children: /* @__PURE__ */ jsx4(IconAllNetworks, { className: "size-5 text-black" }) }) : null,
796
+ !payer ? /* @__PURE__ */ jsx4("button", { type: "button", "aria-label": "All", onClick: onSelectAll, className: chipClass(allSelected, false), children: /* @__PURE__ */ jsx4("span", { className: cx("text-sm font-medium text-black", fontSans), children: "All" }) }) : null,
797
+ payer && walletConnected ? /* @__PURE__ */ jsxs3("div", { className: cx("mt-3.5 flex min-h-0 w-full flex-1 flex-col items-center gap-3.5 overflow-y-auto", thinScrollbar), children: [
798
+ chains.map((chain) => chainButton(chain)),
799
+ /* @__PURE__ */ jsx4("div", { className: "h-px w-8 shrink-0 bg-[#e3e3e3]" }),
800
+ /* @__PURE__ */ jsx4("button", { type: "button", "aria-label": "All networks", onClick: onSelectNetworks, className: chipClass(networksSelected, false), children: /* @__PURE__ */ jsx4(IconAllNetworks, { className: "size-5 text-black" }) }),
801
+ /* @__PURE__ */ jsx4("div", { className: "pb-3" })
802
+ ] }) : null,
803
+ !payer ? /* @__PURE__ */ jsxs3("div", { className: cx("mt-3.5 flex min-h-0 w-full flex-1 flex-col items-center gap-3.5 overflow-y-auto", thinScrollbar), children: [
804
+ chains.map((chain) => chainButton(chain)),
805
+ /* @__PURE__ */ jsx4("div", { className: "pb-3" })
806
+ ] }) : null
1071
807
  ] });
1072
808
  }
1073
809
 
810
+ // src/token-select/pane-transition.tsx
811
+ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
812
+ import { jsx as jsx5 } from "react/jsx-runtime";
813
+ var EASE = [0.32, 0.72, 0, 1];
814
+ var variants = {
815
+ enter: (direction) => ({ x: direction > 0 ? 24 : -24, opacity: 0 }),
816
+ center: { x: 0, opacity: 1 },
817
+ exit: (direction) => ({ x: direction > 0 ? -24 : 24, opacity: 0 })
818
+ };
819
+ function PaneTransition(props) {
820
+ const reduce = useReducedMotion();
821
+ if (reduce) {
822
+ return /* @__PURE__ */ jsx5("div", { className: "relative min-h-0 flex-1 overflow-hidden", children: props.children });
823
+ }
824
+ return /* @__PURE__ */ jsx5("div", { className: "relative min-h-0 flex-1 overflow-hidden", children: /* @__PURE__ */ jsx5(AnimatePresence, { initial: false, custom: props.direction, children: /* @__PURE__ */ jsx5(
825
+ motion.div,
826
+ {
827
+ custom: props.direction,
828
+ variants,
829
+ initial: "enter",
830
+ animate: "center",
831
+ exit: "exit",
832
+ transition: { duration: 0.22, ease: EASE },
833
+ className: "absolute inset-0 flex min-h-0 flex-col",
834
+ children: props.children
835
+ },
836
+ props.paneKey
837
+ ) }) });
838
+ }
839
+
840
+ // src/token-select/prefs.ts
841
+ import { useSyncExternalStore as useSyncExternalStore2 } from "react";
842
+ var EMPTY_PREFS = { recentAssetIds: [], recentBlockchains: [] };
843
+ function rememberRecentAsset(recent, assetId) {
844
+ const id = assetId.trim();
845
+ if (!id) return recent;
846
+ return [id, ...recent.filter((item) => item !== id)].slice(0, MAX_RECENT_ASSETS);
847
+ }
848
+ function rememberRecentBlockchain(recent, blockchain) {
849
+ const code = blockchain.trim();
850
+ if (!code) return recent;
851
+ return [code, ...recent.filter((item) => item !== code)].slice(0, MAX_RECENT_BLOCKCHAINS);
852
+ }
853
+ function migrateTokenSelectPrefs(persisted, version) {
854
+ const state = persisted ?? {};
855
+ const recentBlockchains = version >= 1 ? Array.isArray(state.recentBlockchains) ? state.recentBlockchains : [] : typeof state.lastBlockchain === "string" && state.lastBlockchain ? [state.lastBlockchain] : [];
856
+ const recentAssetIds = version >= TOKEN_SELECT_PREFS_VERSION && Array.isArray(state.recentAssetIds) ? state.recentAssetIds.filter((id) => typeof id === "string" && id.trim()).slice(0, MAX_RECENT_ASSETS) : typeof state.lastAssetId === "string" && state.lastAssetId ? [state.lastAssetId] : [];
857
+ return { recentAssetIds, recentBlockchains };
858
+ }
859
+ function readStorage() {
860
+ if (typeof localStorage === "undefined") return EMPTY_PREFS;
861
+ try {
862
+ const raw = localStorage.getItem(TOKEN_SELECT_PREFS_STORAGE_KEY);
863
+ if (!raw) return EMPTY_PREFS;
864
+ const parsed = JSON.parse(raw);
865
+ const version = typeof parsed.version === "number" ? parsed.version : 0;
866
+ const payload = parsed && typeof parsed === "object" && "state" in parsed ? parsed.state : parsed;
867
+ return migrateTokenSelectPrefs(payload, version);
868
+ } catch {
869
+ return EMPTY_PREFS;
870
+ }
871
+ }
872
+ function writeStorage(state) {
873
+ if (typeof localStorage === "undefined") return;
874
+ localStorage.setItem(TOKEN_SELECT_PREFS_STORAGE_KEY, JSON.stringify({
875
+ state,
876
+ version: TOKEN_SELECT_PREFS_VERSION
877
+ }));
878
+ }
879
+ var prefs = readStorage();
880
+ var listeners2 = /* @__PURE__ */ new Set();
881
+ function emit2(next) {
882
+ prefs = next;
883
+ writeStorage(next);
884
+ for (const listener of listeners2) listener();
885
+ }
886
+ function subscribe2(listener) {
887
+ listeners2.add(listener);
888
+ return () => listeners2.delete(listener);
889
+ }
890
+ function getTokenSelectPrefs() {
891
+ return prefs;
892
+ }
893
+ function setLastToken(assetId, blockchain) {
894
+ emit2({
895
+ recentAssetIds: rememberRecentAsset(prefs.recentAssetIds, assetId),
896
+ recentBlockchains: rememberRecentBlockchain(prefs.recentBlockchains, blockchain)
897
+ });
898
+ }
899
+ function setLastBlockchain(blockchain) {
900
+ emit2({
901
+ ...prefs,
902
+ recentBlockchains: rememberRecentBlockchain(prefs.recentBlockchains, blockchain)
903
+ });
904
+ }
905
+ function useTokenSelectPrefs() {
906
+ return useSyncExternalStore2(subscribe2, getTokenSelectPrefs, getTokenSelectPrefs);
907
+ }
908
+
1074
909
  // src/token-select/token-pane.tsx
1075
- import { SearchInput } from "@stableflow/pay-ui/search-input";
910
+ import { IconChevron as IconChevron2 } from "@stableflow/pay-ui/icons/chevron";
1076
911
  import { Skeleton } from "@stableflow/pay-ui/skeleton";
1077
- import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
912
+ import { Fragment, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1078
913
  function TokenPane(props) {
1079
914
  const {
1080
915
  sections,
@@ -1082,65 +917,67 @@ function TokenPane(props) {
1082
917
  loading,
1083
918
  showBalances = false,
1084
919
  showWalletStatus = true,
920
+ header = null,
1085
921
  getBalance,
1086
922
  isBalanceLoading,
1087
923
  onSelectToken,
1088
924
  isTokenDisabled,
1089
- search = "",
1090
- onSearchChange,
1091
- showSearch = false,
1092
- searchClassName,
1093
925
  rowClassName
1094
926
  } = props;
1095
927
  const visibleSections = sections.filter((section) => section.title || section.tokens.length > 0);
1096
928
  const hasTokens = sections.some((section) => section.tokens.length > 0);
1097
- return /* @__PURE__ */ jsxs4("div", { className: "flex min-h-0 flex-1 flex-col", children: [
1098
- showSearch && onSearchChange ? /* @__PURE__ */ jsx5(
1099
- SearchInput,
929
+ return /* @__PURE__ */ jsx6("div", { className: "flex h-full min-h-0 flex-1 flex-col", children: /* @__PURE__ */ jsx6("div", { className: cx("min-h-0 flex-1 overflow-y-auto", thinScrollbar), children: /* @__PURE__ */ jsxs4("div", { className: "px-2.5 pt-[17px] pb-3", children: [
930
+ header ? /* @__PURE__ */ jsx6(ChainListHeader, { header, showWalletStatus }) : null,
931
+ visibleSections.map((section) => /* @__PURE__ */ jsxs4("section", { className: "mb-2", children: [
932
+ section.title ? /* @__PURE__ */ jsxs4("div", { className: "mb-1 flex items-center justify-between gap-2 px-3.5", children: [
933
+ /* @__PURE__ */ jsx6("p", { className: cx("min-w-0 truncate text-sm font-medium text-[#aaa]", fontSans), children: section.title }),
934
+ showWalletStatus && section.walletKind ? /* @__PURE__ */ jsx6(ChainWalletStatus, { kind: section.walletKind }) : null
935
+ ] }) : null,
936
+ section.tokens.map((token) => /* @__PURE__ */ jsx6(
937
+ TokenRow,
938
+ {
939
+ token,
940
+ selected: token.assetId === selectedAssetId,
941
+ recent: token.assetId === section.badgeAssetId,
942
+ showBalances,
943
+ formatted: showBalances ? getBalance(token) : null,
944
+ loadingBalance: showBalances && isBalanceLoading(token),
945
+ disabled: Boolean(isTokenDisabled?.(token)),
946
+ onSelect: () => onSelectToken(token),
947
+ className: rowClassName
948
+ },
949
+ `${section.id}-${token.assetId}`
950
+ ))
951
+ ] }, section.id)),
952
+ loading && !hasTokens ? /* @__PURE__ */ jsx6(TokenListSkeleton, {}) : null,
953
+ !loading && !hasTokens ? /* @__PURE__ */ jsx6("p", { className: cx("px-3.5 py-4 text-[13px] text-[#606060]", fontSans), children: "No tokens found" }) : null
954
+ ] }) }) });
955
+ }
956
+ function ChainListHeader(props) {
957
+ const { header, showWalletStatus } = props;
958
+ const walletKind = header.walletKind;
959
+ return /* @__PURE__ */ jsxs4("div", { className: "mb-1 flex items-center justify-between gap-3 px-3.5", children: [
960
+ header.kind === "back" ? /* @__PURE__ */ jsxs4(
961
+ "button",
1100
962
  {
1101
- value: search,
1102
- onChange: onSearchChange,
1103
- placeholder: "search or paste address",
1104
- className: "mb-3 shrink-0",
1105
- inputClassName: cx(
1106
- "h-[36px] rounded-[18px] border-[#e3e3e3] bg-[#f6f6f6] text-sm placeholder:text-black/30",
1107
- searchClassName
1108
- )
963
+ type: "button",
964
+ onClick: header.onBack,
965
+ className: cx("flex min-w-0 cursor-pointer items-center gap-3 border-0 bg-transparent p-0 text-sm font-medium text-[#aaa]", fontSans),
966
+ children: [
967
+ /* @__PURE__ */ jsx6(IconChevron2, { className: "rotate-180 text-[#aaa]" }),
968
+ /* @__PURE__ */ jsx6("span", { className: "truncate", children: header.title })
969
+ ]
1109
970
  }
1110
- ) : null,
1111
- /* @__PURE__ */ jsxs4("div", { className: "flex min-h-0 flex-1 flex-col overflow-y-auto", children: [
1112
- visibleSections.map((section) => /* @__PURE__ */ jsxs4("section", { className: "mb-2", children: [
1113
- section.title ? /* @__PURE__ */ jsxs4("div", { className: "mb-1 flex items-center justify-between gap-2 px-2 md:px-3.5", children: [
1114
- /* @__PURE__ */ jsx5("p", { className: cx("min-w-0 truncate text-sm font-medium text-[#aaa]", fontSans), children: section.title }),
1115
- showWalletStatus && section.walletKind ? /* @__PURE__ */ jsx5(ChainWalletStatus, { kind: section.walletKind }) : null
1116
- ] }) : null,
1117
- section.tokens.map((token) => /* @__PURE__ */ jsx5(
1118
- TokenRow,
1119
- {
1120
- token,
1121
- selected: token.assetId === selectedAssetId,
1122
- recent: token.assetId === section.badgeAssetId,
1123
- showBalances,
1124
- formatted: showBalances ? getBalance(token) : null,
1125
- loadingBalance: showBalances && isBalanceLoading(token),
1126
- disabled: Boolean(isTokenDisabled?.(token)),
1127
- onSelect: () => onSelectToken(token),
1128
- className: rowClassName
1129
- },
1130
- `${section.id}-${token.assetId}`
1131
- ))
1132
- ] }, section.id)),
1133
- loading && !hasTokens ? /* @__PURE__ */ jsx5(TokenListSkeleton, {}) : null,
1134
- !loading && !hasTokens ? /* @__PURE__ */ jsx5("p", { className: cx("px-1 py-4 text-[13px] text-[#606060]", fontSans), children: "No tokens found" }) : null
1135
- ] })
971
+ ) : /* @__PURE__ */ jsx6("p", { className: cx("min-w-0 truncate text-sm font-medium text-[#aaa]", fontSans), children: header.title }),
972
+ showWalletStatus && walletKind ? /* @__PURE__ */ jsx6(ChainWalletStatus, { kind: walletKind }) : null
1136
973
  ] });
1137
974
  }
1138
975
  function TokenListSkeleton() {
1139
- return /* @__PURE__ */ jsx5("div", { "aria-hidden": true, children: Array.from({ length: 6 }, (_, index) => /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2.5 px-2 py-3 md:px-3.5", children: [
1140
- /* @__PURE__ */ jsx5(Skeleton, { className: "size-8 shrink-0 rounded-full" }),
976
+ return /* @__PURE__ */ jsx6("div", { "aria-hidden": true, children: Array.from({ length: 6 }, (_, index) => /* @__PURE__ */ jsxs4("div", { className: "flex h-[66px] items-center gap-2.5 px-3.5", children: [
977
+ /* @__PURE__ */ jsx6(Skeleton, { className: "size-8 shrink-0 rounded-full" }),
1141
978
  /* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [
1142
- /* @__PURE__ */ jsx5(Skeleton, { className: "h-3.5 w-24" }),
1143
- /* @__PURE__ */ jsx5(Skeleton, { className: "h-3 w-16" })
979
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-3.5 w-24" }),
980
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-3 w-16" })
1144
981
  ] })
1145
982
  ] }, index)) });
1146
983
  }
@@ -1154,16 +991,16 @@ function TokenRow(props) {
1154
991
  disabled,
1155
992
  onClick: onSelect,
1156
993
  className: cx(
1157
- "flex w-full items-center justify-between gap-2 rounded-[12px] border-0 bg-transparent px-2 py-3 text-left hover:bg-[#F6F6F6] md:px-3.5",
1158
- selected && "bg-[#F6F6F6]",
994
+ "flex h-[66px] w-full cursor-pointer items-center justify-between gap-2 rounded-[12px] border-0 bg-transparent px-3.5 text-left hover:bg-[#f6f6f6]",
995
+ selected && "bg-[#f6f6f6]",
1159
996
  disabled && "cursor-not-allowed opacity-40 hover:bg-transparent",
1160
997
  className
1161
998
  ),
1162
999
  children: [
1163
1000
  /* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 items-center gap-2.5", children: [
1164
1001
  /* @__PURE__ */ jsxs4("span", { className: "relative size-8 shrink-0", children: [
1165
- /* @__PURE__ */ jsx5("img", { src: token.logo, alt: "", className: "size-8 rounded-full object-cover" }),
1166
- /* @__PURE__ */ jsx5(
1002
+ /* @__PURE__ */ jsx6("img", { src: token.logo, alt: "", className: "size-8 rounded-[12px] object-cover" }),
1003
+ /* @__PURE__ */ jsx6(
1167
1004
  "img",
1168
1005
  {
1169
1006
  src: token.chain.logo,
@@ -1174,37 +1011,78 @@ function TokenRow(props) {
1174
1011
  ] }),
1175
1012
  /* @__PURE__ */ jsxs4("span", { className: "min-w-0", children: [
1176
1013
  /* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 items-center gap-1.5", children: [
1177
- /* @__PURE__ */ jsx5("span", { className: cx("truncate text-sm font-semibold text-black", fontSans), children: token.symbol }),
1178
- recent ? /* @__PURE__ */ jsx5("span", { className: cx("shrink-0 rounded-[9px] bg-[#06f]/10 px-1.5 text-[10px] font-medium leading-[18px] text-[#06f]", fontSans), children: "Recently Used" }) : null
1014
+ /* @__PURE__ */ jsx6("span", { className: cx("truncate text-sm font-semibold text-black", fontSans), children: token.symbol }),
1015
+ recent ? /* @__PURE__ */ jsx6("span", { className: cx("shrink-0 rounded-[9px] bg-[#06f]/10 px-1.5 text-[10px] font-medium leading-[18px] text-[#06f]", fontSans), children: "Recently Used" }) : null
1179
1016
  ] }),
1180
- /* @__PURE__ */ jsx5("span", { className: cx("mt-0.5 block truncate text-xs text-[#606060]", fontSans), children: token.chain.chainName })
1017
+ /* @__PURE__ */ jsx6("span", { className: cx("mt-0.5 block truncate text-xs text-[#606060]", fontSans), children: token.chain.chainName })
1181
1018
  ] })
1182
1019
  ] }),
1183
- showBalances ? /* @__PURE__ */ jsx5("span", { className: "shrink-0 text-right", children: loadingBalance ? /* @__PURE__ */ jsx5(
1020
+ showBalances ? /* @__PURE__ */ jsx6("span", { className: "shrink-0 text-right", children: loadingBalance ? /* @__PURE__ */ jsx6(
1184
1021
  "span",
1185
1022
  {
1186
1023
  className: "inline-block size-3.5 animate-spin rounded-full border-2 border-[#606060] border-r-transparent",
1187
1024
  "aria-label": "Loading balance"
1188
1025
  }
1189
1026
  ) : formatted != null ? /* @__PURE__ */ jsxs4(Fragment, { children: [
1190
- /* @__PURE__ */ jsx5("span", { className: cx("block text-sm font-medium text-black", fontSans), children: formatDisplayAmount(formatted, { prefix: "", maxDecimals: 4 }) }),
1191
- usd >= 0 ? /* @__PURE__ */ jsx5("span", { className: cx("block text-xs text-[#606060]", fontSans), children: formatDisplayAmount(usd, { prefix: "$", showDust: true }) }) : null
1192
- ] }) : /* @__PURE__ */ jsx5("span", { className: cx("text-sm text-[#606060]", fontSans), children: "\u2014" }) }) : null
1027
+ /* @__PURE__ */ jsx6("span", { className: cx("block text-sm font-medium text-black", fontSans), children: formatDisplayAmount(formatted, { prefix: "", maxDecimals: 4 }) }),
1028
+ usd >= 0 ? /* @__PURE__ */ jsx6("span", { className: cx("block text-xs text-[#606060]", fontSans), children: formatDisplayAmount(usd, { prefix: "$", showDust: true }) }) : null
1029
+ ] }) : /* @__PURE__ */ jsx6("span", { className: cx("text-sm font-medium text-black", fontSans), children: "-" }) }) : null
1193
1030
  ]
1194
1031
  }
1195
1032
  );
1196
1033
  }
1197
1034
 
1035
+ // src/token-select/search-results.tsx
1036
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1037
+ function SearchResults(props) {
1038
+ const {
1039
+ groups,
1040
+ selectedAssetId,
1041
+ loading,
1042
+ showBalances = false,
1043
+ showWalletStatus = true,
1044
+ getBalance,
1045
+ isBalanceLoading,
1046
+ onSelectToken,
1047
+ isTokenDisabled,
1048
+ rowClassName
1049
+ } = props;
1050
+ const hasTokens = groups.some((group) => group.tokens.length > 0);
1051
+ return /* @__PURE__ */ jsx7("div", { className: "flex h-full min-h-0 flex-1 flex-col", children: /* @__PURE__ */ jsx7("div", { className: cx("min-h-0 flex-1 overflow-y-auto", thinScrollbar), children: /* @__PURE__ */ jsxs5("div", { className: "px-2.5 pt-[17px] pb-3", children: [
1052
+ groups.map((group) => /* @__PURE__ */ jsxs5("section", { className: "mb-2", children: [
1053
+ /* @__PURE__ */ jsxs5("div", { className: "mb-1 flex items-center justify-between gap-2 px-3.5", children: [
1054
+ /* @__PURE__ */ jsx7("p", { className: cx("min-w-0 truncate text-sm font-medium text-[#aaa]", fontSans), children: group.chainName }),
1055
+ showWalletStatus ? /* @__PURE__ */ jsx7(ChainWalletStatus, { kind: group.chainKind }) : null
1056
+ ] }),
1057
+ group.tokens.map((token) => /* @__PURE__ */ jsx7(
1058
+ TokenRow,
1059
+ {
1060
+ token,
1061
+ selected: token.assetId === selectedAssetId,
1062
+ recent: false,
1063
+ showBalances,
1064
+ formatted: showBalances ? getBalance(token) : null,
1065
+ loadingBalance: showBalances && isBalanceLoading(token),
1066
+ disabled: Boolean(isTokenDisabled?.(token)),
1067
+ onSelect: () => onSelectToken(token),
1068
+ className: rowClassName
1069
+ },
1070
+ token.assetId
1071
+ ))
1072
+ ] }, group.blockchain)),
1073
+ !loading && !hasTokens ? /* @__PURE__ */ jsx7("p", { className: cx("px-3.5 py-4 text-[13px] text-[#606060]", fontSans), children: "No tokens found" }) : null
1074
+ ] }) }) });
1075
+ }
1076
+
1077
+ // src/token-select/types.ts
1078
+ var KNOWN_CHAIN_KINDS = ["evm", "near", "solana", "tron", "zec"];
1079
+ function ownerKey2(owners) {
1080
+ return Object.keys(owners).sort().map((kind) => `${kind}:${owners[kind] ?? ""}`).join("|");
1081
+ }
1082
+
1198
1083
  // src/token-select/TokenSelectDialog.tsx
1199
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1084
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
1200
1085
  var SEARCH_INPUT_CLASS = "h-[36px] rounded-[18px] border-[#e3e3e3] bg-[#f6f6f6] text-sm placeholder:text-black/30";
1201
- function chainsFromTokens(tokens) {
1202
- const byCode = /* @__PURE__ */ new Map();
1203
- for (const token of tokens) {
1204
- if (!byCode.has(token.blockchain)) byCode.set(token.blockchain, token.chain);
1205
- }
1206
- return Array.from(byCode.values());
1207
- }
1208
1086
  function ownerForToken(owners, token) {
1209
1087
  return owners[token.chain.chainKind] ?? null;
1210
1088
  }
@@ -1215,6 +1093,15 @@ function isNativeToken(token) {
1215
1093
  const addr = String(token.contractAddress || "").trim().toLowerCase();
1216
1094
  return !addr || addr === "native" || addr === "0x0000000000000000000000000000000000000000";
1217
1095
  }
1096
+ function railSelection(view) {
1097
+ if (view.kind === "wallet") return { kind: "wallet" };
1098
+ if (view.kind === "all") return { kind: "all" };
1099
+ if (view.kind === "rail-chain") return { kind: "chain", blockchain: view.blockchain };
1100
+ return { kind: "networks" };
1101
+ }
1102
+ function chainByCode(chains, blockchain) {
1103
+ return chains.find((chain) => chain.blockchain === blockchain);
1104
+ }
1218
1105
  function TokenSelectDialog(props) {
1219
1106
  const {
1220
1107
  open,
@@ -1231,6 +1118,7 @@ function TokenSelectDialog(props) {
1231
1118
  disabledReason,
1232
1119
  className,
1233
1120
  titleClassName,
1121
+ headerClassName,
1234
1122
  closeClassName,
1235
1123
  contentClassName,
1236
1124
  searchClassName,
@@ -1239,11 +1127,19 @@ function TokenSelectDialog(props) {
1239
1127
  } = props;
1240
1128
  const payer = role === "payer";
1241
1129
  const showBalances = payer;
1242
- const { wallet, popularTokens: popularRefs } = usePayWidgets();
1243
- const { tokens, status } = usePayConfigSnapshot();
1130
+ const {
1131
+ wallet,
1132
+ popularTokens: popularRefs,
1133
+ tokens,
1134
+ loading,
1135
+ stableSymbols,
1136
+ balanceMode,
1137
+ getBalance: syncGetBalance,
1138
+ isBalanceLoading: syncIsBalanceLoading
1139
+ } = usePayWidgets();
1244
1140
  const prefs2 = useTokenSelectPrefs();
1245
- const balanceEntries = useSyncExternalStore4(subscribePayTokenBalances, getPayTokenBalances, getPayTokenBalances);
1246
- const connectedOwners = useMemo2(() => {
1141
+ const balanceEntries = useSyncExternalStore3(subscribePayTokenBalances, getPayTokenBalances, getPayTokenBalances);
1142
+ const connectedOwners = useMemo(() => {
1247
1143
  const owners2 = {};
1248
1144
  for (const [kind, account] of Object.entries(wallet.accounts)) {
1249
1145
  const address = account?.address?.trim();
@@ -1251,53 +1147,57 @@ function TokenSelectDialog(props) {
1251
1147
  }
1252
1148
  return owners2;
1253
1149
  }, [wallet.accounts]);
1254
- const owners = showBalances ? { ...connectedOwners, ...balanceOwners } : {};
1150
+ const owners = useMemo(
1151
+ () => showBalances ? { ...connectedOwners, ...balanceOwners } : {},
1152
+ [showBalances, connectedOwners, balanceOwners]
1153
+ );
1255
1154
  const walletConnected = hasAnyOwner(connectedOwners);
1256
- const walletWasConnected = useRef(null);
1257
- const [search, setSearch] = useState2("");
1258
- const [chainFilter, setChainFilter] = useState2(ALL_CHAIN_FILTER);
1259
- const [view, setView] = useState2("token");
1260
- const loading = status === "loading" || status === "idle";
1261
- useEffect3(() => {
1155
+ const [search, setSearch] = useState("");
1156
+ const [view, setView] = useState(payer ? { kind: "networks" } : { kind: "all" });
1157
+ const [slideDirection, setSlideDirection] = useState(1);
1158
+ const networksScrollRef = useRef(null);
1159
+ const savedNetworksScroll = useRef(0);
1160
+ const walletConnectedRef = useRef(walletConnected);
1161
+ const payerRef = useRef(payer);
1162
+ const lockRef = useRef(lockChainKind);
1163
+ const tokensRef = useRef(tokens);
1164
+ walletConnectedRef.current = walletConnected;
1165
+ payerRef.current = payer;
1166
+ lockRef.current = lockChainKind;
1167
+ tokensRef.current = tokens;
1168
+ useEffect2(() => {
1262
1169
  if (!open) return;
1263
1170
  setSearch("");
1264
- if (lockChainKind) {
1265
- setView("token");
1266
- return;
1267
- }
1268
- if (!payer) {
1269
- setView("token");
1270
- setChainFilter(ALL_CHAIN_FILTER);
1271
- }
1272
- }, [open, lockChainKind, payer]);
1273
- useEffect3(() => {
1274
- if (!open || !lockChainKind || tokens.length === 0) return;
1275
- setChainFilter(initialChainFilter(
1276
- lockChainKind,
1277
- getTokenSelectPrefs().recentBlockchains,
1278
- chainsFromTokens(tokens)
1279
- ));
1280
- }, [open, lockChainKind, tokens]);
1281
- useEffect3(() => {
1282
- if (!open) {
1283
- walletWasConnected.current = null;
1171
+ const lock = lockRef.current;
1172
+ if (lock) {
1173
+ const filter = initialChainFilter(
1174
+ lock,
1175
+ getTokenSelectPrefs().recentBlockchains,
1176
+ chainsFromTokens(tokensRef.current)
1177
+ );
1178
+ if (filter !== ALL_CHAIN_FILTER) {
1179
+ setView({ kind: "rail-chain", blockchain: filter });
1180
+ return;
1181
+ }
1284
1182
  return;
1285
1183
  }
1286
- if (lockChainKind || !payer) {
1287
- walletWasConnected.current = walletConnected;
1184
+ if (!payerRef.current) {
1185
+ setView({ kind: "all" });
1288
1186
  return;
1289
1187
  }
1290
- const previous = walletWasConnected.current;
1291
- walletWasConnected.current = walletConnected;
1292
- if (previous !== null && previous === walletConnected) return;
1293
- setView(walletConnected ? "token" : "network");
1294
- setChainFilter(ALL_CHAIN_FILTER);
1295
- }, [open, lockChainKind, payer, walletConnected]);
1296
- const allowed = useMemo2(() => {
1188
+ setView(walletConnectedRef.current ? { kind: "wallet" } : { kind: "networks" });
1189
+ }, [open]);
1190
+ useEffect2(() => {
1191
+ if (!open || !lockChainKind) return;
1192
+ const filter = initialChainFilter(lockChainKind, getTokenSelectPrefs().recentBlockchains, chainsFromTokens(tokens));
1193
+ if (filter === ALL_CHAIN_FILTER) return;
1194
+ setView((current) => current.kind === "rail-chain" ? current : { kind: "rail-chain", blockchain: filter });
1195
+ }, [open, lockChainKind, tokens]);
1196
+ const allowed = useMemo(() => {
1297
1197
  if (!allowedBlockchains || allowedBlockchains.length === 0) return null;
1298
1198
  return new Set(allowedBlockchains.map((code) => code.toLowerCase()));
1299
1199
  }, [allowedBlockchains]);
1300
- const scopedTokens = useMemo2(() => {
1200
+ const scopedTokens = useMemo(() => {
1301
1201
  return tokens.filter((token) => {
1302
1202
  if (allowed && !allowed.has(token.blockchain.toLowerCase())) return false;
1303
1203
  if (excludeNative && isNativeToken(token)) return false;
@@ -1306,22 +1206,25 @@ function TokenSelectDialog(props) {
1306
1206
  return true;
1307
1207
  });
1308
1208
  }, [tokens, allowed, excludeNative, payer]);
1309
- const ownerKey2 = ["evm", "near", "solana", "tron", "zec"].map((kind) => `${kind}:${owners[kind] ?? ""}`).join("|");
1310
- const tokenKey = useMemo2(() => scopedTokens.map((token) => token.assetId).join("|"), [scopedTokens]);
1209
+ const ownersKey = ownerKey2(owners);
1210
+ const tokenKey = useMemo(() => scopedTokens.map((token) => token.assetId).join("|"), [scopedTokens]);
1311
1211
  const ownersRef = useRef(owners);
1312
- const tokensRef = useRef(scopedTokens);
1212
+ const scopedRef = useRef(scopedTokens);
1313
1213
  ownersRef.current = owners;
1314
- tokensRef.current = scopedTokens;
1315
- useEffect3(() => {
1316
- if (!showBalances || !open || !hasAnyOwner(ownersRef.current) || !tokenKey) return;
1317
- void ensurePayTokenBalances(ownersRef.current, tokensRef.current);
1214
+ scopedRef.current = scopedTokens;
1215
+ useEffect2(() => {
1216
+ if (balanceMode !== "async" || !showBalances || !open || !hasAnyOwner(ownersRef.current) || !tokenKey) return;
1217
+ void ensurePayTokenBalances(ownersRef.current, scopedRef.current);
1318
1218
  const id = window.setInterval(() => {
1319
- void ensurePayTokenBalances(ownersRef.current, tokensRef.current, { force: true });
1219
+ void ensurePayTokenBalances(ownersRef.current, scopedRef.current, { force: true });
1320
1220
  }, TOKEN_BALANCE_POLL_MS);
1321
1221
  return () => window.clearInterval(id);
1322
- }, [showBalances, open, ownerKey2, tokenKey]);
1222
+ }, [balanceMode, showBalances, open, ownersKey, tokenKey]);
1323
1223
  function balanceOf(token) {
1324
- return getPayTokenBalance(ownerForToken(owners, token), token.assetId)?.formatted;
1224
+ const owner = ownerForToken(owners, token);
1225
+ if (!showBalances || !owner) return void 0;
1226
+ if (balanceMode === "sync" && syncGetBalance) return syncGetBalance(token, owner) ?? void 0;
1227
+ return getPayTokenBalance(owner, token.assetId)?.formatted;
1325
1228
  }
1326
1229
  function usdOf(token) {
1327
1230
  if (!showBalances) return -1;
@@ -1331,52 +1234,48 @@ function TokenSelectDialog(props) {
1331
1234
  if (!showBalances) return false;
1332
1235
  const owner = ownerForToken(owners, token);
1333
1236
  if (!owner) return false;
1237
+ if (balanceMode === "sync") return syncIsBalanceLoading?.(token, owner) ?? false;
1334
1238
  const entry = getPayTokenBalance(owner, token.assetId);
1335
1239
  return entry?.formatted == null && (!entry || entry.status === "loading");
1336
1240
  }
1337
- const availableChains = useMemo2(() => {
1338
- const codes = new Set(scopedTokens.map((token) => token.blockchain));
1339
- const byCode = /* @__PURE__ */ new Map();
1340
- for (const token of scopedTokens) {
1341
- if (codes.has(token.blockchain) && !byCode.has(token.blockchain)) byCode.set(token.blockchain, token.chain);
1342
- }
1343
- return Array.from(byCode.values());
1344
- }, [scopedTokens]);
1345
- const sortedChains = useMemo2(
1346
- () => sortChainsForSidebar(availableChains, scopedTokens, {
1347
- showBalances,
1348
- walletConnected,
1349
- getBalanceUsd: (token) => tokenBalanceUsd(token, getPayTokenBalance(ownerForToken(owners, token), token.assetId)?.formatted)
1350
- }),
1351
- [availableChains, scopedTokens, showBalances, walletConnected, owners, balanceEntries]
1352
- );
1353
- const fundedBlockchains = useMemo2(() => {
1241
+ const availableChains = useMemo(() => chainsFromTokens(scopedTokens), [scopedTokens]);
1242
+ const fundedBlockchains = useMemo(() => {
1354
1243
  const funded = /* @__PURE__ */ new Set();
1355
1244
  if (!showBalances) return funded;
1356
1245
  for (const chain of availableChains) {
1357
- if (chainHasBalance(chain.blockchain, scopedTokens, (token) => getPayTokenBalance(ownerForToken(owners, token), token.assetId)?.formatted)) {
1246
+ if (chainHasBalance(chain.blockchain, scopedTokens, (token) => balanceOf(token))) {
1358
1247
  funded.add(chain.blockchain);
1359
1248
  }
1360
1249
  }
1361
1250
  return funded;
1362
- }, [availableChains, owners, scopedTokens, showBalances, balanceEntries]);
1363
- const popularTokens = useMemo2(
1251
+ }, [availableChains, owners, scopedTokens, showBalances, balanceEntries, balanceMode, syncGetBalance, syncIsBalanceLoading]);
1252
+ const railChains = useMemo(() => {
1253
+ if (!payer) return sortChainsAlpha(availableChains);
1254
+ const connected = payerRailChains(availableChains, owners, fundedBlockchains);
1255
+ return sortChainsForSidebar(connected, scopedTokens, {
1256
+ showBalances,
1257
+ getBalanceUsd: (token) => tokenBalanceUsd(token, balanceOf(token))
1258
+ });
1259
+ }, [payer, availableChains, owners, fundedBlockchains, scopedTokens, showBalances, balanceEntries, balanceMode, syncGetBalance]);
1260
+ const networkChains = useMemo(() => sortChainsAlpha(availableChains), [availableChains]);
1261
+ useEffect2(() => {
1262
+ if (!open || !payer || lockChainKind) return;
1263
+ setView((current) => {
1264
+ if (current.kind === "wallet" && !walletConnected) return { kind: "networks" };
1265
+ if (current.kind === "rail-chain" && !railChains.some((chain) => chain.blockchain === current.blockchain)) {
1266
+ return walletConnected ? { kind: "wallet" } : { kind: "networks" };
1267
+ }
1268
+ return current;
1269
+ });
1270
+ }, [open, payer, lockChainKind, walletConnected, railChains]);
1271
+ const popularTokens = useMemo(
1364
1272
  () => popularTokensForWallets(scopedTokens, popularRefs, (kind) => Boolean(connectedOwners[kind])),
1365
1273
  [scopedTokens, popularRefs, connectedOwners]
1366
1274
  );
1367
1275
  const newestAssetId = showBalances ? prefs2.recentAssetIds[0] ?? null : null;
1368
- const searchingAll = chainFilter === ALL_CHAIN_FILTER && search.trim().length > 0;
1369
- const walletKind = chainFilter === ALL_CHAIN_FILTER ? null : availableChains.find((chain) => chain.blockchain === chainFilter)?.chainKind ?? null;
1370
- const sections = useMemo2(() => {
1371
- if (showBalances && chainFilter === ALL_CHAIN_FILTER && searchingAll) {
1372
- const matched2 = scopedTokens.filter((token) => tokenMatchesSearch(token, search));
1373
- return [{
1374
- id: "search",
1375
- tokens: sortTokensByBalance(matched2, usdOf, loadingOf),
1376
- badgeAssetId: newestAssetId
1377
- }];
1378
- }
1379
- if (showBalances && chainFilter === ALL_CHAIN_FILTER) {
1276
+ const searching = search.trim().length > 0;
1277
+ const sections = useMemo(() => {
1278
+ if (view.kind === "wallet") {
1380
1279
  const next = [];
1381
1280
  const recent = recentTokensInOrder(scopedTokens, prefs2.recentAssetIds);
1382
1281
  if (recent.length > 0) {
@@ -1396,42 +1295,87 @@ function TokenSelectDialog(props) {
1396
1295
  }
1397
1296
  return next;
1398
1297
  }
1399
- const matched = scopedTokens.filter((token) => matchesChainFilter(token, chainFilter) && tokenMatchesSearch(token, search));
1400
- if (!showBalances && chainFilter === ALL_CHAIN_FILTER && !search.trim()) {
1298
+ if (view.kind === "all") {
1401
1299
  const next = [];
1402
1300
  if (popularTokens.length > 0) {
1403
1301
  next.push({ id: "popular", title: POPULAR_SECTION_TITLE, tokens: popularTokens });
1404
1302
  }
1405
- next.push({ id: "all", tokens: sortTokensBySymbol(matched) });
1303
+ const sorted = scopedTokens.slice().sort((left, right) => compareTokensByPriority(left, right, stableSymbols));
1304
+ next.push({ id: "all", tokens: sorted });
1406
1305
  return next;
1407
1306
  }
1408
- const chainName = availableChains.find((chain) => chain.blockchain === chainFilter)?.chainName ?? chainFilter;
1409
- return [{
1410
- id: chainFilter,
1411
- title: chainName,
1412
- walletKind,
1413
- tokens: showBalances ? sortTokensByBalance(matched, usdOf, loadingOf) : sortTokensBySymbol(matched),
1414
- badgeAssetId: showBalances ? newestAssetId : null
1415
- }];
1307
+ if (view.kind === "rail-chain" || view.kind === "network-chain") {
1308
+ const matched = scopedTokens.filter((token) => token.blockchain === view.blockchain);
1309
+ if (!payer) {
1310
+ const next = [];
1311
+ const popular = popularTokens.filter((token) => token.blockchain === view.blockchain);
1312
+ if (popular.length > 0) {
1313
+ next.push({ id: "popular", title: POPULAR_SECTION_TITLE, tokens: popular });
1314
+ }
1315
+ next.push({
1316
+ id: view.blockchain,
1317
+ tokens: matched.slice().sort((left, right) => compareTokensByPriority(left, right, stableSymbols))
1318
+ });
1319
+ return next;
1320
+ }
1321
+ const chain = chainByCode(availableChains, view.blockchain);
1322
+ return [{
1323
+ id: view.blockchain,
1324
+ tokens: sortTokensForChain(matched, {
1325
+ withBalances: Boolean(chain && owners[chain.chainKind]),
1326
+ usdOf,
1327
+ loadingOf,
1328
+ stableSymbols
1329
+ }),
1330
+ badgeAssetId: newestAssetId
1331
+ }];
1332
+ }
1333
+ return [];
1416
1334
  }, [
1417
- showBalances,
1418
- chainFilter,
1419
- searchingAll,
1420
- search,
1335
+ view,
1336
+ payer,
1421
1337
  scopedTokens,
1422
1338
  prefs2.recentAssetIds,
1423
1339
  newestAssetId,
1424
- walletKind,
1425
1340
  balanceEntries,
1426
1341
  owners,
1427
1342
  popularTokens,
1428
- availableChains
1343
+ availableChains,
1344
+ stableSymbols,
1345
+ balanceMode,
1346
+ syncGetBalance,
1347
+ syncIsBalanceLoading
1429
1348
  ]);
1430
- function handleSelectFilter(filter) {
1431
- setChainFilter(filter);
1432
- if (filter !== ALL_CHAIN_FILTER) setLastBlockchain(filter);
1433
- setView("token");
1349
+ const searchGroups = useMemo(() => {
1350
+ if (!searching) return [];
1351
+ return groupSearchResults(scopedTokens, search, {
1352
+ role: payer ? "payer" : "receiver",
1353
+ usdOf,
1354
+ loadingOf,
1355
+ hasOwner: (kind) => Boolean(owners[kind]),
1356
+ stableSymbols
1357
+ });
1358
+ }, [searching, scopedTokens, search, payer, owners, stableSymbols, balanceEntries, balanceMode, syncGetBalance, syncIsBalanceLoading]);
1359
+ function handleSelectChain(blockchain, fromNetworks) {
1360
+ setLastBlockchain(blockchain);
1361
+ if (fromNetworks) {
1362
+ savedNetworksScroll.current = networksScrollRef.current?.scrollTop ?? 0;
1363
+ setSlideDirection(1);
1364
+ setView({ kind: "network-chain", blockchain });
1365
+ return;
1366
+ }
1367
+ setView({ kind: "rail-chain", blockchain });
1434
1368
  }
1369
+ function handleBackToNetworks() {
1370
+ setSlideDirection(-1);
1371
+ setView({ kind: "networks" });
1372
+ }
1373
+ useLayoutEffect(() => {
1374
+ if (view.kind !== "networks" || slideDirection !== -1) return;
1375
+ const node = networksScrollRef.current;
1376
+ if (!node) return;
1377
+ node.scrollTop = savedNetworksScroll.current;
1378
+ }, [view, slideDirection]);
1435
1379
  function handleSelectToken(token) {
1436
1380
  if (isBlockchainDisabled(token.blockchain, disabledBlockchains)) return;
1437
1381
  if (isChainKindLocked(token.chain.chainKind, lockChainKind)) return;
@@ -1440,8 +1384,28 @@ function TokenSelectDialog(props) {
1440
1384
  onSelect({ token });
1441
1385
  onClose();
1442
1386
  }
1443
- const awaitingBalances = showBalances && chainFilter === ALL_CHAIN_FILTER && !searchingAll && scopedTokens.some((token) => loadingOf(token));
1444
- return /* @__PURE__ */ jsx6(
1387
+ const activeChain = view.kind === "rail-chain" || view.kind === "network-chain" ? chainByCode(availableChains, view.blockchain) : void 0;
1388
+ const chainHeader = view.kind === "network-chain" ? { kind: "back", title: activeChain?.chainName ?? view.blockchain, onBack: handleBackToNetworks, walletKind: activeChain?.chainKind } : view.kind === "rail-chain" ? { kind: "chain", title: activeChain?.chainName ?? view.blockchain, walletKind: activeChain?.chainKind } : null;
1389
+ const awaitingBalances = showBalances && sections.every((section) => section.tokens.length === 0) && scopedTokens.some((token) => loadingOf(token));
1390
+ const tokenDisabled = (token) => isBlockchainDisabled(token.blockchain, disabledBlockchains) || isChainKindLocked(token.chain.chainKind, lockChainKind);
1391
+ const sliding = view.kind === "networks" || view.kind === "network-chain";
1392
+ const tokenPane = /* @__PURE__ */ jsx8(
1393
+ TokenPane,
1394
+ {
1395
+ sections,
1396
+ header: chainHeader,
1397
+ selectedAssetId,
1398
+ loading: loading || awaitingBalances,
1399
+ showBalances,
1400
+ showWalletStatus: payer,
1401
+ getBalance: balanceOf,
1402
+ isBalanceLoading: loadingOf,
1403
+ onSelectToken: handleSelectToken,
1404
+ isTokenDisabled: tokenDisabled,
1405
+ rowClassName
1406
+ }
1407
+ );
1408
+ return /* @__PURE__ */ jsx8(
1445
1409
  Dialog,
1446
1410
  {
1447
1411
  open,
@@ -1452,10 +1416,11 @@ function TokenSelectDialog(props) {
1452
1416
  fontSans,
1453
1417
  titleClassName
1454
1418
  ),
1455
- closeClassName: cx(view === "token" ? "ml-0" : void 0, closeClassName),
1456
- contentClassName,
1457
- headerAction: view === "token" ? /* @__PURE__ */ jsx6(
1458
- SearchInput2,
1419
+ headerClassName: cx("h-14 border-b border-[#e3e3e3] px-[22px] pr-4", headerClassName),
1420
+ closeClassName: cx("md:ml-0", closeClassName),
1421
+ contentClassName: cx("overflow-hidden", contentClassName),
1422
+ headerAction: /* @__PURE__ */ jsx8(
1423
+ SearchInput,
1459
1424
  {
1460
1425
  value: search,
1461
1426
  onChange: setSearch,
@@ -1463,80 +1428,261 @@ function TokenSelectDialog(props) {
1463
1428
  className: "ml-auto hidden w-[215px] shrink-0 md:block",
1464
1429
  inputClassName: cx(SEARCH_INPUT_CLASS, searchClassName)
1465
1430
  }
1466
- ) : null,
1467
- cardClassName: cx("w-full max-h-[90vh] md:w-[474px]", className),
1468
- children: /* @__PURE__ */ jsxs5("div", { className: "flex h-[min(520px,62vh)] min-h-0 flex-col overflow-hidden md:h-[min(555px,70vh)]", children: [
1469
- view === "token" ? /* @__PURE__ */ jsx6(
1470
- SearchInput2,
1431
+ ),
1432
+ cardClassName: cx(
1433
+ "w-full max-h-[90vh] gap-0 rounded-[12px] border border-[#e0e0e0] bg-[#fdfdfd] p-0 shadow-[0_0_40px_0_rgba(0,0,0,0.06)] md:w-[474px] md:rounded-[12px] md:p-0",
1434
+ className
1435
+ ),
1436
+ children: /* @__PURE__ */ jsxs6("div", { className: "flex h-[min(520px,62vh)] min-h-0 flex-col overflow-hidden md:h-[min(555px,70vh)]", children: [
1437
+ /* @__PURE__ */ jsx8(
1438
+ SearchInput,
1471
1439
  {
1472
1440
  value: search,
1473
1441
  onChange: setSearch,
1474
1442
  placeholder: "search or paste address",
1475
- className: "mb-3 shrink-0 md:hidden",
1443
+ className: "mb-3 shrink-0 px-[22px] md:hidden",
1476
1444
  inputClassName: cx(SEARCH_INPUT_CLASS, searchClassName)
1477
1445
  }
1478
- ) : null,
1479
- /* @__PURE__ */ jsxs5("div", { className: "flex min-h-0 flex-1", children: [
1480
- /* @__PURE__ */ jsx6(
1446
+ ),
1447
+ /* @__PURE__ */ jsxs6("div", { className: "flex min-h-0 flex-1", children: [
1448
+ /* @__PURE__ */ jsx8(
1481
1449
  NetworkRail,
1482
1450
  {
1483
- view,
1484
- chainFilter,
1485
- chips: sortedChains,
1486
- fundedBlockchains,
1451
+ role: payer ? "payer" : "receiver",
1487
1452
  walletConnected,
1488
- showAllLabel: !payer,
1453
+ selection: railSelection(view),
1454
+ chains: railChains,
1455
+ fundedBlockchains,
1489
1456
  lockChainKind,
1490
1457
  disabledBlockchains,
1491
1458
  disabledReason,
1492
- onSelectFilter: handleSelectFilter,
1493
- onOpenNetworks: () => setView("network"),
1459
+ onSelectWallet: () => setView({ kind: "wallet" }),
1460
+ onSelectAll: () => setView({ kind: "all" }),
1461
+ onSelectNetworks: () => setView({ kind: "networks" }),
1462
+ onSelectChain: (blockchain) => handleSelectChain(blockchain, false),
1494
1463
  className: railClassName
1495
1464
  }
1496
1465
  ),
1497
- view === "network" ? /* @__PURE__ */ jsx6("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col pl-3 md:pl-4", children: /* @__PURE__ */ jsx6(
1498
- ChainPane,
1499
- {
1500
- chains: sortedChains,
1501
- fundedBlockchains,
1502
- showWalletStatus: payer,
1503
- onSelectFilter: handleSelectFilter,
1504
- lockChainKind,
1505
- disabledBlockchains,
1506
- disabledReason
1507
- }
1508
- ) }) : /* @__PURE__ */ jsx6("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col pl-3 md:pl-4", children: /* @__PURE__ */ jsx6(
1509
- TokenPane,
1466
+ searching ? /* @__PURE__ */ jsx8(
1467
+ SearchResults,
1510
1468
  {
1511
- sections,
1469
+ groups: searchGroups,
1512
1470
  selectedAssetId,
1513
- loading: loading || awaitingBalances,
1471
+ loading,
1514
1472
  showBalances,
1515
1473
  showWalletStatus: payer,
1516
1474
  getBalance: balanceOf,
1517
1475
  isBalanceLoading: loadingOf,
1518
1476
  onSelectToken: handleSelectToken,
1519
- isTokenDisabled: (token) => isBlockchainDisabled(token.blockchain, disabledBlockchains) || isChainKindLocked(token.chain.chainKind, lockChainKind),
1520
- searchClassName,
1477
+ isTokenDisabled: tokenDisabled,
1521
1478
  rowClassName
1522
1479
  }
1523
- ) })
1480
+ ) : sliding ? /* @__PURE__ */ jsx8(
1481
+ PaneTransition,
1482
+ {
1483
+ paneKey: view.kind === "network-chain" ? `network-chain:${view.blockchain}` : "networks",
1484
+ direction: slideDirection,
1485
+ children: view.kind === "networks" ? /* @__PURE__ */ jsx8(
1486
+ ChainPane,
1487
+ {
1488
+ chains: networkChains,
1489
+ showWalletStatus: payer,
1490
+ onSelectChain: (blockchain) => handleSelectChain(blockchain, true),
1491
+ lockChainKind,
1492
+ disabledBlockchains,
1493
+ disabledReason,
1494
+ scrollRef: networksScrollRef
1495
+ }
1496
+ ) : tokenPane
1497
+ }
1498
+ ) : /* @__PURE__ */ jsx8("div", { className: "relative min-h-0 flex-1", children: tokenPane })
1524
1499
  ] })
1525
1500
  ] })
1526
1501
  }
1527
1502
  );
1528
1503
  }
1504
+
1505
+ // src/token-select/logos.ts
1506
+ var LOGO_HOST = "https://assets.dapdap.net";
1507
+ var CHAIN_LOGO_ALIAS = {
1508
+ eth: "ethereum",
1509
+ ethereum: "ethereum",
1510
+ base: "base",
1511
+ arb: "arbitrum",
1512
+ arbitrum: "arbitrum",
1513
+ op: "optimism",
1514
+ optimism: "optimism",
1515
+ pol: "polygon",
1516
+ polygon: "polygon",
1517
+ bsc: "bsc",
1518
+ avax: "avalanche",
1519
+ avalanche: "avalanche",
1520
+ gnosis: "gnosis",
1521
+ scroll: "scroll",
1522
+ xlayer: "xlayer",
1523
+ bera: "berachain",
1524
+ berachain: "berachain",
1525
+ near: "near",
1526
+ sol: "solana",
1527
+ solana: "solana",
1528
+ tron: "tron",
1529
+ zec: "zcash",
1530
+ zcash: "zcash"
1531
+ };
1532
+ function logoPath(path) {
1533
+ const normalized = path.startsWith("/") ? path : `/${path}`;
1534
+ return `${LOGO_HOST}${normalized}`;
1535
+ }
1536
+ function chainLogoUrl(blockchainOrNetwork) {
1537
+ const key = String(blockchainOrNetwork || "").toLowerCase();
1538
+ const file = CHAIN_LOGO_ALIAS[key] || key.replace(/\s+/g, "");
1539
+ return logoPath(`/stableflow/networks/${file}.png`);
1540
+ }
1541
+ function tokenLogoUrl(symbol) {
1542
+ const upper = String(symbol || "").toUpperCase();
1543
+ const file = upper === "USDT0" ? "usdt" : upper.toLowerCase();
1544
+ return logoPath(`/stableflow/tokens/${file}.png`);
1545
+ }
1546
+
1547
+ // src/token-select/pay-config.ts
1548
+ var ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000";
1549
+ var WRAP_NEAR_CONTRACT = "wrap.near";
1550
+ function asRecord(value) {
1551
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1552
+ return value;
1553
+ }
1554
+ function apiText(value) {
1555
+ if (typeof value === "string") return value.trim();
1556
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
1557
+ return "";
1558
+ }
1559
+ function apiNumber(value) {
1560
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1561
+ if (typeof value === "string" && value.trim()) {
1562
+ const parsed = Number(value);
1563
+ if (Number.isFinite(parsed)) return parsed;
1564
+ }
1565
+ return null;
1566
+ }
1567
+ function asBoolean(value, fallback = true) {
1568
+ if (typeof value === "boolean") return value;
1569
+ if (typeof value === "string") {
1570
+ const lower = value.trim().toLowerCase();
1571
+ if (lower === "true") return true;
1572
+ if (lower === "false") return false;
1573
+ }
1574
+ return fallback;
1575
+ }
1576
+ function mapSourceChain(raw) {
1577
+ const row = asRecord(raw) ?? {};
1578
+ return {
1579
+ network: apiText(row.network),
1580
+ chainId: apiText(row.chain_id ?? row.chainId),
1581
+ chainName: apiText(row.chain_name ?? row.chainName),
1582
+ logo: apiText(row.logo),
1583
+ explorer: apiText(row.explorer)
1584
+ };
1585
+ }
1586
+ function mapSourceToken(raw) {
1587
+ const row = asRecord(raw) ?? {};
1588
+ return {
1589
+ symbol: apiText(row.symbol),
1590
+ network: apiText(row.network),
1591
+ decimals: apiNumber(row.decimals) ?? 0,
1592
+ contractAddress: apiText(row.contract_address ?? row.contractAddress),
1593
+ price: apiText(row.price),
1594
+ supportPayment: asBoolean(row.support_payment ?? row.supportPayment),
1595
+ supportReceive: asBoolean(row.support_receive ?? row.supportReceive)
1596
+ };
1597
+ }
1598
+ function configBody(raw) {
1599
+ const row = asRecord(raw) ?? {};
1600
+ const data = asRecord(row.data);
1601
+ if (typeof row.code === "number" && data) return data;
1602
+ return row;
1603
+ }
1604
+ function mapPayConfigSource(raw) {
1605
+ const row = configBody(raw);
1606
+ const chains = Array.isArray(row.chains) ? row.chains.map(mapSourceChain) : [];
1607
+ const tokens = Array.isArray(row.tokens) ? row.tokens.map(mapSourceToken) : [];
1608
+ return { chains, tokens };
1609
+ }
1610
+ function chainKindFor(network, overrides = {}) {
1611
+ const key = network.trim().toLowerCase();
1612
+ if (!key) return null;
1613
+ return overrides[key] ?? DEFAULT_CHAIN_KINDS[key] ?? null;
1614
+ }
1615
+ function normalizeSymbol(symbol) {
1616
+ const upper = symbol.trim().toUpperCase();
1617
+ if (!upper) return null;
1618
+ if (upper === "USDT0") return "USDT";
1619
+ if (upper === "WNEAR") return "NEAR";
1620
+ return upper;
1621
+ }
1622
+ function tokenAssetId(network, symbol, contractAddress) {
1623
+ const addr = String(contractAddress || "").trim();
1624
+ return `${network}:${symbol}:${addr || "native"}`;
1625
+ }
1626
+ function isNativeAddress(contractAddress) {
1627
+ const addr = String(contractAddress || "").trim();
1628
+ if (!addr) return true;
1629
+ const lower = addr.toLowerCase();
1630
+ return lower === "native" || lower === ZERO_ADDRESS2;
1631
+ }
1632
+ function normalizePayConfig(source, options) {
1633
+ const overrides = options?.chainKinds ?? {};
1634
+ const chains = [];
1635
+ for (const chain of source.chains) {
1636
+ const chainKind = chainKindFor(chain.network, overrides);
1637
+ if (!chainKind || !chain.network) continue;
1638
+ chains.push({
1639
+ blockchain: chain.network,
1640
+ chainName: chain.chainName || chain.network,
1641
+ chainKind,
1642
+ chainId: chain.chainId || void 0,
1643
+ logo: chain.logo || chainLogoUrl(chain.network),
1644
+ explorer: chain.explorer
1645
+ });
1646
+ }
1647
+ const chainByCode2 = new Map(chains.map((chain) => [chain.blockchain, chain]));
1648
+ const tokens = [];
1649
+ for (const token of source.tokens) {
1650
+ const chain = chainByCode2.get(token.network);
1651
+ if (!chain) continue;
1652
+ const symbol = normalizeSymbol(token.symbol);
1653
+ if (!symbol) continue;
1654
+ if (!Number.isInteger(token.decimals) || token.decimals < 0) continue;
1655
+ const contractAddress = token.contractAddress.trim() || null;
1656
+ tokens.push({
1657
+ assetId: tokenAssetId(token.network, symbol, contractAddress),
1658
+ symbol,
1659
+ providerSymbol: token.symbol,
1660
+ blockchain: token.network,
1661
+ decimals: token.decimals,
1662
+ contractAddress,
1663
+ price: Number(token.price || 1),
1664
+ logo: tokenLogoUrl(symbol),
1665
+ supportPayment: token.supportPayment,
1666
+ supportReceive: token.supportReceive,
1667
+ chain
1668
+ });
1669
+ }
1670
+ const hasNearWrap = tokens.some((token) => token.symbol === "NEAR" && token.blockchain === "near" && token.contractAddress?.toLowerCase() === WRAP_NEAR_CONTRACT);
1671
+ if (!hasNearWrap) return tokens;
1672
+ return tokens.filter((token) => !(token.symbol === "NEAR" && token.blockchain === "near" && isNativeAddress(token.contractAddress)));
1673
+ }
1529
1674
  export {
1530
- DEFAULT_API_HOST,
1675
+ KNOWN_CHAIN_KINDS,
1531
1676
  PayWidgetsProvider,
1532
1677
  TokenSelectDialog,
1678
+ chainsFromTokens,
1533
1679
  clearPayTokenBalances,
1534
1680
  mapPayConfigSource,
1535
1681
  normalizePayConfig,
1682
+ ownerKey2 as ownerKey,
1536
1683
  parsePopularTokens,
1537
1684
  refreshPayTokenBalance,
1538
- usePayConfig,
1539
- usePayConfigSnapshot,
1685
+ tokenAssetId,
1540
1686
  usePayTokenBalance
1541
1687
  };
1542
1688
  //# sourceMappingURL=index.js.map