@pollar/react 0.10.0 → 0.10.1-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { pollarPaths, PollarClient, PollarClientConfig, PollarAdapters, OnStorageDegrade, WalletInfo, PollarLoginOptions, SessionsState, TransactionState, TxBuildBody, BuildOutcome, SubmitOutcome, SignOutcome, StellarNetwork, WalletBalanceState, EnabledAssetsState, TrustlineOutcome, TxHistoryState, SwapQuoteParams, SwapQuote, PollarAdapter, WalletId, KycStatus as KycStatus$1, RampQuote, AuthState, KycProvider, KycStartResponse, RampDirection, RampTxStatus, EnabledAssetRecord, WalletBalanceRecord, SwapProvider, DistributionRulesState, DistributionRule } from '@pollar/core';
2
+ import { pollarPaths, PollarClient, PollarClientConfig, PollarAdapters, OnStorageDegrade, WalletInfo, PollarLoginOptions, SessionsState, TransactionState, TxBuildBody, BuildOutcome, SubmitOutcome, SignOutcome, StellarNetwork, WalletBalanceState, EnabledAssetsState, TrustlineOutcome, TxHistoryState, SwapVenue, SwapToken, SwapQuoteParams, SwapQuote, PollarAdapter, WalletId, KycStatus as KycStatus$1, RampQuote, AuthState, KycProvider, KycStartResponse, RampDirection, RampCountry, RampTxStatus, EnabledAssetRecord, WalletBalanceRecord, SwapProvider, DistributionRulesState, DistributionRule } from '@pollar/core';
3
3
  export { AuthProviderContext, PollarAuthProvider, SessionsState } from '@pollar/core';
4
4
  import { ReactNode } from 'react';
5
5
 
@@ -91,6 +91,16 @@ interface PollarContextValue {
91
91
  openWalletBalanceModal: () => void;
92
92
  openSendModal: () => void;
93
93
  openReceiveModal: () => void;
94
+ /**
95
+ * The swap venues this app exposes to end-users (empty = swap disabled, hide
96
+ * swap UI). Mirrors {@link PollarClient.getSwapConfig}.
97
+ */
98
+ getSwapConfig: () => Promise<SwapVenue[]>;
99
+ /**
100
+ * The curated "buy" tokens this app opted into (admin catalog). Mirrors
101
+ * {@link PollarClient.getSwapTokens}.
102
+ */
103
+ getSwapTokens: () => Promise<SwapToken[]>;
94
104
  /**
95
105
  * Quote an asset-to-asset swap across the requested venue(s). Read-only;
96
106
  * returns quotes ranked best-first. Mirrors {@link PollarClient.getSwapQuote}.
@@ -307,6 +317,9 @@ interface RampWidgetTemplateProps {
307
317
  amount: string;
308
318
  currency: string;
309
319
  country: string;
320
+ countries: RampCountry[];
321
+ countriesLoading: boolean;
322
+ refreshing: boolean;
310
323
  quotes: RampQuote[];
311
324
  isLoading: boolean;
312
325
  provider: string;
@@ -325,9 +338,10 @@ interface RampWidgetTemplateProps {
325
338
  onOpenKyc: () => void;
326
339
  onCompleteWithdraw: () => void;
327
340
  onRetry: () => void;
341
+ onRefresh: () => void;
328
342
  onClose: () => void;
329
343
  }
330
- declare function RampWidgetTemplate({ theme, accentColor, step, direction, amount, currency, country, quotes, isLoading, provider, txStatus, kycUrl, stellarTxHash, canComplete, completing, errorMsg, onDirectionChange, onAmountChange, onCurrencyChange, onCountryChange, onFindRoute, onSelectQuote, onOpenKyc, onCompleteWithdraw, onRetry, onClose, }: RampWidgetTemplateProps): react_jsx_runtime.JSX.Element;
344
+ declare function RampWidgetTemplate({ theme, accentColor, step, direction, amount, currency, country, countries, countriesLoading, refreshing, quotes, isLoading, provider, txStatus, kycUrl, stellarTxHash, canComplete, completing, errorMsg, onDirectionChange, onAmountChange, onCurrencyChange, onCountryChange, onFindRoute, onSelectQuote, onOpenKyc, onCompleteWithdraw, onRetry, onRefresh, onClose, }: RampWidgetTemplateProps): react_jsx_runtime.JSX.Element;
331
345
 
332
346
  interface TransactionModalTemplateProps {
333
347
  theme: string;
@@ -454,6 +468,12 @@ interface SwapModalTemplateProps {
454
468
  formError: string;
455
469
  isLoadingData: boolean;
456
470
  smartUnsupported: boolean;
471
+ /** Swap config is still loading. */
472
+ configLoading: boolean;
473
+ /** Config loaded and this app exposes no swap venues — swap is disabled. */
474
+ swapUnavailable: boolean;
475
+ /** Buying the selected asset will create a trustline first (~0.5 XLM reserve). */
476
+ buyNeedsTrustline: boolean;
457
477
  transaction: TransactionState;
458
478
  showXdr: boolean;
459
479
  copied: boolean;
@@ -463,8 +483,11 @@ interface SwapModalTemplateProps {
463
483
  isInProgress: boolean;
464
484
  onClose: () => void;
465
485
  onBack: () => void;
486
+ onRefresh: () => void;
466
487
  onSelectSell: (o: SwapAssetOption) => void;
467
488
  onSelectBuy: (o: SwapAssetOption) => void;
489
+ /** Add a user-pasted token (code + issuer) to the buy list. Returns an error or null. */
490
+ onAddCustomToken: (code: string, issuer: string) => string | null;
468
491
  onAmountChange: (value: string) => void;
469
492
  onProviderChange: (p: SwapProvider) => void;
470
493
  onSwap: () => void;
@@ -473,7 +496,7 @@ interface SwapModalTemplateProps {
473
496
  onRetry: () => void;
474
497
  onDone: () => void;
475
498
  }
476
- declare function SwapModalTemplate({ theme, accentColor, step, txTitle, sellOptions, buyOptions, selectedSell, selectedBuy, amount, provider, providers, quote, quoteLoading, quoteError, formError, isLoadingData, smartUnsupported, transaction, showXdr, copied, explorerUrl, walletType, showBack, isInProgress, onClose, onBack, onSelectSell, onSelectBuy, onAmountChange, onProviderChange, onSwap, onToggleXdr, onCopyHash, onRetry, onDone, }: SwapModalTemplateProps): react_jsx_runtime.JSX.Element;
499
+ declare function SwapModalTemplate({ theme, accentColor, step, txTitle, sellOptions, buyOptions, selectedSell, selectedBuy, amount, provider, providers, quote, quoteLoading, quoteError, formError, isLoadingData, smartUnsupported, configLoading, swapUnavailable, buyNeedsTrustline, transaction, showXdr, copied, explorerUrl, walletType, showBack, isInProgress, onClose, onBack, onRefresh, onSelectSell, onSelectBuy, onAddCustomToken, onAmountChange, onProviderChange, onSwap, onToggleXdr, onCopyHash, onRetry, onDone, }: SwapModalTemplateProps): react_jsx_runtime.JSX.Element;
477
500
 
478
501
  interface ReceiveModalTemplateProps {
479
502
  theme: string;
package/dist/index.js CHANGED
@@ -1035,7 +1035,7 @@ var PollarModalFooter = () => {
1035
1035
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "pollar-footer-name", children: "Pollar" }),
1036
1036
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "pollar-footer-version", children: [
1037
1037
  "v",
1038
- "0.10.0"
1038
+ "0.10.1-rc.1"
1039
1039
  ] })
1040
1040
  ] })
1041
1041
  ] });
@@ -2459,14 +2459,12 @@ function RouteDisplay({ quote, onSelect }) {
2459
2459
  );
2460
2460
  }
2461
2461
  var LOADING_STEPS = ["Detecting your country\u2026", "Consulting providers\u2026", "Route found!"];
2462
- var COUNTRY_CURRENCIES = {
2463
- MX: "MXN",
2464
- BR: "BRL",
2465
- CO: "COP",
2466
- CL: "CLP",
2467
- PE: "PEN",
2468
- AR: "ARS"
2469
- };
2462
+ function flagEmoji(code) {
2463
+ const cc = code.trim().toUpperCase();
2464
+ if (!/^[A-Z]{2}$/.test(cc)) return "";
2465
+ const base = 127462;
2466
+ return String.fromCodePoint(base + (cc.charCodeAt(0) - 65), base + (cc.charCodeAt(1) - 65));
2467
+ }
2470
2468
  var STATUS_LABEL = {
2471
2469
  pending: "Pending",
2472
2470
  processing: "Processing",
@@ -2481,6 +2479,9 @@ function RampWidgetTemplate({
2481
2479
  amount,
2482
2480
  currency,
2483
2481
  country,
2482
+ countries,
2483
+ countriesLoading,
2484
+ refreshing,
2484
2485
  quotes,
2485
2486
  isLoading,
2486
2487
  provider,
@@ -2499,6 +2500,7 @@ function RampWidgetTemplate({
2499
2500
  onOpenKyc,
2500
2501
  onCompleteWithdraw,
2501
2502
  onRetry,
2503
+ onRefresh,
2502
2504
  onClose
2503
2505
  }) {
2504
2506
  const isDark = theme === "dark";
@@ -2537,9 +2539,35 @@ function RampWidgetTemplate({
2537
2539
  error: "Please try again"
2538
2540
  };
2539
2541
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pollar-modal-card pollar-ramp-modal", style: cssVars, onClick: (e) => e.stopPropagation(), children: [
2540
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pollar-ramp-header", children: [
2541
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "pollar-ramp-title", children: stepTitle[step] }),
2542
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "pollar-ramp-subtitle", children: stepSubtitle[step] })
2542
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pollar-modal-header", children: [
2543
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pollar-ramp-header-text", children: [
2544
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "pollar-modal-title", children: stepTitle[step] }),
2545
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "pollar-ramp-subtitle", children: stepSubtitle[step] })
2546
+ ] }),
2547
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pollar-modal-header-actions", children: [
2548
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "pollar-modal-close", onClick: onRefresh, disabled: refreshing, "aria-label": "Refresh", children: /* @__PURE__ */ jsxRuntime.jsx(
2549
+ "svg",
2550
+ {
2551
+ className: refreshing ? "pollar-modal-refresh-icon spinning" : "pollar-modal-refresh-icon",
2552
+ width: "16",
2553
+ height: "16",
2554
+ viewBox: "0 0 16 16",
2555
+ fill: "none",
2556
+ "aria-hidden": true,
2557
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2558
+ "path",
2559
+ {
2560
+ d: "M13.5 8a5.5 5.5 0 1 1-1.6-3.9M13.5 2v3h-3",
2561
+ stroke: "currentColor",
2562
+ strokeWidth: "1.6",
2563
+ strokeLinecap: "round",
2564
+ strokeLinejoin: "round"
2565
+ }
2566
+ )
2567
+ }
2568
+ ) }),
2569
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "pollar-modal-close", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2 2l12 12M14 2L2 14", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) }) })
2570
+ ] })
2543
2571
  ] }),
2544
2572
  step === "input" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2545
2573
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pollar-ramp-tabs", children: [
@@ -2596,30 +2624,25 @@ function RampWidgetTemplate({
2596
2624
  ] }),
2597
2625
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pollar-ramp-field", children: [
2598
2626
  /* @__PURE__ */ jsxRuntime.jsx("label", { className: "pollar-ramp-label", children: "Country" }),
2599
- /* @__PURE__ */ jsxRuntime.jsxs(
2600
- "select",
2601
- {
2602
- className: "pollar-ramp-input",
2603
- value: country,
2604
- onChange: (e) => {
2605
- const c = e.target.value;
2606
- onCountryChange(c);
2607
- if (COUNTRY_CURRENCIES[c]) onCurrencyChange(COUNTRY_CURRENCIES[c]);
2608
- },
2609
- children: [
2610
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "MX", children: "\u{1F1F2}\u{1F1FD} Mexico" }),
2611
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "BR", children: "\u{1F1E7}\u{1F1F7} Brazil" }),
2612
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "CO", children: "\u{1F1E8}\u{1F1F4} Colombia" }),
2613
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "CL", children: "\u{1F1E8}\u{1F1F1} Chile" }),
2614
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "PE", children: "\u{1F1F5}\u{1F1EA} Peru" }),
2615
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "AR", children: "\u{1F1E6}\u{1F1F7} Argentina" })
2616
- ]
2617
- }
2618
- )
2627
+ countriesLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pollar-ramp-input pollar-ramp-input-loading", children: "Loading countries\u2026" }) : countries.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pollar-modal-error", children: "No ramp providers available on this network yet." }) : /* @__PURE__ */ jsxRuntime.jsx("select", { className: "pollar-ramp-input", value: country, onChange: (e) => onCountryChange(e.target.value), children: countries.map((c) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: c.code, children: [
2628
+ flagEmoji(c.code),
2629
+ " ",
2630
+ c.code,
2631
+ c.currency ? ` \u2014 ${c.currency}` : ""
2632
+ ] }, c.code)) })
2619
2633
  ] }),
2620
2634
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pollar-modal-actions", children: [
2621
2635
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "pollar-btn-secondary", onClick: onClose, children: "Cancel" }),
2622
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "pollar-btn-primary", disabled: !amount || isLoading, onClick: onFindRoute, children: "Find best route" })
2636
+ /* @__PURE__ */ jsxRuntime.jsx(
2637
+ "button",
2638
+ {
2639
+ type: "button",
2640
+ className: "pollar-btn-primary",
2641
+ disabled: !amount || isLoading || countriesLoading || countries.length === 0,
2642
+ onClick: onFindRoute,
2643
+ children: "Find best route"
2644
+ }
2645
+ )
2623
2646
  ] })
2624
2647
  ] }),
2625
2648
  step === "loading_quote" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pollar-ramp-loading", children: LOADING_STEPS.map((text, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pollar-ramp-loading-row", children: [
@@ -2637,13 +2660,7 @@ function RampWidgetTemplate({
2637
2660
  ] }),
2638
2661
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pollar-ramp-payment-field", children: [
2639
2662
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "pollar-ramp-payment-label", children: "Status" }),
2640
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pollar-ramp-payment-value", children: /* @__PURE__ */ jsxRuntime.jsx(
2641
- "code",
2642
- {
2643
- style: { color: txStatus === "completed" ? "var(--pollar-success-text)" : void 0 },
2644
- children: txStatus ? STATUS_LABEL[txStatus] : "Processing"
2645
- }
2646
- ) })
2663
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pollar-ramp-payment-value", children: /* @__PURE__ */ jsxRuntime.jsx("code", { style: { color: txStatus === "completed" ? "var(--pollar-success-text)" : void 0 }, children: txStatus ? STATUS_LABEL[txStatus] : "Processing" }) })
2647
2664
  ] }),
2648
2665
  stellarTxHash && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pollar-ramp-payment-field", children: [
2649
2666
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "pollar-ramp-payment-label", children: "Stellar tx" }),
@@ -2679,8 +2696,11 @@ function RampWidget({ onClose }) {
2679
2696
  const [step, setStep] = react.useState("input");
2680
2697
  const [direction, setDirection] = react.useState("onramp");
2681
2698
  const [amount, setAmount] = react.useState("");
2682
- const [currency, setCurrency] = react.useState("ARS");
2683
- const [country, setCountry] = react.useState("AR");
2699
+ const [currency, setCurrency] = react.useState("");
2700
+ const [country, setCountry] = react.useState("");
2701
+ const [countries, setCountries] = react.useState([]);
2702
+ const [countriesLoading, setCountriesLoading] = react.useState(true);
2703
+ const [refreshing, setRefreshing] = react.useState(false);
2684
2704
  const [quotes, setQuotes] = react.useState([]);
2685
2705
  const [isLoading, setIsLoading] = react.useState(false);
2686
2706
  const [txId, setTxId] = react.useState(null);
@@ -2712,6 +2732,50 @@ function RampWidget({ onClose }) {
2712
2732
  clearInterval(id);
2713
2733
  };
2714
2734
  }, [step, txId, txStatus, client]);
2735
+ async function loadCountries(resetSelection) {
2736
+ setCountriesLoading(true);
2737
+ try {
2738
+ const { countries: list } = await client.getRampCountries();
2739
+ setCountries(list);
2740
+ const first = list[0];
2741
+ const stillValid = list.some((c) => c.code === country);
2742
+ if (first && (resetSelection || !stillValid)) {
2743
+ setCountry(first.code);
2744
+ if (first.currency) setCurrency(first.currency);
2745
+ }
2746
+ } catch {
2747
+ setCountries([]);
2748
+ } finally {
2749
+ setCountriesLoading(false);
2750
+ }
2751
+ }
2752
+ react.useEffect(() => {
2753
+ loadCountries(true);
2754
+ }, []);
2755
+ function handleCountryChange(code) {
2756
+ setCountry(code);
2757
+ const match = countries.find((c) => c.code === code);
2758
+ if (match?.currency) setCurrency(match.currency);
2759
+ }
2760
+ async function handleRefresh() {
2761
+ if (refreshing) return;
2762
+ setRefreshing(true);
2763
+ try {
2764
+ await loadCountries(false);
2765
+ if (step === "select_route") {
2766
+ const result = await client.getRampsQuote({ country, amount: Number(amount), currency, direction });
2767
+ setQuotes(result.quotes ?? []);
2768
+ } else if (step === "status" && txId) {
2769
+ const tx = await client.getRampTransaction(txId);
2770
+ setTxStatus(tx.status);
2771
+ if (tx.stellarTxHash) setStellarTxHash(tx.stellarTxHash);
2772
+ if (tx.kycUrl) setKycUrl(tx.kycUrl);
2773
+ }
2774
+ } catch {
2775
+ } finally {
2776
+ setRefreshing(false);
2777
+ }
2778
+ }
2715
2779
  function resetToInput() {
2716
2780
  setStep("input");
2717
2781
  setQuotes([]);
@@ -2817,6 +2881,9 @@ function RampWidget({ onClose }) {
2817
2881
  amount,
2818
2882
  currency,
2819
2883
  country,
2884
+ countries,
2885
+ countriesLoading,
2886
+ refreshing,
2820
2887
  quotes,
2821
2888
  isLoading,
2822
2889
  provider,
@@ -2829,12 +2896,13 @@ function RampWidget({ onClose }) {
2829
2896
  onDirectionChange: setDirection,
2830
2897
  onAmountChange: setAmount,
2831
2898
  onCurrencyChange: setCurrency,
2832
- onCountryChange: setCountry,
2899
+ onCountryChange: handleCountryChange,
2833
2900
  onFindRoute: handleFindRoute,
2834
2901
  onSelectQuote: handleSelectQuote,
2835
2902
  onOpenKyc: handleOpenKyc,
2836
2903
  onCompleteWithdraw: handleCompleteWithdraw,
2837
2904
  onRetry: resetToInput,
2905
+ onRefresh: handleRefresh,
2838
2906
  onClose
2839
2907
  }
2840
2908
  ) });
@@ -3507,7 +3575,6 @@ var PROVIDER_LABELS = {
3507
3575
  soroswap: "Soroswap",
3508
3576
  sdex: "Stellar DEX"
3509
3577
  };
3510
- var IMPLEMENTED_PROVIDERS = ["auto", "aquarius"];
3511
3578
  function formatAmount2(value) {
3512
3579
  const n = parseFloat(value);
3513
3580
  return isNaN(n) ? value : n.toLocaleString(void 0, { maximumFractionDigits: 7 });
@@ -3530,6 +3597,9 @@ function SwapModalTemplate({
3530
3597
  formError,
3531
3598
  isLoadingData,
3532
3599
  smartUnsupported,
3600
+ configLoading,
3601
+ swapUnavailable,
3602
+ buyNeedsTrustline,
3533
3603
  transaction,
3534
3604
  showXdr,
3535
3605
  copied,
@@ -3539,8 +3609,10 @@ function SwapModalTemplate({
3539
3609
  isInProgress,
3540
3610
  onClose,
3541
3611
  onBack,
3612
+ onRefresh,
3542
3613
  onSelectSell,
3543
3614
  onSelectBuy,
3615
+ onAddCustomToken,
3544
3616
  onAmountChange,
3545
3617
  onProviderChange,
3546
3618
  onSwap,
@@ -3550,6 +3622,21 @@ function SwapModalTemplate({
3550
3622
  onDone
3551
3623
  }) {
3552
3624
  const isDark = theme === "dark";
3625
+ const [customOpen, setCustomOpen] = react.useState(false);
3626
+ const [customCode, setCustomCode] = react.useState("");
3627
+ const [customIssuer, setCustomIssuer] = react.useState("");
3628
+ const [customError, setCustomError] = react.useState("");
3629
+ const submitCustomToken = () => {
3630
+ const err = onAddCustomToken(customCode, customIssuer);
3631
+ if (err) {
3632
+ setCustomError(err);
3633
+ return;
3634
+ }
3635
+ setCustomCode("");
3636
+ setCustomIssuer("");
3637
+ setCustomError("");
3638
+ setCustomOpen(false);
3639
+ };
3553
3640
  const cssVars = {
3554
3641
  "--pollar-accent": accentColor,
3555
3642
  "--pollar-bg": isDark ? "#1a1a1a" : "#ffffff",
@@ -3577,9 +3664,34 @@ function SwapModalTemplate({
3577
3664
  showBack && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "pollar-modal-close", onClick: onBack, "aria-label": "Back", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10 3L5 8l5 5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }),
3578
3665
  /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "pollar-modal-title", children: title })
3579
3666
  ] }),
3580
- !isInProgress && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pollar-modal-header-actions", children: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "pollar-modal-close", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2 2l12 12M14 2L2 14", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) }) }) })
3667
+ !isInProgress && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pollar-modal-header-actions", children: [
3668
+ step === "form" && /* @__PURE__ */ jsxRuntime.jsx(
3669
+ "button",
3670
+ {
3671
+ type: "button",
3672
+ className: "pollar-modal-close",
3673
+ onClick: onRefresh,
3674
+ disabled: configLoading || isLoadingData,
3675
+ "aria-label": "Refresh",
3676
+ title: "Refresh balances and options",
3677
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx(
3678
+ "path",
3679
+ {
3680
+ d: "M13.5 8a5.5 5.5 0 1 1-1.6-3.9M13.5 2.5v3h-3",
3681
+ stroke: "currentColor",
3682
+ strokeWidth: "1.6",
3683
+ strokeLinecap: "round",
3684
+ strokeLinejoin: "round"
3685
+ }
3686
+ ) })
3687
+ }
3688
+ ),
3689
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "pollar-modal-close", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2 2l12 12M14 2L2 14", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) }) })
3690
+ ] })
3581
3691
  ] }),
3582
- step === "form" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3692
+ step === "form" && configLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pollar-send-hint", children: "Loading swap options\u2026" }),
3693
+ step === "form" && !configLoading && swapUnavailable && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pollar-modal-error", children: "Swap is not available for this app." }),
3694
+ step === "form" && !configLoading && !swapUnavailable && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3583
3695
  smartUnsupported && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pollar-modal-error", children: "Swaps are not yet available for smart (passkey) wallets." }),
3584
3696
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pollar-send-field", children: [
3585
3697
  /* @__PURE__ */ jsxRuntime.jsx("label", { className: "pollar-send-label", children: "You pay" }),
@@ -3643,7 +3755,45 @@ function SwapModalTemplate({
3643
3755
  ] }, assetOptionKey(o)))
3644
3756
  ]
3645
3757
  }
3646
- )
3758
+ ),
3759
+ !isLoadingData && (customOpen ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pollar-swap-custom", children: [
3760
+ /* @__PURE__ */ jsxRuntime.jsx(
3761
+ "input",
3762
+ {
3763
+ className: "pollar-input",
3764
+ type: "text",
3765
+ placeholder: "Code (e.g. USDC)",
3766
+ value: customCode,
3767
+ onChange: (e) => setCustomCode(e.target.value)
3768
+ }
3769
+ ),
3770
+ /* @__PURE__ */ jsxRuntime.jsx(
3771
+ "input",
3772
+ {
3773
+ className: "pollar-input",
3774
+ type: "text",
3775
+ placeholder: "Issuer (G...)",
3776
+ value: customIssuer,
3777
+ onChange: (e) => setCustomIssuer(e.target.value)
3778
+ }
3779
+ ),
3780
+ customError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pollar-modal-error", children: customError }),
3781
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pollar-swap-custom-actions", children: [
3782
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "pollar-btn-primary", onClick: submitCustomToken, children: "Add token" }),
3783
+ /* @__PURE__ */ jsxRuntime.jsx(
3784
+ "button",
3785
+ {
3786
+ type: "button",
3787
+ className: "pollar-swap-custom-cancel",
3788
+ onClick: () => {
3789
+ setCustomOpen(false);
3790
+ setCustomError("");
3791
+ },
3792
+ children: "Cancel"
3793
+ }
3794
+ )
3795
+ ] })
3796
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "pollar-swap-custom-toggle", onClick: () => setCustomOpen(true), children: "+ Add a token by code / issuer" }))
3647
3797
  ] }),
3648
3798
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pollar-send-field", children: [
3649
3799
  /* @__PURE__ */ jsxRuntime.jsx("label", { className: "pollar-send-label", children: "Route" }),
@@ -3653,13 +3803,7 @@ function SwapModalTemplate({
3653
3803
  className: "pollar-input pollar-send-select",
3654
3804
  value: provider,
3655
3805
  onChange: (e) => onProviderChange(e.target.value),
3656
- children: providers.map((p) => {
3657
- const implemented = IMPLEMENTED_PROVIDERS.includes(p);
3658
- return /* @__PURE__ */ jsxRuntime.jsxs("option", { value: p, disabled: !implemented, children: [
3659
- PROVIDER_LABELS[p],
3660
- implemented ? "" : " (coming soon)"
3661
- ] }, p);
3662
- })
3806
+ children: providers.map((p) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: p, children: PROVIDER_LABELS[p] }, p))
3663
3807
  }
3664
3808
  )
3665
3809
  ] }),
@@ -3696,8 +3840,13 @@ function SwapModalTemplate({
3696
3840
  ] })
3697
3841
  ] }),
3698
3842
  !quoteLoading && !quoteError && !quote && selectedSell && selectedBuy && !!amount && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pollar-send-hint", children: "No route found for this pair." }),
3843
+ buyNeedsTrustline && selectedBuy && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pollar-swap-trustline-notice", children: [
3844
+ "To receive ",
3845
+ selectedBuy.code,
3846
+ " your wallet needs a trustline. Swapping will create it first (~0.5 XLM reserve, refundable if you later remove it)."
3847
+ ] }),
3699
3848
  formError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pollar-modal-error", children: formError }),
3700
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pollar-modal-actions", children: /* @__PURE__ */ jsxRuntime.jsx("button", { className: "pollar-btn-primary", onClick: onSwap, disabled: !canSwap, children: "Swap" }) })
3849
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pollar-modal-actions", children: /* @__PURE__ */ jsxRuntime.jsx("button", { className: "pollar-btn-primary", onClick: onSwap, disabled: !canSwap, children: buyNeedsTrustline ? "Create trustline & swap" : "Swap" }) })
3701
3850
  ] }),
3702
3851
  step === "tx" && /* @__PURE__ */ jsxRuntime.jsx(
3703
3852
  TxStatusView,
@@ -3719,14 +3868,18 @@ function SwapModalTemplate({
3719
3868
  ] });
3720
3869
  }
3721
3870
  var QUOTE_DEBOUNCE_MS = 400;
3722
- var ALL_PROVIDERS = ["auto", "aquarius", "soroswap", "sdex"];
3723
3871
  function toRef(a) {
3724
3872
  if (a.type === "native") return { type: "native" };
3725
3873
  if (a.type === "credit_alphanum4") return { type: "credit_alphanum4", code: a.code, issuer: a.issuer };
3726
3874
  return { type: "credit_alphanum12", code: a.code, issuer: a.issuer };
3727
3875
  }
3876
+ function catalogRef(code, issuer) {
3877
+ return code.length <= 4 ? { type: "credit_alphanum4", code, issuer } : { type: "credit_alphanum12", code, issuer };
3878
+ }
3728
3879
  function SwapModal({ onClose }) {
3729
3880
  const {
3881
+ getSwapConfig,
3882
+ getSwapTokens,
3730
3883
  getSwapQuote,
3731
3884
  swap,
3732
3885
  walletBalance,
@@ -3746,6 +3899,9 @@ function SwapModal({ onClose }) {
3746
3899
  const [selectedBuy, setSelectedBuy] = react.useState(null);
3747
3900
  const [amount, setAmount] = react.useState("");
3748
3901
  const [provider, setProvider] = react.useState("auto");
3902
+ const [venues, setVenues] = react.useState(null);
3903
+ const [catalogTokens, setCatalogTokens] = react.useState([]);
3904
+ const [customTokens, setCustomTokens] = react.useState([]);
3749
3905
  const [quotes, setQuotes] = react.useState([]);
3750
3906
  const [quoteLoading, setQuoteLoading] = react.useState(false);
3751
3907
  const [quoteError, setQuoteError] = react.useState("");
@@ -3758,6 +3914,26 @@ function SwapModal({ onClose }) {
3758
3914
  void refreshWalletBalance();
3759
3915
  void refreshAssets();
3760
3916
  }, [refreshWalletBalance, refreshAssets]);
3917
+ const loadConfig = react.useCallback(() => {
3918
+ setVenues(null);
3919
+ return getSwapConfig().then(setVenues).catch(() => setVenues([]));
3920
+ }, [getSwapConfig]);
3921
+ react.useEffect(() => {
3922
+ void loadConfig();
3923
+ }, [loadConfig]);
3924
+ const loadCatalog = react.useCallback(
3925
+ () => getSwapTokens().then(setCatalogTokens).catch(() => setCatalogTokens([])),
3926
+ [getSwapTokens]
3927
+ );
3928
+ react.useEffect(() => {
3929
+ void loadCatalog();
3930
+ }, [loadCatalog]);
3931
+ function handleRefresh() {
3932
+ void refreshWalletBalance();
3933
+ void refreshAssets();
3934
+ void loadConfig();
3935
+ void loadCatalog();
3936
+ }
3761
3937
  react.useEffect(
3762
3938
  () => () => {
3763
3939
  if (copyTimerRef.current !== null) clearTimeout(copyTimerRef.current);
@@ -3769,11 +3945,47 @@ function SwapModal({ onClose }) {
3769
3945
  const isLoadingData = walletBalance.step === "loading" || enabledAssets.step === "loading";
3770
3946
  const sellOptions = balances.filter((b) => parseFloat(b.available) > 0).map((b) => ({ ref: toRef(b), code: b.code, issuer: b.issuer, available: b.available, enabledInApp: b.enabledInApp }));
3771
3947
  const buyKeyOfSell = selectedSell ? `${selectedSell.code}:${selectedSell.issuer ?? "native"}` : "";
3772
- const buyOptions = assetRecords.map((a) => ({ ref: toRef(a), code: a.code, issuer: a.issuer, enabledInApp: a.enabledInApp })).filter((o) => `${o.code}:${o.issuer ?? "native"}` !== buyKeyOfSell);
3773
- const providers = smartUnsupported ? ALL_PROVIDERS.filter((p) => p !== "sdex") : ALL_PROVIDERS;
3948
+ const optKey = (o) => `${o.code}:${o.issuer ?? "native"}`;
3949
+ const enabledBuy = assetRecords.map((a) => ({
3950
+ ref: toRef(a),
3951
+ code: a.code,
3952
+ issuer: a.issuer,
3953
+ enabledInApp: a.enabledInApp
3954
+ }));
3955
+ const enabledKeys = new Set(enabledBuy.map(optKey));
3956
+ const catalogBuy = catalogTokens.map((tk) => ({
3957
+ ref: catalogRef(tk.code, tk.issuer),
3958
+ code: tk.code,
3959
+ issuer: tk.issuer,
3960
+ enabledInApp: false
3961
+ })).filter((o) => !enabledKeys.has(optKey(o)));
3962
+ const knownKeys = /* @__PURE__ */ new Set([...enabledKeys, ...catalogBuy.map(optKey)]);
3963
+ const customBuy = customTokens.filter((o) => !knownKeys.has(optKey(o)));
3964
+ const buyOptions = [...enabledBuy, ...catalogBuy, ...customBuy].filter(
3965
+ (o) => optKey(o) !== buyKeyOfSell
3966
+ );
3967
+ function addCustomToken(code, issuer) {
3968
+ const c = code.trim().toUpperCase();
3969
+ const i = issuer.trim();
3970
+ if (c.length < 1 || c.length > 12) return "Code must be 1-12 characters";
3971
+ if (i.length !== 56 || !i.startsWith("G")) return "Issuer must be a Stellar address (G..., 56 chars)";
3972
+ const opt = { ref: catalogRef(c, i), code: c, issuer: i, enabledInApp: false };
3973
+ setCustomTokens((prev) => prev.some((o) => optKey(o) === optKey(opt)) ? prev : [...prev, opt]);
3974
+ setSelectedBuy(opt);
3975
+ return null;
3976
+ }
3977
+ const buyNeedsTrustline = (() => {
3978
+ if (!selectedBuy || smartUnsupported) return false;
3979
+ if (selectedBuy.ref.type === "native") return false;
3980
+ const rec = assetRecords.find((a) => a.code === selectedBuy.code && a.issuer === selectedBuy.issuer);
3981
+ return !rec?.trustlineEstablished;
3982
+ })();
3983
+ const configLoading = venues === null;
3984
+ const swapUnavailable = venues !== null && venues.length === 0;
3985
+ const providers = venues && venues.length > 0 ? ["auto", ...venues] : [];
3774
3986
  const quote = quotes[0] ?? null;
3775
3987
  react.useEffect(() => {
3776
- if (step !== "form" || !selectedSell || !selectedBuy || !amount || parseFloat(amount) <= 0) {
3988
+ if (step !== "form" || swapUnavailable || configLoading || !selectedSell || !selectedBuy || !amount || parseFloat(amount) <= 0) {
3777
3989
  setQuotes([]);
3778
3990
  setQuoteError("");
3779
3991
  setQuoteLoading(false);
@@ -3805,7 +4017,7 @@ function SwapModal({ onClose }) {
3805
4017
  cancelled = true;
3806
4018
  clearTimeout(t);
3807
4019
  };
3808
- }, [step, selectedSell, selectedBuy, amount, provider, getSwapQuote]);
4020
+ }, [step, swapUnavailable, configLoading, selectedSell, selectedBuy, amount, provider, getSwapQuote]);
3809
4021
  const hash = transaction.step === "success" ? transaction.hash : null;
3810
4022
  const buildData = "buildData" in transaction ? transaction.buildData : null;
3811
4023
  const explorerNetwork = buildData?.summary.network?.toLowerCase().includes("testnet") ? "testnet" : buildData ? "public" : network === "testnet" ? "testnet" : "public";
@@ -3886,6 +4098,9 @@ function SwapModal({ onClose }) {
3886
4098
  formError,
3887
4099
  isLoadingData,
3888
4100
  smartUnsupported,
4101
+ configLoading,
4102
+ swapUnavailable,
4103
+ buyNeedsTrustline,
3889
4104
  transaction,
3890
4105
  showXdr,
3891
4106
  copied,
@@ -3895,8 +4110,10 @@ function SwapModal({ onClose }) {
3895
4110
  isInProgress,
3896
4111
  onClose,
3897
4112
  onBack: handleBack,
4113
+ onRefresh: handleRefresh,
3898
4114
  onSelectSell: setSelectedSell,
3899
4115
  onSelectBuy: setSelectedBuy,
4116
+ onAddCustomToken: addCustomToken,
3900
4117
  onAmountChange: setAmount,
3901
4118
  onProviderChange: setProvider,
3902
4119
  onSwap: () => void handleSwap(),
@@ -4608,6 +4825,18 @@ function PollarProvider({
4608
4825
  const [pollarClient] = react.useState(
4609
4826
  () => client instanceof core.PollarClient ? client : new core.PollarClient({ passkey: browserPasskeyCeremony, passkeySign: browserPasskeySigner, ...client })
4610
4827
  );
4828
+ const ownsClientRef = react.useRef(!(client instanceof core.PollarClient));
4829
+ const destroyTimerRef = react.useRef(null);
4830
+ react.useEffect(() => {
4831
+ if (!ownsClientRef.current) return;
4832
+ if (destroyTimerRef.current) {
4833
+ clearTimeout(destroyTimerRef.current);
4834
+ destroyTimerRef.current = null;
4835
+ }
4836
+ return () => {
4837
+ destroyTimerRef.current = setTimeout(() => pollarClient.destroy(), 0);
4838
+ };
4839
+ }, [pollarClient]);
4611
4840
  const [networkState, setNetworkState] = react.useState(() => pollarClient.getNetworkState());
4612
4841
  const [sessionState, setSessionState] = react.useState(null);
4613
4842
  const [verified, setVerified] = react.useState(false);
@@ -4747,6 +4976,8 @@ function PollarProvider({
4747
4976
  openSendModal: () => setSendModalOpen(true),
4748
4977
  openReceiveModal: () => setReceiveModalOpen(true),
4749
4978
  // swap
4979
+ getSwapConfig: () => pollarClient.getSwapConfig(),
4980
+ getSwapTokens: () => pollarClient.getSwapTokens(),
4750
4981
  getSwapQuote: (params) => pollarClient.getSwapQuote(params),
4751
4982
  swap: (quote, opts) => pollarClient.swap(quote, opts),
4752
4983
  openSwapModal: () => setSwapModalOpen(true),