@pollar/react 0.11.5 → 0.12.0-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { AUTH_ERROR_CODES, transactionErrorMessage, WalletType, cosignGroupOf, cosignStepKeyOf, stepInGroup, isPollarApiError, isPollarClient, PollarClient, isInteractiveAuthAdapter, toBaseUnits } from '@pollar/core';
2
+ import { AUTH_ERROR_CODES, rampLegTitle, rampRequirementCopy, rampTrustlineAsset, rampWalletStep, rampResolutionText, describeRampAction, rampLegStatus, rampNextApproval, rampOrderStopped, rampOrderReceive, rampRailLabel, rampNoRouteText, rampFieldError, rampFieldValid, rampLegNote, rampStepTitle, rampStepStatusText, rampLegTimeLeft, rampOrderResolutionText, rampCountries, rampQuoteFields, isPollarApiError, transactionErrorMessage, WalletType, isPollarClient, PollarClient, isInteractiveAuthAdapter, rampTrustlineErrorText, toBaseUnits, appUnavailableMessage, cosignRefusalMessage, fromBaseUnits, assertRampStellarSigner } from '@pollar/core';
3
3
  import { forwardRef, createContext, useState, useCallback, useEffect, useMemo, useRef, useContext, Component } from 'react';
4
4
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
5
  import { startAuthentication, startRegistration } from '@simplewebauthn/browser';
@@ -999,7 +999,6 @@ var require_QRCode = __commonJS({
999
999
  });
1000
1000
 
1001
1001
  // src/constants.ts
1002
- var LOGO_POLLAR = "https://pollar.xyz/assets/logo_pollar.png";
1003
1002
  var LOGO_POLLAR_WORDMARK_ON_LIGHT = "https://www.pollar.xyz/brandkit/pollar-logo-horizontal-on-light.png";
1004
1003
  var LOGO_POLLAR_WORDMARK_ON_DARK = "https://www.pollar.xyz/brandkit/pollar-logo-horizontal-on-dark.png";
1005
1004
  var LOGO_POLLAR_MARK_ON_LIGHT = "https://www.pollar.xyz/brandkit/pollar-mark-on-light.png";
@@ -1029,6 +1028,13 @@ var ModalErrorBoundary = class extends Component {
1029
1028
  return this.props.children;
1030
1029
  }
1031
1030
  };
1031
+ function ModalLogo({ logoUrl }) {
1032
+ if (logoUrl) return /* @__PURE__ */ jsx("img", { src: logoUrl, alt: "Logo", className: "pollar-logo" });
1033
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1034
+ /* @__PURE__ */ jsx("img", { src: LOGO_POLLAR_MARK_ON_LIGHT, alt: "Pollar", className: "pollar-logo pollar-logo-on-light" }),
1035
+ /* @__PURE__ */ jsx("img", { src: LOGO_POLLAR_MARK_ON_DARK, alt: "Pollar", className: "pollar-logo pollar-logo-on-dark" })
1036
+ ] });
1037
+ }
1032
1038
  var PollarModalFooter = () => {
1033
1039
  return /* @__PURE__ */ jsxs("div", { className: "pollar-footer", children: [
1034
1040
  /* @__PURE__ */ jsx("span", { className: "pollar-footer-protected", children: "Protected by" }),
@@ -1036,7 +1042,7 @@ var PollarModalFooter = () => {
1036
1042
  /* @__PURE__ */ jsx("img", { src: LOGO_POLLAR_WORDMARK_ON_DARK, alt: "Pollar", className: "pollar-footer-logo pollar-logo-on-dark" }),
1037
1043
  /* @__PURE__ */ jsxs("span", { className: "pollar-footer-version", children: [
1038
1044
  "v",
1039
- "0.11.5"
1045
+ "0.12.0-rc.5"
1040
1046
  ] })
1041
1047
  ] });
1042
1048
  };
@@ -1218,42 +1224,75 @@ function readableTextOn(background) {
1218
1224
  const [r, g, b] = rgb;
1219
1225
  return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 > 0.6 ? "#111827" : "#ffffff";
1220
1226
  }
1227
+ var PALETTES = {
1228
+ light: {
1229
+ bg: "#ffffff",
1230
+ border: "#e5e7eb",
1231
+ text: "#111827",
1232
+ muted: "#6b7280",
1233
+ "input-bg": "#f9fafb",
1234
+ "error-bg": "#fef2f2",
1235
+ "error-border": "#fecaca",
1236
+ "error-text": "#dc2626",
1237
+ "success-text": "#16a34a"
1238
+ },
1239
+ dark: {
1240
+ bg: "#1a1a1a",
1241
+ border: "#374151",
1242
+ text: "#ffffff",
1243
+ muted: "#9ca3af",
1244
+ "input-bg": "#374151",
1245
+ "error-bg": "#2a1515",
1246
+ "error-border": "#7f1d1d",
1247
+ "error-text": "#f87171",
1248
+ "success-text": "#4ade80"
1249
+ }
1250
+ };
1251
+ function mix(fg, percent, bg) {
1252
+ return `color-mix(in srgb, ${fg} ${percent}%, ${bg})`;
1253
+ }
1221
1254
  function buildModalCssVars(theme, accentColor, overrides = {}, variant = "compact") {
1222
1255
  const isDark = theme === "dark";
1223
- const textColor = color(overrides.textColor, isDark ? "#ffffff" : "#111827");
1256
+ const palette = isDark ? PALETTES.dark : PALETTES.light;
1257
+ const bg = color(overrides.backgroundColor, "");
1258
+ const text = color(overrides.textColor, "");
1259
+ const pinned = isDark || bg !== "" || text !== "";
1224
1260
  const secondaryBg = color(overrides.buttonColor, "");
1225
- return {
1226
- "--pollar-accent": accentColor,
1227
- "--pollar-bg": color(overrides.backgroundColor, isDark ? "#1a1a1a" : "#ffffff"),
1228
- "--pollar-border": isDark ? "#374151" : "#e5e7eb",
1229
- "--pollar-text": textColor,
1230
- "--pollar-muted": isDark ? "#9ca3af" : "#6b7280",
1261
+ const vars = {
1262
+ "--pollar-brand-accent": accentColor,
1231
1263
  // The primary button is the accent's, straight up; its label follows the
1232
1264
  // accent's luminance so a pale accent doesn't render white-on-white.
1233
- "--pollar-btn-primary-fg": readableTextOn(accentColor),
1234
- ...secondaryBg ? {
1235
- "--pollar-btn-secondary-bg": secondaryBg,
1236
- "--pollar-btn-secondary-fg": readableTextOn(secondaryBg)
1237
- } : {},
1238
- "--pollar-input-bg": isDark ? "#374151" : "#f9fafb",
1239
- "--pollar-error-bg": isDark ? "#2a1515" : "#fef2f2",
1240
- "--pollar-error-border": isDark ? "#7f1d1d" : "#fecaca",
1241
- "--pollar-error-text": isDark ? "#f87171" : "#dc2626",
1242
- "--pollar-success-text": isDark ? "#4ade80" : "#16a34a",
1243
- "--pollar-logo-on-light-display": isDark ? "none" : "block",
1244
- "--pollar-logo-on-dark-display": isDark ? "block" : "none",
1245
- "--pollar-modal-border-radius": px(overrides.modalBorderRadius, DEFAULT_MODAL_BORDER_RADIUS),
1246
- "--pollar-buttons-border-radius": px(overrides.buttonBorderRadius, DEFAULT_BUTTON_BORDER_RADIUS),
1247
- "--pollar-buttons-height": "44px",
1248
- "--pollar-input-height": "44px",
1249
- "--pollar-input-border-radius": "0.5rem",
1250
- "--pollar-card-border-radius": "10px",
1251
- ...variant === "hero" ? {
1252
- "--pollar-modal-padding": "2rem",
1253
- "--pollar-modal-heading-size": "1.375rem",
1254
- "--pollar-modal-subtitle-size": "0.9rem"
1255
- } : {}
1265
+ "--pollar-brand-btn-primary-fg": readableTextOn(accentColor),
1266
+ "--pollar-brand-modal-border-radius": px(overrides.modalBorderRadius, DEFAULT_MODAL_BORDER_RADIUS),
1267
+ "--pollar-brand-buttons-border-radius": px(overrides.buttonBorderRadius, DEFAULT_BUTTON_BORDER_RADIUS)
1256
1268
  };
1269
+ if (secondaryBg) {
1270
+ vars["--pollar-brand-btn-secondary-bg"] = secondaryBg;
1271
+ vars["--pollar-brand-btn-secondary-fg"] = readableTextOn(secondaryBg);
1272
+ }
1273
+ if (pinned) {
1274
+ for (const [name, value] of Object.entries(palette)) vars[`--pollar-brand-${name}`] = value;
1275
+ vars["--pollar-brand-logo-on-light-display"] = isDark ? "none" : "block";
1276
+ vars["--pollar-brand-logo-on-dark-display"] = isDark ? "block" : "none";
1277
+ }
1278
+ if (bg || text) {
1279
+ const surface = bg || palette.bg;
1280
+ const ink = text || palette.text;
1281
+ vars["--pollar-brand-bg"] = surface;
1282
+ vars["--pollar-brand-text"] = ink;
1283
+ vars["--pollar-brand-border"] = mix(ink, 14, surface);
1284
+ vars["--pollar-brand-muted"] = mix(ink, 62, surface);
1285
+ vars["--pollar-brand-input-bg"] = mix(ink, 4, surface);
1286
+ const darkSurface = readableTextOn(surface) === "#ffffff";
1287
+ vars["--pollar-brand-logo-on-light-display"] = darkSurface ? "none" : "block";
1288
+ vars["--pollar-brand-logo-on-dark-display"] = darkSurface ? "block" : "none";
1289
+ }
1290
+ if (variant === "hero") {
1291
+ vars["--pollar-brand-modal-padding"] = "2rem";
1292
+ vars["--pollar-brand-modal-heading-size"] = "1.375rem";
1293
+ vars["--pollar-brand-modal-subtitle-size"] = "0.9rem";
1294
+ }
1295
+ return vars;
1257
1296
  }
1258
1297
  function modalChrome(styles) {
1259
1298
  const zIndex = styles.modalZIndex;
@@ -1268,7 +1307,7 @@ function modalChrome(styles) {
1268
1307
  buttonBorderRadius: styles.buttonBorderRadius
1269
1308
  },
1270
1309
  overlayStyle: {
1271
- "--pollar-overlay-z-index": typeof zIndex === "number" && Number.isFinite(zIndex) ? zIndex : DEFAULT_OVERLAY_Z_INDEX
1310
+ "--pollar-brand-overlay-z-index": typeof zIndex === "number" && Number.isFinite(zIndex) ? zIndex : DEFAULT_OVERLAY_Z_INDEX
1272
1311
  }
1273
1312
  };
1274
1313
  }
@@ -1913,7 +1952,7 @@ function KycStatus({ status, label, className }) {
1913
1952
 
1914
1953
  // src/components/kyc-modal/kyc-messages.ts
1915
1954
  function kycErrorMessage(error, phase) {
1916
- const code = error?.code;
1955
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
1917
1956
  switch (code) {
1918
1957
  case "SDK_KYC_NOT_CONFIGURED":
1919
1958
  return "Identity verification is not available for this app yet.";
@@ -1953,6 +1992,8 @@ function KycModalTemplate({
1953
1992
  reviewReason,
1954
1993
  processing = false,
1955
1994
  isLoading,
1995
+ startingProviderId = null,
1996
+ directOpen = false,
1956
1997
  error,
1957
1998
  onSelectProvider,
1958
1999
  onDoneVerifying,
@@ -1961,6 +2002,7 @@ function KycModalTemplate({
1961
2002
  onClose
1962
2003
  }) {
1963
2004
  const cssVars = buildModalCssVars(theme, accentColor, styleOverrides, "hero");
2005
+ const busy = isLoading || startingProviderId !== null;
1964
2006
  return /* @__PURE__ */ jsxs(
1965
2007
  "div",
1966
2008
  {
@@ -1988,7 +2030,7 @@ function KycModalTemplate({
1988
2030
  type: "button",
1989
2031
  className: "pollar-modal-close",
1990
2032
  onClick: onRefresh,
1991
- disabled: isLoading,
2033
+ disabled: busy,
1992
2034
  "aria-label": "Refresh",
1993
2035
  title: "Refresh providers",
1994
2036
  children: /* @__PURE__ */ jsx(
@@ -2018,7 +2060,14 @@ function KycModalTemplate({
2018
2060
  ] })
2019
2061
  ] }),
2020
2062
  error && /* @__PURE__ */ jsx("p", { className: "pollar-kyc-error", role: "alert", children: error }),
2021
- step === "select_provider" && (isLoading && providers.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "pollar-loading-block", children: [
2063
+ step === "select_provider" && (startingProviderId !== null && directOpen ? /* @__PURE__ */ jsxs("div", { className: "pollar-loading-block", "aria-live": "polite", children: [
2064
+ /* @__PURE__ */ jsx("div", { className: "pollar-spinner" }),
2065
+ /* @__PURE__ */ jsxs("span", { children: [
2066
+ "Opening ",
2067
+ selectedProvider?.name ?? "verification",
2068
+ "\u2026"
2069
+ ] })
2070
+ ] }) : isLoading && providers.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "pollar-loading-block", children: [
2022
2071
  /* @__PURE__ */ jsx("div", { className: "pollar-spinner" }),
2023
2072
  /* @__PURE__ */ jsx("span", { children: "Loading providers\u2026" })
2024
2073
  ] }) : /* @__PURE__ */ jsxs("div", { className: "pollar-kyc-providers", children: [
@@ -2028,11 +2077,15 @@ function KycModalTemplate({
2028
2077
  {
2029
2078
  type: "button",
2030
2079
  className: "pollar-kyc-provider-btn",
2031
- disabled: isLoading,
2080
+ disabled: busy,
2081
+ "aria-busy": startingProviderId === p.id,
2032
2082
  onClick: () => onSelectProvider(p),
2033
2083
  children: [
2034
2084
  /* @__PURE__ */ jsx("span", { className: "pollar-kyc-provider-name", children: p.name }),
2035
- /* @__PURE__ */ jsx("span", { className: "pollar-kyc-provider-flow", children: isLoading && selectedProvider?.id === p.id ? "Opening\u2026" : p.flow === "redirect" ? "Continue in a new tab" : "Continue here" })
2085
+ /* @__PURE__ */ jsx("span", { className: "pollar-kyc-provider-flow", children: startingProviderId === p.id ? /* @__PURE__ */ jsxs(Fragment, { children: [
2086
+ /* @__PURE__ */ jsx("span", { className: "pollar-spinner pollar-spinner-sm" }),
2087
+ "Opening\u2026"
2088
+ ] }) : p.flow === "redirect" ? "Continue in a new tab" : "Continue here" })
2036
2089
  ]
2037
2090
  },
2038
2091
  p.id
@@ -2083,7 +2136,7 @@ function KycModalTemplate({
2083
2136
  /* @__PURE__ */ jsx(KycStatus, { status: kycStatus, ...processing ? { label: "Approved" } : {} }),
2084
2137
  /* @__PURE__ */ jsx("p", { className: "pollar-kyc-result-text", children: kycStatus === "approved" ? "Your identity has been verified successfully." : kycStatus === "rejected" ? "Your verification was not approved. Contact support for the next steps." : kycStatus === "expired" ? "Your verification expired. Start again to verify your identity." : processing ? kycProcessingMessage() : kycReviewMessage(reviewReason) }),
2085
2138
  /* @__PURE__ */ jsxs("div", { className: "pollar-modal-actions", children: [
2086
- kycStatus === "expired" && onStartAgain && /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-btn-secondary", onClick: onStartAgain, disabled: isLoading, children: "Start again" }),
2139
+ kycStatus === "expired" && onStartAgain && /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-btn-secondary", onClick: onStartAgain, disabled: busy, children: "Start again" }),
2087
2140
  kycStatus !== "approved" && kycStatus !== "rejected" && kycStatus !== "expired" && /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-btn-secondary", onClick: onDoneVerifying, children: "Check again" }),
2088
2141
  /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-btn-primary", onClick: onClose, children: "Close" })
2089
2142
  ] })
@@ -2116,6 +2169,8 @@ function KycModal({
2116
2169
  const [reviewReason, setReviewReason] = useState(null);
2117
2170
  const [processing, setProcessing] = useState(false);
2118
2171
  const [isLoading, setIsLoading] = useState(false);
2172
+ const [startingProviderId, setStartingProviderId] = useState(null);
2173
+ const [directOpen, setDirectOpen] = useState(false);
2119
2174
  const [error, setError] = useState(null);
2120
2175
  const idempotencyKeys = useRef({});
2121
2176
  const autoOpened = useRef(false);
@@ -2143,7 +2198,7 @@ function KycModal({
2143
2198
  starting.current = true;
2144
2199
  setSelectedProvider(provider);
2145
2200
  setError(null);
2146
- setIsLoading(true);
2201
+ setStartingProviderId(provider.id);
2147
2202
  const key = (_a = idempotencyKeys.current)[_b = provider.id] ?? (_a[_b] = newIdempotencyKey());
2148
2203
  try {
2149
2204
  const result = await client.resolveKyc(
@@ -2158,7 +2213,13 @@ function KycModal({
2158
2213
  finish("approved");
2159
2214
  return;
2160
2215
  }
2161
- setSession(result);
2216
+ const { sessionId } = result;
2217
+ if (sessionId === void 0) throw new Error("The KYC provider answered without a session.");
2218
+ setSession({
2219
+ sessionId,
2220
+ ...result.kycUrl !== void 0 ? { kycUrl: result.kycUrl } : {},
2221
+ ...result.fields !== void 0 ? { fields: result.fields } : {}
2222
+ });
2162
2223
  setStep("verifying");
2163
2224
  } catch (e) {
2164
2225
  const code = isPollarApiError(e) ? e.code : void 0;
@@ -2175,7 +2236,7 @@ function KycModal({
2175
2236
  setStep("select_provider");
2176
2237
  } finally {
2177
2238
  starting.current = false;
2178
- setIsLoading(false);
2239
+ setStartingProviderId(null);
2179
2240
  }
2180
2241
  }
2181
2242
  const loadProviders = useCallback(() => {
@@ -2194,6 +2255,7 @@ function KycModal({
2194
2255
  void loadProviders().then((list) => {
2195
2256
  if (!providerId || autoOpened.current) return;
2196
2257
  autoOpened.current = true;
2258
+ setDirectOpen(true);
2197
2259
  const target = list.find((p) => p.id === providerId);
2198
2260
  if (target) void handleSelectProvider(target);
2199
2261
  else
@@ -2243,6 +2305,8 @@ function KycModal({
2243
2305
  reviewReason,
2244
2306
  processing,
2245
2307
  isLoading,
2308
+ startingProviderId,
2309
+ directOpen,
2246
2310
  error,
2247
2311
  onSelectProvider: handleSelectProvider,
2248
2312
  onDoneVerifying: handleDoneVerifying,
@@ -2345,10 +2409,14 @@ function fieldErrorMessage(code, language) {
2345
2409
  }
2346
2410
  function fieldErrorsOf(error) {
2347
2411
  if (!error || typeof error !== "object") return null;
2348
- const { code, body } = error;
2349
- if (code !== "KYC_FORM_INVALID_ANSWERS" || !Array.isArray(body?.errors)) return null;
2412
+ if (!("code" in error) || error.code !== "KYC_FORM_INVALID_ANSWERS") return null;
2413
+ const body = "body" in error ? error.body : void 0;
2414
+ const list = typeof body === "object" && body !== null && "errors" in body ? body.errors : void 0;
2415
+ if (!Array.isArray(list)) return null;
2416
+ const items = list;
2350
2417
  const errors = {};
2351
- for (const item of body.errors) {
2418
+ for (const item of items) {
2419
+ if (typeof item !== "object" || item === null || !("key" in item) || !("code" in item)) continue;
2352
2420
  if (typeof item.key === "string" && typeof item.code === "string") errors[item.key] = item.code;
2353
2421
  }
2354
2422
  return Object.keys(errors).length ? errors : null;
@@ -2928,7 +2996,7 @@ function LoginModalTemplate({
2928
2996
  }
2929
2997
  ) }),
2930
2998
  /* @__PURE__ */ jsxs("div", { className: "pollar-header", children: [
2931
- /* @__PURE__ */ jsx("div", { className: "pollar-logo-wrap", children: /* @__PURE__ */ jsx("img", { src: logoUrl ?? LOGO_POLLAR, alt: "Logo", className: "pollar-logo" }) }),
2999
+ /* @__PURE__ */ jsx("div", { className: "pollar-logo-wrap", children: /* @__PURE__ */ jsx(ModalLogo, { logoUrl }) }),
2932
3000
  /* @__PURE__ */ jsx("h2", { className: "pollar-title", children: appName }),
2933
3001
  /* @__PURE__ */ jsx("p", { className: "pollar-subtitle", children: "Log in or sign up" })
2934
3002
  ] }),
@@ -3094,7 +3162,7 @@ function LoginModalStatus({
3094
3162
  }
3095
3163
  ) }),
3096
3164
  /* @__PURE__ */ jsxs("div", { className: "pollar-header", children: [
3097
- /* @__PURE__ */ jsx("div", { className: "pollar-logo-wrap", children: /* @__PURE__ */ jsx("img", { src: logoUrl ?? LOGO_POLLAR, alt: "Logo", className: "pollar-logo" }) }),
3165
+ /* @__PURE__ */ jsx("div", { className: "pollar-logo-wrap", children: /* @__PURE__ */ jsx(ModalLogo, { logoUrl }) }),
3098
3166
  /* @__PURE__ */ jsx("h2", { className: "pollar-title", children: appName }),
3099
3167
  /* @__PURE__ */ jsx("p", { className: "pollar-subtitle", children: "Log in or sign up" })
3100
3168
  ] }),
@@ -3196,7 +3264,7 @@ function PrivyLoginSubmodal({
3196
3264
  }
3197
3265
  ) }),
3198
3266
  /* @__PURE__ */ jsxs("div", { className: "pollar-header", children: [
3199
- /* @__PURE__ */ jsx("div", { className: "pollar-logo-wrap", children: /* @__PURE__ */ jsx("img", { src: logoUrl ?? LOGO_POLLAR, alt: "Logo", className: "pollar-logo" }) }),
3267
+ /* @__PURE__ */ jsx("div", { className: "pollar-logo-wrap", children: /* @__PURE__ */ jsx(ModalLogo, { logoUrl }) }),
3200
3268
  /* @__PURE__ */ jsx("h2", { className: "pollar-title", children: appName }),
3201
3269
  /* @__PURE__ */ jsx("p", { className: "pollar-subtitle", children: adapter.meta.label })
3202
3270
  ] }),
@@ -3397,69 +3465,45 @@ function LoginModal({ onClose }) {
3397
3465
  }
3398
3466
  ) });
3399
3467
  }
3400
-
3401
- // src/components/ramp-widget/ramp-kyc.ts
3402
- var nonEmpty = (value) => typeof value === "string" && !!value.trim();
3403
- function requiredRampKyc(error) {
3404
- if (!error || typeof error !== "object") return null;
3405
- const { code, body } = error;
3406
- if (code !== "SDK_RAMPS_KYC_REQUIRED" || !body || typeof body !== "object") return null;
3407
- const { rampProviderId, corridorId, requirementType, optionId, kycProviderId } = body;
3408
- if (!nonEmpty(rampProviderId) || !nonEmpty(corridorId)) return null;
3409
- if ((requirementType === "FORM" || requirementType === "REGISTRY_CHECK" || requirementType === "PROVIDER_REGISTRATION") && nonEmpty(optionId)) {
3410
- return { rampProviderId, corridorId, type: requirementType, optionId };
3411
- }
3412
- const kyc = nonEmpty(optionId) && requirementType === "KYC" ? optionId : kycProviderId;
3413
- return nonEmpty(kyc) ? { rampProviderId, corridorId, type: "KYC", optionId: kyc } : null;
3414
- }
3415
- function pendingFromQuote(requirement) {
3416
- return {
3417
- rampProviderId: requirement.rampProviderId,
3418
- corridorId: requirement.corridorId,
3419
- type: requirement.type,
3420
- optionId: requirement.optionId,
3421
- progress: { position: requirement.completed + 1, total: requirement.total }
3422
- };
3423
- }
3424
- function lockedRouteCopy(requirement) {
3425
- if (requirement.type === "FORM") return { message: "Answer a few questions to see this route", action: "Continue" };
3426
- if (requirement.type === "PROVIDER_REGISTRATION") {
3427
- return { message: "Register with the provider to see this route", action: "Continue" };
3428
- }
3429
- if (requirement.type === "REGISTRY_CHECK") {
3430
- return requirement.status === "pending" ? { message: "Your details are under review", action: null } : { message: "Confirm your details to see this route", action: "Confirm" };
3431
- }
3432
- switch (requirement.status) {
3433
- case "expired":
3434
- return { message: "Your verification expired", action: "Verify again" };
3435
- case "pending":
3436
- if (!requirement.reviewReason) return { message: "Verification in progress", action: "Continue" };
3437
- return {
3438
- message: requirement.reviewReason === "DUPLICATE_DOCUMENT" ? kycReviewMessage(requirement.reviewReason) : "Your verification is under review",
3439
- action: null
3440
- };
3441
- case "rejected":
3442
- return { message: "Verification was not approved", action: null };
3443
- default:
3444
- return { message: "Verify your identity to see this route", action: "Verify" };
3445
- }
3468
+ function findWalletBalance(balances, asset) {
3469
+ const { issuer } = asset;
3470
+ return balances.find((b) => {
3471
+ if (resolveChain(b.chain) !== asset.chain) return false;
3472
+ if (issuer === null) return b.type === "native" || !b.issuer && b.code === asset.code;
3473
+ if (b.code !== asset.code) return false;
3474
+ return issuer === void 0 || !b.issuer || b.issuer.toLowerCase() === issuer.toLowerCase();
3475
+ });
3446
3476
  }
3447
- var RAIL_LABELS = {
3448
- SPEI: "SPEI (Mexico)",
3449
- PIX: "PIX (Brazil)",
3450
- PSE: "PSE (Colombia)",
3451
- ACH: "ACH (US)"
3452
- };
3453
- function RouteDisplay({ quote, busy = false, disabled = false, onSelect }) {
3477
+ function useWalletAvailable(asset) {
3478
+ const { walletBalance, refreshWalletBalance } = usePollar();
3479
+ const wanted = !!asset;
3480
+ useEffect(() => {
3481
+ if (wanted && walletBalance.step === "idle") void refreshWalletBalance();
3482
+ }, [wanted, walletBalance.step, refreshWalletBalance]);
3483
+ if (!asset || walletBalance.step !== "loaded") return void 0;
3484
+ const record = findWalletBalance(walletBalance.data.balances, asset);
3485
+ return record ? record.available : "0";
3486
+ }
3487
+ var CUSTODY_LABEL = { COSIGNED: "co-signed", SELF_CUSTODY: "self-custody" };
3488
+ var assetOf = (node) => node.kind === "fiat" ? node.currency : node.code;
3489
+ function etaText(seconds) {
3490
+ if (seconds === null) return null;
3491
+ if (seconds < 90) return `~${Math.max(1, Math.round(seconds))} s`;
3492
+ if (seconds < 5400) return `~${Math.round(seconds / 60)} min`;
3493
+ return `~${Math.round(seconds / 3600)} h`;
3494
+ }
3495
+ function RouteDisplay({ quote, best = false, busy = false, disabled = false, onSelect }) {
3454
3496
  const inert = busy || disabled;
3455
3497
  const select = () => {
3456
3498
  if (!inert) onSelect(quote);
3457
3499
  };
3500
+ const first = quote.legs[0];
3501
+ const last = quote.legs[quote.legs.length - 1];
3458
3502
  return /* @__PURE__ */ jsxs(
3459
3503
  "div",
3460
3504
  {
3461
3505
  className: "pollar-ramp-route-card",
3462
- "data-recommended": quote.recommended,
3506
+ "data-recommended": best,
3463
3507
  "data-busy": busy || void 0,
3464
3508
  "data-disabled": disabled || void 0,
3465
3509
  role: "button",
@@ -3470,54 +3514,152 @@ function RouteDisplay({ quote, busy = false, disabled = false, onSelect }) {
3470
3514
  onKeyDown: (e) => e.key === "Enter" && select(),
3471
3515
  children: [
3472
3516
  /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-route-left", children: [
3473
- /* @__PURE__ */ jsx("span", { className: "pollar-ramp-route-provider", children: quote.provider }),
3474
- /* @__PURE__ */ jsx("span", { className: "pollar-ramp-route-meta", children: busy ? "Starting\u2026" : `${RAIL_LABELS[quote.rail] ?? quote.rail} \xB7 ${quote.protocol} \xB7 ${quote.estimatedTime}` })
3517
+ /* @__PURE__ */ jsx("span", { className: "pollar-ramp-route-provider", children: first && last ? `${quote.amountIn} ${assetOf(first.from)} \u2192 ~${quote.amountOut} ${assetOf(last.to)}` : `~${quote.amountOut}` }),
3518
+ /* @__PURE__ */ jsx("span", { className: "pollar-ramp-route-meta", children: busy ? "Starting\u2026" : [quote.legs.map((leg) => rampLegTitle(leg)).join(" \xB7 "), etaText(quote.etaSeconds)].filter(Boolean).join(" \xB7 ") })
3475
3519
  ] }),
3476
3520
  /* @__PURE__ */ jsx("div", { className: "pollar-ramp-route-right", children: busy ? /* @__PURE__ */ jsx("span", { className: "pollar-spinner pollar-spinner-sm" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
3477
3521
  /* @__PURE__ */ jsxs("span", { className: "pollar-ramp-route-fee", children: [
3478
- quote.fee,
3479
- "% fee"
3522
+ quote.legs.length === 1 ? "1 step" : `${quote.legs.length} steps`,
3523
+ " \xB7 fees included"
3480
3524
  ] }),
3481
- quote.recommended && /* @__PURE__ */ jsx("span", { className: "pollar-ramp-route-badge", children: "Best rate" })
3525
+ best && /* @__PURE__ */ jsx("span", { className: "pollar-ramp-route-badge", children: "Best rate" })
3482
3526
  ] }) })
3483
3527
  ]
3484
3528
  }
3485
3529
  );
3486
3530
  }
3487
- function LockedRouteDisplay({ requirement, disabled = false, onVerify }) {
3488
- const { message, action } = lockedRouteCopy(requirement);
3531
+ function RejectedRouteDisplay({
3532
+ rejection,
3533
+ disabled = false,
3534
+ trusting = false,
3535
+ walletBusy = false,
3536
+ custodyChoice = false,
3537
+ onVerify,
3538
+ onTrust,
3539
+ onWalletStep
3540
+ }) {
3541
+ const requirement = rejection.requirement;
3542
+ const copy = requirement ? rampRequirementCopy(requirement) : null;
3543
+ const trustAsset = !requirement && onTrust ? rampTrustlineAsset(rejection) : null;
3544
+ const step = !requirement ? rampWalletStep(rejection) : null;
3545
+ const fromMenu = step?.action === "create_wallet" && custodyChoice;
3546
+ const walletStep = onWalletStep && !fromMenu ? step : null;
3547
+ const picks = custodyChoice && step?.action !== "set_up_recovery" ? ["COSIGNED", "SELF_CUSTODY"] : [void 0];
3548
+ const meta = copy ? copy.message : fromMenu ? "You need a wallet to receive this. Create it from the wallet menu, then find the route again." : rampResolutionText(rejection.failure);
3549
+ const busy = trusting || !!walletStep && walletBusy;
3489
3550
  return /* @__PURE__ */ jsxs(
3490
3551
  "div",
3491
3552
  {
3492
3553
  className: "pollar-ramp-route-card",
3493
3554
  "data-locked": true,
3494
- "data-kyc-status": requirement.status,
3495
- "data-disabled": disabled || void 0,
3555
+ "data-kyc-status": requirement?.status,
3556
+ "data-disabled": !(copy?.action && onVerify) && !trustAsset && !walletStep || disabled && !busy || void 0,
3496
3557
  children: [
3497
3558
  /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-route-left", children: [
3498
- /* @__PURE__ */ jsx("span", { className: "pollar-ramp-route-provider", children: requirement.provider }),
3499
- /* @__PURE__ */ jsx("span", { className: "pollar-ramp-route-meta", children: message })
3559
+ /* @__PURE__ */ jsx("span", { className: "pollar-ramp-route-provider", children: rejection.legs.map((leg) => rampLegTitle(leg)).join(" \xB7 ") }),
3560
+ /* @__PURE__ */ jsx("span", { className: "pollar-ramp-route-meta", children: meta })
3500
3561
  ] }),
3501
- action && /* @__PURE__ */ jsx("div", { className: "pollar-ramp-route-right", children: /* @__PURE__ */ jsx(
3562
+ trustAsset && /* @__PURE__ */ jsx("div", { className: "pollar-ramp-route-right", children: /* @__PURE__ */ jsx(
3563
+ "button",
3564
+ {
3565
+ type: "button",
3566
+ className: "pollar-btn-secondary pollar-ramp-route-verify",
3567
+ disabled,
3568
+ "aria-busy": trusting,
3569
+ onClick: () => onTrust(trustAsset),
3570
+ children: trusting ? /* @__PURE__ */ jsxs(Fragment, { children: [
3571
+ /* @__PURE__ */ jsx("span", { className: "pollar-spinner pollar-spinner-sm" }),
3572
+ " Adding\u2026"
3573
+ ] }) : `Add ${trustAsset.code} trustline`
3574
+ }
3575
+ ) }),
3576
+ walletStep && /* @__PURE__ */ jsx("div", { className: "pollar-ramp-route-right", children: walletBusy ? /* @__PURE__ */ jsxs("button", { type: "button", className: "pollar-btn-secondary pollar-ramp-route-verify", disabled: true, "aria-busy": true, children: [
3577
+ /* @__PURE__ */ jsx("span", { className: "pollar-spinner pollar-spinner-sm" }),
3578
+ " ",
3579
+ walletStep.busyLabel
3580
+ ] }) : picks.map((custody) => /* @__PURE__ */ jsx(
3581
+ "button",
3582
+ {
3583
+ type: "button",
3584
+ className: "pollar-btn-secondary pollar-ramp-route-verify",
3585
+ disabled,
3586
+ onClick: () => onWalletStep(walletStep, custody),
3587
+ children: custody ? `${walletStep.label} (${CUSTODY_LABEL[custody]})` : walletStep.label
3588
+ },
3589
+ custody ?? "app"
3590
+ )) }),
3591
+ requirement && copy?.action && onVerify && /* @__PURE__ */ jsx("div", { className: "pollar-ramp-route-right", children: /* @__PURE__ */ jsx(
3502
3592
  "button",
3503
3593
  {
3504
3594
  type: "button",
3505
3595
  className: "pollar-btn-secondary pollar-ramp-route-verify",
3506
3596
  disabled,
3507
3597
  onClick: () => onVerify(requirement),
3508
- children: action
3598
+ children: copy.action
3509
3599
  }
3510
3600
  ) })
3511
3601
  ]
3512
3602
  }
3513
3603
  );
3514
3604
  }
3515
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3516
- function isFieldValid(spec, raw) {
3517
- const value = (raw ?? "").trim();
3518
- if (!value) return spec.optional === true;
3519
- if (spec.type === "email") return EMAIL_RE.test(value);
3520
- return true;
3605
+ function formatAvailable(available) {
3606
+ if (available === null) return "\u2014";
3607
+ const n = parseFloat(available);
3608
+ return isNaN(n) ? available : n.toLocaleString(void 0, { maximumFractionDigits: 7 });
3609
+ }
3610
+ function AmountField({
3611
+ id,
3612
+ label,
3613
+ value,
3614
+ onChange,
3615
+ unit,
3616
+ available,
3617
+ onMax,
3618
+ placeholder = "0.00",
3619
+ disabled = false
3620
+ }) {
3621
+ const showBalance = available !== void 0;
3622
+ const canMax = showBalance && available !== null && parseFloat(available) > 0 && !disabled;
3623
+ return /* @__PURE__ */ jsxs("div", { className: "pollar-send-field", children: [
3624
+ /* @__PURE__ */ jsxs("div", { className: "pollar-send-label-row", children: [
3625
+ /* @__PURE__ */ jsx("label", { className: "pollar-send-label", htmlFor: id, children: label }),
3626
+ showBalance && /* @__PURE__ */ jsxs("span", { className: "pollar-send-hint", children: [
3627
+ "Available: ",
3628
+ formatAvailable(available),
3629
+ " ",
3630
+ unit ?? ""
3631
+ ] })
3632
+ ] }),
3633
+ /* @__PURE__ */ jsxs("div", { className: "pollar-send-input-shell", children: [
3634
+ /* @__PURE__ */ jsx(
3635
+ "input",
3636
+ {
3637
+ id,
3638
+ className: "pollar-input pollar-send-amount-input",
3639
+ type: "text",
3640
+ inputMode: "decimal",
3641
+ placeholder,
3642
+ value,
3643
+ disabled,
3644
+ onChange: (e) => onChange(e.target.value),
3645
+ "aria-label": id ? void 0 : label
3646
+ }
3647
+ ),
3648
+ /* @__PURE__ */ jsxs("div", { className: "pollar-send-input-suffix", children: [
3649
+ showBalance && /* @__PURE__ */ jsx(
3650
+ "button",
3651
+ {
3652
+ type: "button",
3653
+ className: "pollar-send-inline-action",
3654
+ disabled: !canMax,
3655
+ onClick: () => onMax ? onMax() : available && onChange(available),
3656
+ children: "Max"
3657
+ }
3658
+ ),
3659
+ unit && /* @__PURE__ */ jsx("span", { children: unit })
3660
+ ] })
3661
+ ] })
3662
+ ] });
3521
3663
  }
3522
3664
  function scannableImageSrc(image) {
3523
3665
  return image.encoding === "base64" ? `data:${image.mediaType};base64,${image.data}` : `data:${image.mediaType};charset=utf-8,${encodeURIComponent(image.data)}`;
@@ -3536,102 +3678,143 @@ function placeholderFor(field, fields, values) {
3536
3678
  }
3537
3679
  return field.placeholder;
3538
3680
  }
3539
- var CHAIN_NAME = { STELLAR: "Stellar", POLYGON: "Polygon", SOLANA: "Solana" };
3540
- var EXPLORER_NAME = { STELLAR: "Stellar Expert", POLYGON: "Polygonscan", SOLANA: "Solana Explorer" };
3541
- var LOADING_STEPS = ["Detecting your country\u2026", "Consulting providers\u2026", "Route found!"];
3542
3681
  function flagEmoji(code) {
3543
3682
  const cc = code.trim().toUpperCase();
3544
3683
  if (!/^[A-Z]{2}$/.test(cc)) return "";
3545
3684
  const base = 127462;
3546
3685
  return String.fromCodePoint(base + (cc.charCodeAt(0) - 65), base + (cc.charCodeAt(1) - 65));
3547
3686
  }
3548
- var STATUS_LABEL = {
3549
- pending: "Pending",
3550
- processing: "Processing",
3551
- completed: "Completed",
3552
- failed: "Failed"
3553
- };
3554
- function displayValue(field) {
3555
- if (field.type !== "datetime") return field.value;
3556
- const at = new Date(field.value);
3557
- return Number.isNaN(at.getTime()) ? field.value : at.toLocaleString();
3687
+ var CHAIN_NAME = { STELLAR: "Stellar", POLYGON: "Polygon", SOLANA: "Solana" };
3688
+ function explorerTxUrl2(chain, hash, network) {
3689
+ const testnet = network === "testnet";
3690
+ if (chain === "SOLANA") return `https://explorer.solana.com/tx/${hash}${testnet ? "?cluster=devnet" : ""}`;
3691
+ if (chain === "POLYGON") return `https://${testnet ? "amoy." : ""}polygonscan.com/tx/${hash}`;
3692
+ return `https://stellar.expert/explorer/${testnet ? "testnet" : "public"}/tx/${hash}`;
3693
+ }
3694
+ function legChain(leg) {
3695
+ if (leg.from.kind === "asset") return leg.from.chain;
3696
+ return leg.to.kind === "asset" ? leg.to.chain : "STELLAR";
3697
+ }
3698
+ function stepChain(kind, leg) {
3699
+ if (kind === "mint" && leg.to.kind === "asset") return leg.to.chain;
3700
+ return legChain(leg);
3701
+ }
3702
+ var timeLeftText = (seconds) => seconds <= 0 ? "Taking a little longer than usual" : seconds < 60 ? `About ${seconds}s left` : `About ${Math.ceil(seconds / 60)} min left`;
3703
+ function useNowEverySecond(running) {
3704
+ const [now, setNow] = useState(() => Date.now());
3705
+ useEffect(() => {
3706
+ if (!running) return;
3707
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
3708
+ return () => clearInterval(timer);
3709
+ }, [running]);
3710
+ return now;
3711
+ }
3712
+ var fiatKey = (fiat) => `${fiat.currency}:${fiat.rail}`;
3713
+ var assetKey3 = (asset) => `${asset.code}:${asset.chain}:${asset.identifier ?? ""}`;
3714
+ function trustKey(rejection) {
3715
+ const asset = rampTrustlineAsset(rejection);
3716
+ return asset ? `${asset.code}:${asset.issuer}` : null;
3717
+ }
3718
+ var LOADING_STEPS = ["Checking routes\u2026", "Quoting each step\u2026", "Route found!"];
3719
+ function useRenderAt(at) {
3720
+ const [, setTick] = useState(0);
3721
+ useEffect(() => {
3722
+ if (at === null) return;
3723
+ const timer = setTimeout(() => setTick((n) => n + 1), Math.max(0, at - Date.now()) + 50);
3724
+ return () => clearTimeout(timer);
3725
+ }, [at]);
3558
3726
  }
3559
3727
  function RampWidgetTemplate({
3560
3728
  theme,
3561
3729
  accentColor,
3562
3730
  styleOverrides,
3731
+ network,
3563
3732
  step,
3564
3733
  flowSteps,
3565
3734
  flowStepIndex,
3566
- startingQuoteId,
3567
3735
  direction,
3568
- amount,
3569
- currency,
3570
- country,
3571
- requiredFields,
3572
- fieldValues,
3736
+ routesLoading,
3573
3737
  countries,
3574
- countriesLoading,
3575
- refreshing,
3738
+ country,
3739
+ fiats,
3740
+ fiat,
3741
+ assets,
3742
+ asset,
3743
+ amount,
3744
+ sellAvailable,
3576
3745
  quotes,
3577
- kycRequired,
3746
+ rejected,
3747
+ startingQuoteId,
3748
+ fields,
3749
+ fieldValues,
3750
+ order,
3751
+ pendingOrders,
3578
3752
  isLoading,
3579
- provider,
3580
- txStatus,
3581
- kycUrl,
3582
- tosUrl,
3583
- kycBlocking,
3584
- onboardingStatus,
3585
- kycJustApproved,
3586
- stellarTxHash,
3587
- txHash,
3588
- txChain,
3589
- explorerUrl,
3590
- fundsInTransit = false,
3591
- retrying = false,
3592
- onRetryTransit,
3593
- depositInstructions,
3594
- canComplete,
3595
- completing,
3753
+ refreshing,
3754
+ signing,
3596
3755
  errorMsg,
3597
- noticeMsg,
3756
+ notice = null,
3598
3757
  onDirectionChange,
3599
- onAmountChange,
3600
- onFieldChange,
3601
3758
  onCountryChange,
3759
+ onFiatChange,
3760
+ onAssetChange,
3761
+ onAmountChange,
3602
3762
  onFindRoute,
3603
3763
  onSelectQuote,
3604
3764
  onVerifyRoute,
3765
+ trustingAsset = null,
3766
+ onTrustAsset,
3767
+ walletBusy = null,
3768
+ custodyChoice = false,
3769
+ onWalletStep,
3770
+ noChains = false,
3771
+ retrying = false,
3772
+ onRetryOrder,
3773
+ onSetUpRecovery,
3774
+ recoveryMissing = null,
3775
+ checkingPayment = false,
3776
+ onCheckPayment,
3777
+ pollTrouble = false,
3778
+ onFieldChange,
3605
3779
  onContactContinue,
3606
- onOpenKyc,
3607
- onOpenTos,
3608
- onCompleteWithdraw,
3780
+ onSign,
3781
+ onApprove,
3782
+ onResumeOrder,
3783
+ onNewOrder,
3609
3784
  onBack,
3785
+ onStepSelect,
3610
3786
  onRetry,
3611
3787
  onRefresh,
3612
3788
  onClose
3613
3789
  }) {
3614
- const settledHash = txHash ?? stellarTxHash;
3615
- const chainName = CHAIN_NAME[txChain ?? "STELLAR"];
3616
3790
  const cssVars = buildModalCssVars(theme, accentColor, styleOverrides, "hero");
3791
+ const onramp = direction === "ONRAMP";
3792
+ const sendUnit = onramp ? fiat?.currency : asset?.code;
3793
+ const view = order ? describeRampAction(order) : null;
3794
+ const settled = order ? ["completed", "failed", "refunded"].includes(order.state) : false;
3795
+ const running = order?.legs.find((leg) => rampLegStatus(leg) === "active" && order.pendingAction?.legIndex !== leg.index);
3796
+ const now = useNowEverySecond(step === "status" && !settled && !!running && running.etaSeconds !== null);
3797
+ const nextApproval = order && !settled ? rampNextApproval(order) : null;
3798
+ const stopped = order ? rampOrderStopped(order) : false;
3799
+ const receive = order ? rampOrderReceive(order) : null;
3800
+ const waitsOnRecovery = order?.failure?.resolutionAction === "set_up_recovery";
3801
+ const payBy = view?.action?.kind === "FIAT_PAYMENT" && view.expiresAt ? Date.parse(view.expiresAt) : null;
3802
+ useRenderAt(view?.canSign && view.expiresAt ? Date.parse(view.expiresAt) : null);
3803
+ const stepsOpen = !!onStepSelect && step !== "status" && !isLoading && startingQuoteId === null && trustingAsset === null && walletBusy === null;
3617
3804
  const stepTitle = {
3618
- input: direction === "onramp" ? "Buy crypto" : "Sell crypto",
3619
- loading_quote: "Finding best route",
3620
- select_route: "Select provider",
3805
+ input: onramp ? "Buy crypto" : "Sell crypto",
3806
+ loading_quote: "Finding the best route",
3807
+ select_route: "Choose a route",
3621
3808
  contact: "Your details",
3622
- status: direction === "onramp" ? "Complete your deposit" : "Complete your withdrawal",
3809
+ status: order?.state === "completed" ? "Transfer completed" : onramp ? "Your purchase" : "Your sale",
3623
3810
  error: "Something went wrong"
3624
3811
  };
3625
3812
  const stepSubtitle = {
3626
- input: direction === "onramp" ? "Enter the amount you want to deposit" : "Enter the amount you want to withdraw",
3627
- loading_quote: "Comparing providers in real time\u2026",
3628
- select_route: "All prices include fees",
3629
- // What this step actually asks for is whatever the route declared, and that
3630
- // is rarely a name and an email: an Abroad off-ramp asks where to send the
3631
- // money, not who you are. Say which, rather than asserting the Bridge case
3632
- // over every provider.
3633
- contact: `${provider || "This provider"} needs ${listOf(requiredFields.filter((f) => !f.optional).map((f) => f.label.toLowerCase()))} to continue`,
3634
- status: `Finish the flow at ${provider || "the provider"} to continue`,
3813
+ input: onramp ? "Pick what you receive and how much you pay" : "Pick what you sell and how much",
3814
+ loading_quote: "Comparing routes in real time\u2026",
3815
+ select_route: "All amounts include fees",
3816
+ contact: `This route needs ${listOf(fields.filter((f) => !f.optional).map((f) => f.label.toLowerCase()))} to continue`,
3817
+ status: view?.title ?? "Processing",
3635
3818
  error: "Please try again"
3636
3819
  };
3637
3820
  return /* @__PURE__ */ jsxs("div", { className: "pollar-modal-card pollar-ramp-modal", style: cssVars, onClick: (e) => e.stopPropagation(), children: [
@@ -3672,85 +3855,124 @@ function RampWidgetTemplate({
3672
3855
  " of ",
3673
3856
  flowSteps.length
3674
3857
  ] }),
3675
- /* @__PURE__ */ jsx(
3676
- "div",
3858
+ /* @__PURE__ */ jsx("ol", { className: "pollar-ramp-steps-track", children: flowSteps.map((label, i) => /* @__PURE__ */ jsx("li", { className: "pollar-ramp-steps-item", "aria-current": i === flowStepIndex ? "step" : void 0, children: /* @__PURE__ */ jsxs(
3859
+ "button",
3677
3860
  {
3678
- className: "pollar-ramp-steps-track",
3679
- role: "progressbar",
3680
- "aria-valuenow": flowStepIndex + 1,
3681
- "aria-valuemin": 1,
3682
- "aria-valuemax": flowSteps.length,
3683
- "aria-label": flowSteps[flowStepIndex],
3684
- children: flowSteps.map((label, i) => /* @__PURE__ */ jsx("span", { className: "pollar-ramp-steps-segment", "data-done": i <= flowStepIndex || void 0 }, label))
3861
+ type: "button",
3862
+ className: "pollar-ramp-steps-step",
3863
+ "data-done": i <= flowStepIndex || void 0,
3864
+ disabled: !stepsOpen || i >= flowStepIndex,
3865
+ onClick: () => onStepSelect?.(label),
3866
+ children: [
3867
+ /* @__PURE__ */ jsx("span", { className: "pollar-ramp-steps-segment" }),
3868
+ /* @__PURE__ */ jsx("span", { className: "pollar-ramp-steps-name", children: label })
3869
+ ]
3685
3870
  }
3686
- )
3871
+ ) }, label)) })
3872
+ ] }),
3873
+ step === "input" && pendingOrders.length > 0 && /* @__PURE__ */ jsxs("section", { className: "pollar-ramp-payment pollar-ramp-saved", "aria-label": "Pending operations", children: [
3874
+ /* @__PURE__ */ jsx("p", { className: "pollar-ramp-payment-note", children: "These operations are waiting for you. Starting another does not cancel them." }),
3875
+ pendingOrders.map((pending) => /* @__PURE__ */ jsx("div", { className: "pollar-ramp-saved-row", children: /* @__PURE__ */ jsxs(
3876
+ "button",
3877
+ {
3878
+ type: "button",
3879
+ className: "pollar-btn-secondary",
3880
+ disabled: isLoading,
3881
+ onClick: () => onResumeOrder(pending.id),
3882
+ children: [
3883
+ "Resume ",
3884
+ pending.direction === "ONRAMP" ? "purchase" : "sale",
3885
+ " \xB7 ",
3886
+ pending.id.slice(-8)
3887
+ ]
3888
+ }
3889
+ ) }, pending.id))
3687
3890
  ] }),
3688
3891
  step === "input" && /* @__PURE__ */ jsxs(Fragment, { children: [
3689
3892
  /* @__PURE__ */ jsxs("div", { className: "pollar-tabs", children: [
3690
- /* @__PURE__ */ jsx(
3691
- "button",
3692
- {
3693
- type: "button",
3694
- className: "pollar-tab",
3695
- "data-active": direction === "onramp",
3696
- onClick: () => onDirectionChange("onramp"),
3697
- children: "Buy"
3698
- }
3699
- ),
3700
- /* @__PURE__ */ jsx(
3701
- "button",
3702
- {
3703
- type: "button",
3704
- className: "pollar-tab",
3705
- "data-active": direction === "offramp",
3706
- onClick: () => onDirectionChange("offramp"),
3707
- children: "Sell"
3708
- }
3709
- )
3893
+ /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-tab", "data-active": onramp, onClick: () => onDirectionChange("ONRAMP"), children: "Buy" }),
3894
+ /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-tab", "data-active": !onramp, onClick: () => onDirectionChange("OFFRAMP"), children: "Sell" })
3710
3895
  ] }),
3711
- /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-field", children: [
3712
- /* @__PURE__ */ jsx("label", { className: "pollar-ramp-label", children: "Country" }),
3713
- countriesLoading ? /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-input pollar-select-loading", children: [
3714
- /* @__PURE__ */ jsx("span", { className: "pollar-spinner pollar-spinner-sm" }),
3715
- /* @__PURE__ */ jsx("span", { children: "Loading countries\u2026" })
3716
- ] }) : countries.length === 0 ? /* @__PURE__ */ jsx("div", { className: "pollar-modal-error", children: "No ramp providers available on this network yet." }) : /* @__PURE__ */ jsx("select", { className: "pollar-ramp-input", value: country, onChange: (e) => onCountryChange(e.target.value), children: countries.map((c) => /* @__PURE__ */ jsxs("option", { value: c.code, children: [
3717
- flagEmoji(c.code),
3718
- " ",
3719
- c.code,
3720
- c.currency ? ` \u2014 ${c.currency}` : ""
3721
- ] }, c.code)) })
3722
- ] }),
3723
- /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-field", children: [
3724
- /* @__PURE__ */ jsxs("label", { className: "pollar-ramp-label", children: [
3725
- "Amount",
3726
- currency ? ` (${currency})` : ""
3896
+ routesLoading ? /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-input pollar-select-loading", children: [
3897
+ /* @__PURE__ */ jsx("span", { className: "pollar-spinner pollar-spinner-sm" }),
3898
+ /* @__PURE__ */ jsx("span", { children: "Loading routes\u2026" })
3899
+ ] }) : countries.length === 0 ? /* @__PURE__ */ jsx("div", { className: "pollar-modal-error", children: noChains ? "This app has no chains enabled, so there is nothing to buy or sell yet." : "No ramp routes are available for this app yet." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
3900
+ /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-field", children: [
3901
+ /* @__PURE__ */ jsx("label", { className: "pollar-ramp-label", htmlFor: "pollar-ramp-country", children: "Country" }),
3902
+ /* @__PURE__ */ jsx(
3903
+ "select",
3904
+ {
3905
+ id: "pollar-ramp-country",
3906
+ className: "pollar-ramp-input",
3907
+ value: country,
3908
+ onChange: (e) => onCountryChange(e.target.value),
3909
+ children: countries.map((c) => /* @__PURE__ */ jsxs("option", { value: c.country, children: [
3910
+ flagEmoji(c.country),
3911
+ " ",
3912
+ c.country
3913
+ ] }, c.country))
3914
+ }
3915
+ )
3727
3916
  ] }),
3728
- /* @__PURE__ */ jsx(
3729
- "input",
3730
- {
3731
- type: "number",
3732
- className: "pollar-ramp-input",
3733
- placeholder: "0.00",
3734
- value: amount,
3735
- min: "0",
3736
- onChange: (e) => onAmountChange(e.target.value)
3737
- }
3738
- ),
3739
- errorMsg && /* @__PURE__ */ jsx("span", { className: "pollar-ramp-field-error", children: errorMsg })
3917
+ fiats.length > 1 && /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-field", children: [
3918
+ /* @__PURE__ */ jsx("label", { className: "pollar-ramp-label", htmlFor: "pollar-ramp-fiat", children: onramp ? "Pay with" : "Receive in" }),
3919
+ /* @__PURE__ */ jsx(
3920
+ "select",
3921
+ {
3922
+ id: "pollar-ramp-fiat",
3923
+ className: "pollar-ramp-input",
3924
+ value: fiat ? fiatKey(fiat) : "",
3925
+ onChange: (e) => onFiatChange(e.target.value),
3926
+ children: fiats.map((f) => /* @__PURE__ */ jsxs("option", { value: fiatKey(f), children: [
3927
+ f.currency,
3928
+ " \xB7 ",
3929
+ rampRailLabel(f.rail, { language: formLanguage() })
3930
+ ] }, fiatKey(f)))
3931
+ }
3932
+ )
3933
+ ] }),
3934
+ /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-field", children: [
3935
+ /* @__PURE__ */ jsx("label", { className: "pollar-ramp-label", htmlFor: "pollar-ramp-asset", children: onramp ? "Receive" : "Sell" }),
3936
+ /* @__PURE__ */ jsx(
3937
+ "select",
3938
+ {
3939
+ id: "pollar-ramp-asset",
3940
+ className: "pollar-ramp-input",
3941
+ value: asset ? assetKey3(asset) : "",
3942
+ onChange: (e) => onAssetChange(e.target.value),
3943
+ children: assets.map((a) => /* @__PURE__ */ jsxs("option", { value: assetKey3(a), children: [
3944
+ a.code,
3945
+ " on ",
3946
+ CHAIN_NAME[a.chain] ?? a.chain
3947
+ ] }, assetKey3(a)))
3948
+ }
3949
+ )
3950
+ ] }),
3951
+ /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-field", children: [
3952
+ /* @__PURE__ */ jsx(
3953
+ AmountField,
3954
+ {
3955
+ id: "pollar-ramp-amount",
3956
+ label: onramp ? "You pay" : "You sell",
3957
+ value: amount,
3958
+ onChange: onAmountChange,
3959
+ unit: sendUnit,
3960
+ available: onramp ? void 0 : sellAvailable
3961
+ }
3962
+ ),
3963
+ errorMsg && /* @__PURE__ */ jsx("span", { className: "pollar-ramp-field-error", children: errorMsg })
3964
+ ] })
3740
3965
  ] }),
3741
- /* @__PURE__ */ jsxs("div", { className: "pollar-modal-actions", children: [
3742
- /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-btn-secondary", onClick: onClose, children: "Cancel" }),
3743
- /* @__PURE__ */ jsx(
3744
- "button",
3745
- {
3746
- type: "button",
3747
- className: "pollar-btn-primary",
3748
- disabled: !amount || isLoading || countriesLoading || countries.length === 0,
3749
- onClick: onFindRoute,
3750
- children: "Find best route"
3751
- }
3752
- )
3753
- ] })
3966
+ /* @__PURE__ */ jsx("div", { className: "pollar-modal-actions", children: /* @__PURE__ */ jsx(
3967
+ "button",
3968
+ {
3969
+ type: "button",
3970
+ className: "pollar-btn-primary",
3971
+ disabled: !amount || !fiat || !asset || isLoading || routesLoading,
3972
+ onClick: onFindRoute,
3973
+ children: "Find best route"
3974
+ }
3975
+ ) })
3754
3976
  ] }),
3755
3977
  step === "loading_quote" && /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-loading", children: [
3756
3978
  /* @__PURE__ */ jsx("div", { className: "pollar-spinner pollar-ramp-loading-spinner" }),
@@ -3760,33 +3982,44 @@ function RampWidgetTemplate({
3760
3982
  ] }, i))
3761
3983
  ] }),
3762
3984
  step === "select_route" && /* @__PURE__ */ jsxs(Fragment, { children: [
3763
- noticeMsg && /* @__PURE__ */ jsx("p", { className: "pollar-ramp-payment-note", children: noticeMsg }),
3764
3985
  /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-route-list", children: [
3765
3986
  quotes.map((q, i) => /* @__PURE__ */ jsx(
3766
3987
  RouteDisplay,
3767
3988
  {
3768
3989
  quote: q,
3769
- busy: startingQuoteId != null && q.quoteId === startingQuoteId,
3990
+ best: i === 0,
3991
+ busy: startingQuoteId === q.quoteId,
3770
3992
  disabled: startingQuoteId != null && q.quoteId !== startingQuoteId,
3771
3993
  onSelect: onSelectQuote
3772
3994
  },
3773
- i
3995
+ q.quoteId
3774
3996
  )),
3775
- kycRequired.map((r) => /* @__PURE__ */ jsx(
3776
- LockedRouteDisplay,
3997
+ rejected.map((r, i) => /* @__PURE__ */ jsx(
3998
+ RejectedRouteDisplay,
3777
3999
  {
3778
- requirement: r,
3779
- disabled: startingQuoteId != null,
3780
- onVerify: onVerifyRoute
4000
+ rejection: r,
4001
+ disabled: startingQuoteId != null || trustingAsset !== null || walletBusy !== null,
4002
+ trusting: trustKey(r) === trustingAsset,
4003
+ walletBusy: walletBusy === i,
4004
+ custodyChoice,
4005
+ onVerify: onVerifyRoute,
4006
+ onTrust: onTrustAsset,
4007
+ onWalletStep: onWalletStep && ((step2, custody) => onWalletStep(step2, i, custody))
3781
4008
  },
3782
- `${r.rampProviderId}:${r.corridorId}`
4009
+ i
3783
4010
  ))
3784
4011
  ] }),
4012
+ quotes.length === 0 && /* @__PURE__ */ jsx("p", { className: "pollar-ramp-payment-note", children: rampNoRouteText(rejected) }),
4013
+ notice && /* @__PURE__ */ jsxs("p", { className: "pollar-ramp-payment-note", children: [
4014
+ /* @__PURE__ */ jsx("span", { className: "pollar-spinner pollar-spinner-sm", "aria-hidden": true }),
4015
+ " ",
4016
+ notice
4017
+ ] }),
3785
4018
  errorMsg && /* @__PURE__ */ jsx("p", { className: "pollar-ramp-payment-note", style: { color: "var(--pollar-error-text)" }, children: errorMsg }),
3786
- /* @__PURE__ */ jsx("button", { type: "button", className: errorMsg ? "pollar-btn-primary" : "pollar-btn-secondary", onClick: onBack, children: "Back" })
4019
+ (errorMsg || !quotes.length) && /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-btn-primary", onClick: onBack, children: "Back" })
3787
4020
  ] }),
3788
4021
  step === "contact" && /* @__PURE__ */ jsxs(Fragment, { children: [
3789
- requiredFields.map((f) => /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-field", children: [
4022
+ fields.map((f) => /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-field", children: [
3790
4023
  /* @__PURE__ */ jsxs("label", { className: "pollar-ramp-label", children: [
3791
4024
  f.label,
3792
4025
  f.optional && /* @__PURE__ */ jsx("span", { className: "pollar-ramp-field-optional", children: " (optional)" })
@@ -3808,175 +4041,178 @@ function RampWidgetTemplate({
3808
4041
  type: f.type,
3809
4042
  className: "pollar-ramp-input",
3810
4043
  value: fieldValues[f.key] ?? "",
3811
- placeholder: placeholderFor(f, requiredFields, fieldValues),
4044
+ placeholder: placeholderFor(f, fields, fieldValues),
3812
4045
  autoComplete: f.type === "email" ? "email" : "off",
3813
4046
  onChange: (e) => onFieldChange(f.key, e.target.value)
3814
4047
  }
3815
4048
  ),
3816
4049
  f.hint && /* @__PURE__ */ jsx("span", { className: "pollar-ramp-field-hint", children: f.hint }),
3817
- f.type === "email" && (fieldValues[f.key] ?? "").trim() !== "" && !isFieldValid(f, fieldValues[f.key]) && /* @__PURE__ */ jsx("span", { className: "pollar-ramp-field-error", children: "Enter a valid email address." })
4050
+ rampFieldError(f, fieldValues[f.key]) && /* @__PURE__ */ jsx("span", { className: "pollar-ramp-field-error", children: rampFieldError(f, fieldValues[f.key]) })
3818
4051
  ] }, f.key)),
3819
- /* @__PURE__ */ jsxs("div", { className: "pollar-modal-actions", children: [
3820
- /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-btn-secondary", onClick: onBack, children: "Back" }),
3821
- /* @__PURE__ */ jsx(
4052
+ errorMsg && /* @__PURE__ */ jsx("p", { className: "pollar-ramp-field-error", children: errorMsg }),
4053
+ /* @__PURE__ */ jsx("div", { className: "pollar-modal-actions", children: /* @__PURE__ */ jsx(
4054
+ "button",
4055
+ {
4056
+ type: "button",
4057
+ className: "pollar-btn-primary",
4058
+ disabled: fields.some((f) => !rampFieldValid(f, fieldValues[f.key])) || isLoading,
4059
+ onClick: onContactContinue,
4060
+ children: isLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [
4061
+ /* @__PURE__ */ jsx("span", { className: "pollar-spinner pollar-spinner-sm" }),
4062
+ "Starting\u2026"
4063
+ ] }) : "Continue"
4064
+ }
4065
+ ) })
4066
+ ] }),
4067
+ step === "status" && order && view && /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-payment", children: [
4068
+ /* @__PURE__ */ jsx("ol", { className: "pollar-ramp-legs", "aria-label": "Progress", children: order.legs.map((leg) => {
4069
+ const status = rampLegStatus(leg);
4070
+ const hash = leg.onchainTxHash;
4071
+ const note = rampLegNote(leg);
4072
+ return /* @__PURE__ */ jsxs("li", { className: "pollar-ramp-leg", "data-status": status, children: [
4073
+ /* @__PURE__ */ jsx("span", { className: "pollar-ramp-leg-dot", "aria-hidden": true }),
4074
+ /* @__PURE__ */ jsx("span", { className: "pollar-ramp-leg-title", children: rampLegTitle(leg) }),
4075
+ status === "active" && !settled && /* @__PURE__ */ jsx("span", { className: "pollar-spinner pollar-spinner-sm", "aria-label": "In progress" }),
4076
+ hash && /* @__PURE__ */ jsxs(
4077
+ "a",
4078
+ {
4079
+ className: "pollar-ramp-leg-link",
4080
+ href: explorerTxUrl2(legChain(leg), hash, network),
4081
+ target: "_blank",
4082
+ rel: "noopener noreferrer",
4083
+ children: [
4084
+ hash.slice(0, 6),
4085
+ "\u2026",
4086
+ hash.slice(-4)
4087
+ ]
4088
+ }
4089
+ ),
4090
+ note && /* @__PURE__ */ jsx("span", { className: "pollar-ramp-leg-note", children: note }),
4091
+ status === "active" && !settled && !!leg.steps?.length && /* @__PURE__ */ jsx("ol", { className: "pollar-ramp-steps", children: leg.steps.map((s) => /* @__PURE__ */ jsxs("li", { className: "pollar-ramp-step", "data-status": s.status, children: [
4092
+ /* @__PURE__ */ jsx("span", { className: "pollar-ramp-step-title", children: rampStepTitle(s, leg) }),
4093
+ /* @__PURE__ */ jsxs("span", { className: "pollar-ramp-step-state", children: [
4094
+ s.status === "confirming" && /* @__PURE__ */ jsx("span", { className: "pollar-spinner pollar-spinner-sm", "aria-hidden": true }),
4095
+ s.hash ? /* @__PURE__ */ jsx(
4096
+ "a",
4097
+ {
4098
+ className: "pollar-ramp-leg-link",
4099
+ href: explorerTxUrl2(stepChain(s.kind, leg), s.hash, network),
4100
+ target: "_blank",
4101
+ rel: "noopener noreferrer",
4102
+ children: rampStepStatusText(s)
4103
+ }
4104
+ ) : rampStepStatusText(s)
4105
+ ] })
4106
+ ] }, s.kind)) }),
4107
+ running === leg && !settled && rampLegTimeLeft(leg, now) !== null && /* @__PURE__ */ jsx("span", { className: "pollar-ramp-leg-eta", children: timeLeftText(rampLegTimeLeft(leg, now)) })
4108
+ ] }, leg.index);
4109
+ }) }),
4110
+ nextApproval && !order.pendingAction && /* @__PURE__ */ jsxs("p", { className: "pollar-ramp-next", children: [
4111
+ "Next: ",
4112
+ nextApproval,
4113
+ ". You approve it on this device when it comes up."
4114
+ ] }),
4115
+ receive && /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-payment-field", children: [
4116
+ /* @__PURE__ */ jsx("span", { className: "pollar-ramp-payment-label", children: order.state === "completed" ? "You received" : "You receive" }),
4117
+ /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-payment-value", children: [
4118
+ receive.estimated && "~",
4119
+ receive.amount,
4120
+ " ",
4121
+ receive.unit
4122
+ ] })
4123
+ ] }),
4124
+ order.failure && /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-payment-field", children: [
4125
+ /* @__PURE__ */ jsx("span", { className: "pollar-ramp-payment-label", children: "What to do" }),
4126
+ /* @__PURE__ */ jsx("p", { className: "pollar-ramp-payment-note", children: rampOrderResolutionText(order) }),
4127
+ stopped && !settled && waitsOnRecovery && recoveryMissing !== false && onSetUpRecovery && /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-btn-secondary", onClick: onSetUpRecovery, children: "Set up recovery" }),
4128
+ stopped && !settled && waitsOnRecovery && recoveryMissing === true && /* @__PURE__ */ jsx("p", { className: "pollar-ramp-payment-note", children: "Once it is set up, the order goes on by itself." }),
4129
+ stopped && !settled && order.failure.resolutionOwner === "END_USER" && onRetryOrder && /* @__PURE__ */ jsx(
3822
4130
  "button",
3823
4131
  {
3824
4132
  type: "button",
3825
4133
  className: "pollar-btn-primary",
3826
- disabled: requiredFields.some((f) => !isFieldValid(f, fieldValues[f.key])) || isLoading,
3827
- onClick: onContactContinue,
3828
- children: isLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [
4134
+ disabled: retrying || waitsOnRecovery && recoveryMissing === true,
4135
+ onClick: onRetryOrder,
4136
+ children: retrying ? /* @__PURE__ */ jsxs(Fragment, { children: [
3829
4137
  /* @__PURE__ */ jsx("span", { className: "pollar-spinner pollar-spinner-sm" }),
3830
- "Starting\u2026"
3831
- ] }) : "Continue"
4138
+ " Trying again\u2026"
4139
+ ] }) : "Try again"
3832
4140
  }
3833
- )
3834
- ] })
3835
- ] }),
3836
- step === "status" && /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-payment", children: [
3837
- /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-payment-field", children: [
3838
- /* @__PURE__ */ jsx("span", { className: "pollar-ramp-payment-label", children: "Provider" }),
3839
- /* @__PURE__ */ jsx("div", { className: "pollar-ramp-payment-value", children: /* @__PURE__ */ jsx("code", { children: provider }) })
3840
- ] }),
3841
- /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-payment-field", children: [
3842
- /* @__PURE__ */ jsx("span", { className: "pollar-ramp-payment-label", children: "Status" }),
3843
- /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-payment-value", children: [
3844
- /* @__PURE__ */ jsx("code", { style: { color: txStatus === "completed" ? "var(--pollar-success-text)" : void 0 }, children: txStatus ? STATUS_LABEL[txStatus] : "Processing" }),
3845
- txStatus !== "completed" && txStatus !== "failed" && /* @__PURE__ */ jsx("span", { className: "pollar-spinner pollar-spinner-sm", "aria-label": "Checking status" })
3846
- ] })
3847
- ] }),
3848
- fundsInTransit && txStatus !== "completed" && txStatus !== "failed" && /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-payment-field", children: [
3849
- /* @__PURE__ */ jsx("span", { className: "pollar-ramp-payment-label", children: "Funds in transit" }),
3850
- /* @__PURE__ */ jsx("p", { className: "pollar-ramp-payment-note", children: "Your money is safe in your own wallet, waiting for the next step of the transfer." }),
3851
- onRetryTransit && /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-btn-secondary", onClick: onRetryTransit, disabled: retrying, children: retrying ? "Retrying..." : "Retry now" })
3852
- ] }),
3853
- settledHash && /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-payment-field", children: [
3854
- /* @__PURE__ */ jsxs("span", { className: "pollar-ramp-payment-label", children: [
3855
- chainName,
3856
- " tx"
3857
- ] }),
4141
+ ),
3858
4142
  /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-payment-value", style: { display: "flex", alignItems: "center", gap: 6 }, children: [
3859
4143
  /* @__PURE__ */ jsxs("code", { style: { flex: 1, wordBreak: "break-all" }, children: [
3860
- settledHash.slice(0, 8),
3861
- "\u2026",
3862
- settledHash.slice(-8)
4144
+ "Order ",
4145
+ order.id
3863
4146
  ] }),
3864
- /* @__PURE__ */ jsx(CopyButton, { value: settledHash, label: "Copy transaction hash" }),
3865
- explorerUrl && /* @__PURE__ */ jsx(
3866
- "a",
3867
- {
3868
- href: explorerUrl,
3869
- target: "_blank",
3870
- rel: "noopener noreferrer",
3871
- className: "pollar-copy-btn",
3872
- "aria-label": `View on ${EXPLORER_NAME[txChain ?? "STELLAR"]}`,
3873
- title: `View on ${EXPLORER_NAME[txChain ?? "STELLAR"]}`,
3874
- children: /* @__PURE__ */ jsxs("svg", { width: "13", height: "13", viewBox: "0 0 14 14", fill: "none", "aria-hidden": true, children: [
3875
- /* @__PURE__ */ jsx(
3876
- "path",
3877
- {
3878
- d: "M6 3H3v8h8V8",
3879
- stroke: "currentColor",
3880
- strokeWidth: "1.5",
3881
- strokeLinecap: "round",
3882
- strokeLinejoin: "round"
3883
- }
3884
- ),
3885
- /* @__PURE__ */ jsx(
3886
- "path",
3887
- {
3888
- d: "M8.5 2.5h3v3M11 3L6.5 7.5",
3889
- stroke: "currentColor",
3890
- strokeWidth: "1.5",
3891
- strokeLinecap: "round",
3892
- strokeLinejoin: "round"
3893
- }
3894
- )
3895
- ] })
3896
- }
3897
- )
4147
+ /* @__PURE__ */ jsx(CopyButton, { value: order.id, label: "Copy order id" })
3898
4148
  ] })
3899
4149
  ] }),
3900
- depositInstructions?.scannable && txStatus !== "completed" && /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-payment-field", children: [
4150
+ !settled && payBy !== null && /* @__PURE__ */ jsx("p", { className: "pollar-ramp-payment-note", children: payBy > Date.now() ? `Pay before ${new Date(payBy).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.` : "This payment window has closed. If you have not paid, start a new operation." }),
4151
+ !settled && view.scannable && /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-payment-field", children: [
3901
4152
  /* @__PURE__ */ jsx("span", { className: "pollar-ramp-payment-label", children: "Payment QR" }),
3902
- /* @__PURE__ */ jsx("div", { className: "pollar-ramp-payment-value", children: depositInstructions.scannable.image.inlineSafe ? /* @__PURE__ */ jsx(
4153
+ /* @__PURE__ */ jsx("div", { className: "pollar-ramp-payment-value", children: view.scannable.image.inlineSafe ? /* @__PURE__ */ jsx(
3903
4154
  "div",
3904
4155
  {
3905
4156
  className: "pollar-ramp-qr",
3906
4157
  "aria-label": "Payment QR",
3907
- dangerouslySetInnerHTML: { __html: depositInstructions.scannable.image.data }
4158
+ dangerouslySetInnerHTML: { __html: view.scannable.image.data }
3908
4159
  }
3909
4160
  ) : /* @__PURE__ */ jsx(
3910
4161
  "img",
3911
4162
  {
3912
- src: scannableImageSrc(depositInstructions.scannable.image),
4163
+ src: scannableImageSrc(view.scannable.image),
3913
4164
  alt: "Payment QR",
3914
4165
  style: { width: "100%", maxWidth: 220, height: "auto", display: "block", margin: "0 auto" }
3915
4166
  }
3916
4167
  ) })
3917
4168
  ] }),
3918
- depositInstructions?.scannable?.payload && depositInstructions.scannable.payloadLabel && txStatus !== "completed" && /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-payment-field", children: [
3919
- /* @__PURE__ */ jsx("span", { className: "pollar-ramp-payment-label", children: depositInstructions.scannable.payloadLabel }),
4169
+ !settled && view.codeMissing && /* @__PURE__ */ jsx("p", { className: "pollar-ramp-payment-note", style: { color: "var(--pollar-error-text)" }, children: "The payment code could not be shown. Refresh; if it still does not appear, contact this app's support." }),
4170
+ !settled && view.scannable?.payload && view.scannable.payloadLabel && /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-payment-field", children: [
4171
+ /* @__PURE__ */ jsx("span", { className: "pollar-ramp-payment-label", children: view.scannable.payloadLabel }),
3920
4172
  /* @__PURE__ */ jsx("div", { className: "pollar-ramp-payment-value", children: /* @__PURE__ */ jsxs("span", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
3921
- /* @__PURE__ */ jsx("code", { style: { flex: 1, wordBreak: "break-all" }, children: depositInstructions.scannable.payload }),
3922
- /* @__PURE__ */ jsx(
3923
- CopyButton,
3924
- {
3925
- value: depositInstructions.scannable.payload,
3926
- label: `Copy ${depositInstructions.scannable.payloadLabel}`
3927
- }
3928
- )
4173
+ /* @__PURE__ */ jsx("code", { style: { flex: 1, wordBreak: "break-all" }, children: view.scannable.payload }),
4174
+ /* @__PURE__ */ jsx(CopyButton, { value: view.scannable.payload, label: `Copy ${view.scannable.payloadLabel}` })
3929
4175
  ] }) })
3930
4176
  ] }),
3931
- depositInstructions && txStatus !== "completed" && depositInstructions.fields.map((f) => /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-payment-field", children: [
3932
- /* @__PURE__ */ jsx("span", { className: "pollar-ramp-payment-label", children: f.label }),
3933
- /* @__PURE__ */ jsx("div", { className: "pollar-ramp-payment-value", children: f.copyable ? /* @__PURE__ */ jsxs("span", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
3934
- /* @__PURE__ */ jsx("code", { style: { flex: 1, wordBreak: "break-all" }, children: f.value }),
3935
- /* @__PURE__ */ jsx(CopyButton, { value: f.value, label: `Copy ${f.label}` })
3936
- ] }) : f.type === "url" ? /* @__PURE__ */ jsx("a", { href: f.value, target: "_blank", rel: "noopener noreferrer", children: f.value }) : /* @__PURE__ */ jsx("span", { children: displayValue(f) }) })
3937
- ] }, f.key)),
3938
- tosUrl && !depositInstructions && txStatus !== "completed" && /* @__PURE__ */ jsxs("button", { type: "button", className: "pollar-btn-primary", onClick: onOpenTos, children: [
3939
- "Accept terms at ",
3940
- provider
3941
- ] }),
3942
- kycUrl && !depositInstructions && txStatus !== "completed" && /* @__PURE__ */ jsxs("button", { type: "button", className: "pollar-btn-primary", onClick: onOpenKyc, children: [
3943
- "Continue at ",
3944
- provider
3945
- ] }),
3946
- kycBlocking && onboardingStatus === "awaiting_provider" && /* @__PURE__ */ jsxs("p", { className: "pollar-ramp-payment-note", children: [
3947
- provider,
3948
- " is still setting up your account for this payment method. Your verification is complete and there is nothing left for you to do \u2014 nothing has been sent, and this will update on its own once ",
3949
- provider,
3950
- " is ready."
3951
- ] }),
3952
- kycBlocking && onboardingStatus !== "awaiting_provider" && /* @__PURE__ */ jsxs("p", { className: "pollar-ramp-payment-note", children: [
3953
- provider,
3954
- " needs to verify your identity before this payout. Nothing has been sent yet \u2014 complete verification with ",
3955
- provider,
3956
- ", and this will update on its own."
3957
- ] }),
3958
- kycJustApproved && /* @__PURE__ */ jsxs(Fragment, { children: [
3959
- /* @__PURE__ */ jsxs("p", { className: "pollar-ramp-payment-note", children: [
3960
- provider,
3961
- " approved your verification. Request a new quote to continue \u2014 the previous one was consumed."
3962
- ] }),
3963
- /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-btn-primary", onClick: onRetry, children: "Request a new quote" })
3964
- ] }),
3965
- canComplete && /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-btn-primary", disabled: completing, onClick: onCompleteWithdraw, children: completing ? /* @__PURE__ */ jsxs(Fragment, { children: [
4177
+ !settled && view.details.map((d) => /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-payment-field", children: [
4178
+ /* @__PURE__ */ jsx("span", { className: "pollar-ramp-payment-label", children: d.label }),
4179
+ /* @__PURE__ */ jsx("div", { className: "pollar-ramp-payment-value", children: /* @__PURE__ */ jsxs("span", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
4180
+ /* @__PURE__ */ jsx("code", { style: { flex: 1, wordBreak: "break-all" }, children: d.value }),
4181
+ /* @__PURE__ */ jsx(CopyButton, { value: d.value, label: `Copy ${d.label}` })
4182
+ ] }) })
4183
+ ] }, d.label)),
4184
+ !settled && view.action?.kind === "FIAT_PAYMENT" && onCheckPayment && /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-btn-primary", disabled: checkingPayment, onClick: onCheckPayment, children: checkingPayment ? /* @__PURE__ */ jsxs(Fragment, { children: [
3966
4185
  /* @__PURE__ */ jsx("span", { className: "pollar-spinner pollar-spinner-sm" }),
3967
- "Submitting\u2026"
3968
- ] }) : "I've completed KYC \u2014 withdraw" }),
3969
- errorMsg && /* @__PURE__ */ jsx("p", { className: "pollar-ramp-payment-note", style: { color: "var(--pollar-error-text)" }, children: errorMsg }),
3970
- /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-btn-secondary", onClick: onClose, children: txStatus === "completed" ? "Done" : "Close" })
4186
+ " Checking your payment..."
4187
+ ] }) : "I've paid" }),
4188
+ !settled && view.canSign && /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-btn-primary", disabled: signing, onClick: onSign, children: signing ? "Waiting for your wallet\u2026" : "Authorize" }),
4189
+ !settled && view.action?.kind === "SIGNATURE" && !view.canSign && /* @__PURE__ */ jsx("p", { className: "pollar-ramp-payment-note", children: "Preparing a new signature request\u2026" }),
4190
+ !settled && view.canApprove && /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-btn-primary", onClick: onApprove, children: "Approve on this device" }),
4191
+ !settled && view.link && /* @__PURE__ */ jsx(
4192
+ "a",
4193
+ {
4194
+ className: "pollar-btn-primary pollar-ramp-hosted-link",
4195
+ href: view.link,
4196
+ target: "_blank",
4197
+ rel: "noopener noreferrer",
4198
+ children: view.linkLabel
4199
+ }
4200
+ ),
4201
+ !settled && order.pendingAction?.kind === "KYC" && !view.link && /* @__PURE__ */ jsx("p", { className: "pollar-ramp-payment-note", children: view.note ?? "The provider is reviewing your details. This updates on its own." }),
4202
+ pollTrouble && !settled && /* @__PURE__ */ jsx("p", { className: "pollar-ramp-payment-note", children: "Having trouble reaching the server. Retrying\u2026" }),
4203
+ errorMsg && /* @__PURE__ */ jsx("p", { className: "pollar-ramp-field-error", children: errorMsg }),
4204
+ /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-explore", children: [
4205
+ /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-btn-secondary", onClick: onNewOrder, children: "Start another operation" }),
4206
+ !stopped && /* @__PURE__ */ jsx("p", { className: "pollar-ramp-payment-note", children: "This one keeps going; you can resume it later." })
4207
+ ] })
3971
4208
  ] }),
3972
- step === "error" && /* @__PURE__ */ jsxs("div", { className: "pollar-ramp-payment", children: [
3973
- /* @__PURE__ */ jsx("p", { className: "pollar-ramp-payment-note", style: { color: "var(--pollar-error-text)" }, children: errorMsg ?? "Unexpected error." }),
4209
+ step === "error" && /* @__PURE__ */ jsxs(Fragment, { children: [
4210
+ /* @__PURE__ */ jsx("div", { className: "pollar-modal-error", children: errorMsg ?? "Something went wrong." }),
3974
4211
  /* @__PURE__ */ jsxs("div", { className: "pollar-modal-actions", children: [
3975
4212
  /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-btn-secondary", onClick: onClose, children: "Close" }),
3976
4213
  /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-btn-primary", onClick: onRetry, children: "Try again" })
3977
4214
  ] })
3978
- ] }),
3979
- /* @__PURE__ */ jsx(PollarModalFooter, {})
4215
+ ] })
3980
4216
  ] });
3981
4217
  }
3982
4218
 
@@ -4131,19 +4367,26 @@ var SHARED_FIELD_LABELS = {
4131
4367
  country_code: "Pa\xEDs de residencia"
4132
4368
  }
4133
4369
  };
4134
- var errorCode = (error) => error && typeof error === "object" && typeof error.code === "string" ? error.code : null;
4370
+ var errorCode = (error) => typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : null;
4371
+ var errorBody = (error) => {
4372
+ const body = typeof error === "object" && error !== null && "body" in error ? error.body : void 0;
4373
+ return typeof body === "object" && body !== null ? body : void 0;
4374
+ };
4135
4375
  function missingFieldsOf(error) {
4136
- const body = error && typeof error === "object" ? error.body : void 0;
4137
- return Array.isArray(body?.missing) ? body.missing.filter((item) => typeof item === "string") : [];
4376
+ const body = errorBody(error);
4377
+ const missing = body && "missing" in body ? body.missing : void 0;
4378
+ return Array.isArray(missing) ? missing.filter((item) => typeof item === "string") : [];
4138
4379
  }
4139
4380
  function registrationFixOf(error) {
4140
- const body = error && typeof error === "object" ? error.body : void 0;
4381
+ const body = errorBody(error);
4382
+ const invalid = body && "invalid" in body ? body.invalid : void 0;
4383
+ const formList = body && "forms" in body ? body.forms : void 0;
4141
4384
  const strings = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
4142
- const forms = Array.isArray(body?.forms) ? body.forms.flatMap((item) => {
4143
- const form = item && typeof item === "object" ? item : {};
4144
- return typeof form.formId === "string" ? [{ formId: form.formId, keys: strings(form.keys) }] : [];
4385
+ const forms = Array.isArray(formList) ? formList.flatMap((item) => {
4386
+ if (typeof item !== "object" || item === null || !("formId" in item) || typeof item.formId !== "string") return [];
4387
+ return [{ formId: item.formId, keys: strings("keys" in item ? item.keys : void 0) }];
4145
4388
  }) : [];
4146
- return { invalid: strings(body?.invalid), forms };
4389
+ return { invalid: strings(invalid), forms };
4147
4390
  }
4148
4391
  function RegistryCheckModal({ optionId, progress, onClose, onApproved }) {
4149
4392
  const { getClient, styles } = usePollar();
@@ -4463,472 +4706,499 @@ function ProviderRegistrationModal({
4463
4706
  }
4464
4707
  ) });
4465
4708
  }
4466
- var TERMINAL = ["completed", "failed"];
4467
- function requiredFieldsOf(quote) {
4468
- return quote.requiredFields ?? [];
4469
- }
4470
- function flowStepsOf(quotes, selected) {
4471
- const needsDetails = selected != null ? requiredFieldsOf(selected).length > 0 : quotes.some((q) => requiredFieldsOf(q).length > 0);
4472
- return needsDetails ? ["Amount", "Provider", "Details", "Complete"] : ["Amount", "Provider", "Complete"];
4473
- }
4474
- var STEP_LABEL = {
4475
- input: "Amount",
4476
- loading_quote: "Amount",
4477
- select_route: "Provider",
4478
- contact: "Details",
4479
- status: "Complete"
4480
- };
4481
- function brokenLimitOf(amount, quote) {
4482
- const { minAmount, maxAmount } = quote;
4483
- if (!Number.isFinite(amount)) return null;
4484
- if (minAmount != null && amount < minAmount) return { limit: "min", value: minAmount };
4485
- if (maxAmount != null && amount > maxAmount) return { limit: "max", value: maxAmount };
4486
- return null;
4487
- }
4488
- function limitMessage(limit, value, currency) {
4489
- return `The ${limit === "min" ? "minimum" : "maximum"} amount for this route is ${value} ${currency}.`;
4490
- }
4709
+ var PAYMENT_CHECK_EVERY_MS = 1e4;
4710
+ var PAYMENT_CHECK_WINDOW_MS = 2 * 6e4;
4491
4711
  var RAMP_ERROR_MESSAGES = {
4492
- SDK_RAMPS_QUOTE_EXPIRED: "This quote expired. Request a new one and try again.",
4493
- SDK_RAMPS_ASSET_NOT_ENABLED: "This currency is not available for that route right now.",
4494
- SDK_RAMPS_KYC_REQUIRED: "The provider needs to verify your identity before continuing.",
4495
- SDK_RAMPS_WALLET_UNSUPPORTED: "This wallet type cannot be used for this ramp.",
4496
- SDK_RAMPS_PROVIDER_NOT_CONFIGURED: "This ramp provider is not configured for this app yet.",
4497
- SDK_RAMPS_ANCHOR_ERROR: "The provider rejected the request. Please try again in a moment.",
4498
- SDK_RAMPS_BRIDGE_ERROR: "The provider rejected the request. Please try again in a moment.",
4499
- SDK_RAMPS_ETHERFUSE_ERROR: "The provider rejected the request. Please try again in a moment.",
4500
- SDK_RAMPS_INSUFFICIENT_BALANCE: "This wallet does not hold enough to cover that amount. Try a smaller one.",
4501
- // Nothing moved on-chain, so the balance is untouched. Almost always no XLM
4502
- // for the network fee on a wallet the app does not sponsor.
4503
- SDK_RAMPS_ONCHAIN_SUBMIT_FAILED: "The network rejected the transaction, so nothing was sent. The wallet may need XLM for the fee."
4712
+ SDK_RAMPS_QUOTE_EXPIRED: "This quote expired. Find a route again.",
4713
+ SDK_RAMPS_QUOTE_NOT_FOUND: "This quote is no longer available. Find a route again.",
4714
+ SDK_RAMPS_ACTION_STALE: "This step changed. It was refreshed; try again.",
4715
+ SDK_RAMPS_TX_NOT_FOUND: "This operation was not found.",
4716
+ SDK_RAMPS_WALLET_NOT_READY: "Approve the new wallet account on your device, then try again."
4504
4717
  };
4505
- function rampErrorMessage(e, fallback) {
4506
- const body = e?.body;
4507
- const code = e?.code;
4508
- if (typeof code !== "string") return e instanceof Error ? e.message : fallback;
4509
- const limit = body?.limit;
4510
- const value = body?.limitAmount;
4511
- const currency = body?.limitCurrency;
4512
- if ((limit === "min" || limit === "max") && typeof value === "number" && typeof currency === "string") {
4513
- return limitMessage(limit, value, currency);
4514
- }
4515
- return RAMP_ERROR_MESSAGES[code] ?? (e instanceof Error ? e.message : fallback);
4718
+ function rampErrorMessage(error, fallback) {
4719
+ const unavailable = appUnavailableMessage(error);
4720
+ if (unavailable) return unavailable;
4721
+ if (isPollarApiError(error)) {
4722
+ const known = RAMP_ERROR_MESSAGES[error.code];
4723
+ if (known) return known;
4724
+ const message = error.body.message;
4725
+ if (typeof message === "string" && message && !/^[A-Z0-9_]+$/.test(message)) return message;
4726
+ }
4727
+ return fallback;
4516
4728
  }
4517
- function explorerTxUrl2(chain, hash, network) {
4518
- if (!hash) return null;
4519
- const testnet = network === "testnet";
4520
- if (chain === "SOLANA") return `https://explorer.solana.com/tx/${hash}${testnet ? "?cluster=devnet" : ""}`;
4521
- if (chain === "POLYGON") return `https://${testnet ? "amoy." : ""}polygonscan.com/tx/${hash}`;
4522
- return `https://stellar.expert/explorer/${testnet ? "testnet" : "public"}/tx/${hash}`;
4729
+ function routeKey(quote) {
4730
+ const node = (n) => n.kind === "asset" ? `${n.code}:${n.chain}` : `${n.currency}:${n.rail}`;
4731
+ return quote.legs.map((leg) => `${leg.kind}:${node(leg.from)}>${node(leg.to)}`).join("|");
4732
+ }
4733
+ var DECIMAL = /^\d+(\.\d+)?$/;
4734
+ async function signLegWithWallet(client, orderId, actionId) {
4735
+ const lent = [];
4736
+ const lend = (chain, encoding, handler) => {
4737
+ try {
4738
+ lent.push(client.registerRampSigningHandler(chain, encoding, handler));
4739
+ } catch {
4740
+ }
4741
+ };
4742
+ lend("STELLAR", "xdr", async (action) => {
4743
+ const wallet = client.getWallet();
4744
+ if (!wallet) throw new Error("Connect the Stellar wallet this step was built for.");
4745
+ assertRampStellarSigner(action, wallet.address);
4746
+ const outcome = await client.signTx(action.payload.value);
4747
+ if (client.getWallet()?.address !== wallet.address)
4748
+ throw new Error("The connected wallet changed. Refresh before signing.");
4749
+ if (outcome.status === "signed") return outcome.signedXdr;
4750
+ throw new Error(
4751
+ outcome.status === "error" ? outcome.message ?? outcome.details ?? "Signing was cancelled." : "Signing was cancelled."
4752
+ );
4753
+ });
4754
+ lend("SOLANA", "solana-v0-json", async (action) => {
4755
+ const payload = JSON.parse(action.payload.value);
4756
+ const solanaWallet = () => client.getWallets().find((w) => w.chain === "SOLANA" && w.custody === "external");
4757
+ const wallet = solanaWallet();
4758
+ if (!wallet || payload.from !== wallet.address || !payload.unsignedTransaction)
4759
+ throw new Error("This step must be signed by the Solana wallet it was built for. Reconnect that wallet.");
4760
+ const signed = await client.signSolanaTransaction(payload.unsignedTransaction);
4761
+ if (solanaWallet()?.address !== wallet.address) throw new Error("The connected wallet changed. Refresh before signing.");
4762
+ return signed;
4763
+ });
4764
+ try {
4765
+ return await client.signRampLeg(orderId, actionId);
4766
+ } finally {
4767
+ for (const release of lent) release();
4768
+ }
4523
4769
  }
4524
4770
  function RampWidget({ onClose }) {
4525
- const { getClient, signTx, wallet, styles, network } = usePollar();
4526
- const walletAddress = wallet?.address ?? "";
4771
+ const {
4772
+ getClient,
4773
+ styles,
4774
+ network,
4775
+ openCosignModal,
4776
+ setCosignNextStep,
4777
+ openRecoveryModal,
4778
+ recovery,
4779
+ recoveryLocked,
4780
+ appConfig,
4781
+ cosign,
4782
+ refreshWalletBalance
4783
+ } = usePollar();
4527
4784
  const client = getClient();
4528
4785
  const { theme, accentColor, styleOverrides, overlayStyle } = modalChrome(styles);
4529
4786
  const [step, setStep] = useState("input");
4530
- const [direction, setDirection] = useState("onramp");
4531
- const [amount, setAmount] = useState("");
4532
- const [currency, setCurrency] = useState("");
4787
+ const [direction, setDirection] = useState("ONRAMP");
4788
+ const [routes, setRoutes] = useState([]);
4789
+ const [routesLoading, setRoutesLoading] = useState(true);
4533
4790
  const [country, setCountry] = useState("");
4534
- const [fieldValues, setFieldValues] = useState({});
4535
- const setFieldValue = (key, value) => setFieldValues((v) => ({ ...v, [key]: value }));
4536
- const [countries, setCountries] = useState([]);
4537
- const [countriesLoading, setCountriesLoading] = useState(true);
4538
- const [refreshing, setRefreshing] = useState(false);
4791
+ const [fiatChoice, setFiatChoice] = useState("");
4792
+ const [assetChoice, setAssetChoice] = useState("");
4793
+ const [amount, setAmount] = useState("");
4539
4794
  const [quotes, setQuotes] = useState([]);
4540
- const [requirementsRequired, setRequirementsRequired] = useState([]);
4541
- const [selectedQuote, setSelectedQuote] = useState(null);
4795
+ const [rejected, setRejected] = useState([]);
4796
+ const [selected, setSelected] = useState(null);
4797
+ const [startingQuoteId, setStartingQuoteId] = useState(null);
4798
+ const [fieldValues, setFieldValues] = useState({});
4799
+ const [order, setOrder] = useState(null);
4800
+ const [pendingOrders, setPendingOrders] = useState([]);
4542
4801
  const [isLoading, setIsLoading] = useState(false);
4543
- const [pendingKyc, setPendingKyc] = useState(null);
4544
- const [noticeMsg, setNoticeMsg] = useState(null);
4545
- const kycAttempt = useRef(null);
4546
- useEffect(
4547
- () => () => {
4548
- kycAttempt.current = null;
4549
- },
4550
- []
4551
- );
4552
- const [txId, setTxId] = useState(null);
4553
- const [provider, setProvider] = useState("");
4554
- const [kycUrl, setKycUrl] = useState(null);
4555
- const [tosUrl, setTosUrl] = useState(null);
4556
- const [kycPending, setKycPending] = useState(false);
4557
- const [kycApproved, setKycApproved] = useState(false);
4558
- const [onboardingStatus, setOnboardingStatus] = useState(void 0);
4559
- const [txStatus, setTxStatus] = useState(null);
4560
- const [stellarTxHash, setStellarTxHash] = useState(null);
4561
- const [txHash, setTxHash] = useState(null);
4562
- const [txChain, setTxChain] = useState(null);
4563
- const [fundsInTransit, setFundsInTransit] = useState(false);
4564
- const [retrying, setRetrying] = useState(false);
4565
- const [depositInstructions, setDepositInstructions] = useState(null);
4566
- const [completing, setCompleting] = useState(false);
4802
+ const [refreshing, setRefreshing] = useState(false);
4803
+ const [signing, setSigning] = useState(false);
4567
4804
  const [errorMsg, setErrorMsg] = useState(null);
4568
- const signingRef = useRef(false);
4569
- const directionRef = useRef(direction);
4570
- directionRef.current = direction;
4571
- function applySettlement(tx) {
4572
- if (tx.stellarTxHash) setStellarTxHash(tx.stellarTxHash);
4573
- if (tx.txHash) setTxHash(tx.txHash);
4574
- if (tx.chain) setTxChain(tx.chain);
4575
- setFundsInTransit(tx.fundsInTransit === true);
4576
- }
4577
- async function handleRetryTransit() {
4578
- if (!txId || retrying) return;
4579
- setRetrying(true);
4580
- setErrorMsg(null);
4581
- try {
4582
- const tx = await client.retryRampTransaction(txId);
4583
- setTxStatus(tx.status);
4584
- applySettlement(tx);
4585
- } catch (e) {
4586
- setErrorMsg(e instanceof Error && e.message ? e.message : "Could not retry the transfer. Try again in a moment.");
4587
- } finally {
4588
- setRetrying(false);
4589
- }
4590
- }
4591
- useEffect(() => {
4592
- if (step !== "status" || !txId) return;
4593
- if (txStatus && TERMINAL.includes(txStatus)) return;
4594
- let active = true;
4595
- const id = setInterval(async () => {
4596
- try {
4597
- const tx = await client.getRampTransaction(txId);
4598
- if (!active) return;
4599
- setTxStatus(tx.status);
4600
- applySettlement(tx);
4601
- const pending = tx.pendingSignature;
4602
- if (pending && !signingRef.current) {
4603
- signingRef.current = true;
4604
- void resumeRef.current(txId, pending).finally(() => {
4605
- signingRef.current = false;
4606
- });
4607
- }
4608
- if (tx.kycUrl) setKycUrl(tx.kycUrl);
4609
- if (tx.depositInstructions) setDepositInstructions(tx.depositInstructions);
4610
- if (TERMINAL.includes(tx.status)) clearInterval(id);
4611
- } catch {
4612
- }
4613
- }, 5e3);
4614
- return () => {
4615
- active = false;
4616
- clearInterval(id);
4617
- };
4618
- }, [step, txId, txStatus, client]);
4805
+ const [verifying, setVerifying] = useState(null);
4806
+ const [trustingAsset, setTrustingAsset] = useState(null);
4807
+ const [walletBusy, setWalletBusy] = useState(null);
4808
+ const [retrying, setRetrying] = useState(false);
4809
+ const [awaitingAccount, setAwaitingAccount] = useState(null);
4810
+ const [pollTrouble, setPollTrouble] = useState(false);
4811
+ const [pollKey, setPollKey] = useState(0);
4812
+ const [paymentCheckUntil, setPaymentCheckUntil] = useState(null);
4813
+ const promptedApproval = useRef(null);
4814
+ const custodyChoice = appConfig.application?.walletCustodyMode === "USER_CHOICE";
4815
+ const appChains = appConfig.application?.chains;
4816
+ const verifyAttempt = useRef(null);
4817
+ const countries = useMemo(() => rampCountries(routes), [routes]);
4818
+ const fiats = countries.find((c) => c.country === country)?.fiats ?? [];
4819
+ const fiat = fiats.find((f) => fiatKey(f) === fiatChoice) ?? fiats[0] ?? null;
4820
+ const assets = useMemo(() => {
4821
+ const seen = /* @__PURE__ */ new Map();
4822
+ for (const route of routes)
4823
+ if (fiat && route.fiat.country === country && fiatKey(route.fiat) === fiatKey(fiat))
4824
+ seen.set(assetKey3(route.asset), route.asset);
4825
+ return [...seen.values()];
4826
+ }, [routes, country, fiat]);
4827
+ const asset = assets.find((a) => assetKey3(a) === assetChoice) ?? assets[0] ?? null;
4828
+ const fields = selected ? rampQuoteFields(selected) : [];
4829
+ const sellAvailable = useWalletAvailable(
4830
+ direction === "OFFRAMP" && asset ? { chain: asset.chain, code: asset.code, issuer: asset.identifier } : null
4831
+ );
4619
4832
  useEffect(() => {
4620
- if (step !== "status" || !kycPending || kycApproved) return;
4621
- if (onboardingStatus === "awaiting_provider") return;
4622
- let active = true;
4623
- const check = async () => {
4624
- try {
4625
- const { hasApproved } = await client.getRampKycStatus();
4626
- if (active && hasApproved) setKycApproved(true);
4627
- } catch {
4628
- }
4629
- };
4630
- void check();
4631
- const id = setInterval(check, 1e4);
4632
- return () => {
4633
- active = false;
4634
- clearInterval(id);
4635
- };
4636
- }, [step, kycPending, kycApproved, onboardingStatus, client]);
4637
- async function loadCountries(resetSelection) {
4638
- setCountriesLoading(true);
4833
+ if (direction === "OFFRAMP") void refreshWalletBalance();
4834
+ }, [direction, refreshWalletBalance]);
4835
+ async function loadRoutes(next) {
4836
+ setRoutesLoading(true);
4639
4837
  try {
4640
- const { countries: list } = await client.getRampCountries();
4641
- setCountries(list);
4642
- const first = list[0];
4643
- const stillValid = list.some((c) => c.code === country);
4644
- if (first && (resetSelection || !stillValid)) {
4645
- setCountry(first.code);
4646
- if (first.currency) setCurrency(first.currency);
4647
- }
4648
- } catch {
4649
- setCountries([]);
4838
+ const { routes: found } = await client.getRampRoutes(next);
4839
+ setRoutes(found);
4840
+ const first = rampCountries(found)[0]?.country ?? "";
4841
+ setCountry((current) => found.some((r) => r.fiat.country === current) ? current : first);
4842
+ } catch (error) {
4843
+ setRoutes([]);
4844
+ setErrorMsg(rampErrorMessage(error, "Could not load the ramp routes."));
4650
4845
  } finally {
4651
- setCountriesLoading(false);
4846
+ setRoutesLoading(false);
4652
4847
  }
4653
4848
  }
4654
- useEffect(() => {
4655
- loadCountries(true);
4656
- }, []);
4657
- function handleCountryChange(code) {
4658
- setCountry(code);
4659
- const match = countries.find((c) => c.code === code);
4660
- if (match?.currency) setCurrency(match.currency);
4661
- }
4662
- async function handleRefresh() {
4663
- if (refreshing) return;
4664
- setRefreshing(true);
4849
+ async function loadPendingOrders() {
4665
4850
  try {
4666
- await loadCountries(false);
4667
- if (step === "select_route") {
4668
- const result = await client.getRampsQuote({ country, amount: Number(amount), currency, direction });
4669
- setQuotes(result.quotes ?? []);
4670
- setRequirementsRequired(result.requirementsRequired ?? []);
4671
- } else if (step === "status" && txId) {
4672
- const tx = await client.getRampTransaction(txId);
4673
- setTxStatus(tx.status);
4674
- applySettlement(tx);
4675
- if (tx.kycUrl) setKycUrl(tx.kycUrl);
4676
- if (tx.depositInstructions) setDepositInstructions(tx.depositInstructions);
4677
- }
4851
+ const page = await client.listRampOrders({ open: "true", limit: 5 });
4852
+ setPendingOrders(page.orders);
4678
4853
  } catch {
4679
- } finally {
4680
- setRefreshing(false);
4854
+ setPendingOrders([]);
4681
4855
  }
4682
4856
  }
4683
- function resetToInput({ keepMessage = false } = {}) {
4684
- setStep("input");
4685
- setQuotes([]);
4686
- setRequirementsRequired([]);
4687
- setSelectedQuote(null);
4688
- setFieldValues({});
4689
- setTxId(null);
4690
- setProvider("");
4691
- setKycUrl(null);
4692
- setTosUrl(null);
4693
- setTxStatus(null);
4694
- setStellarTxHash(null);
4695
- setTxHash(null);
4696
- setTxChain(null);
4697
- setFundsInTransit(false);
4698
- setDepositInstructions(null);
4699
- setKycPending(false);
4700
- setKycApproved(false);
4701
- setNoticeMsg(null);
4702
- if (!keepMessage) setErrorMsg(null);
4703
- }
4704
- async function resumeWithSignature(id, ps) {
4705
- const outcome = await signTx(ps.unsignedXdr);
4706
- if (outcome.status === "cosign_pending") {
4707
- setErrorMsg("Approve the signature on your device, then resume.");
4708
- setStep("error");
4709
- return;
4710
- }
4711
- if (outcome.status !== "signed") {
4712
- setErrorMsg(outcome.message ?? outcome.details ?? "Signing was cancelled.");
4713
- setStep("error");
4714
- return;
4715
- }
4716
- const result = await client.submitRampSignature(id, {
4717
- signedXdr: outcome.signedXdr,
4718
- // The CCTP actions are not in the generated schema until it is regenerated.
4719
- action: ps.action
4857
+ useEffect(() => {
4858
+ void loadRoutes(direction);
4859
+ }, [client, direction]);
4860
+ useEffect(() => {
4861
+ void loadPendingOrders();
4862
+ }, [client]);
4863
+ const orderId = order?.id ?? null;
4864
+ useEffect(() => {
4865
+ if (step !== "status" || !orderId) return;
4866
+ const controller = new AbortController();
4867
+ client.pollRampOrder(orderId, {
4868
+ signal: controller.signal,
4869
+ onUpdate: (next) => {
4870
+ setOrder(next);
4871
+ setPollTrouble(false);
4872
+ },
4873
+ onError: () => setPollTrouble(true)
4874
+ }).catch((error) => {
4875
+ if (!controller.signal.aborted) setErrorMsg(rampErrorMessage(error, "This operation could not be read."));
4720
4876
  });
4721
- await applyResult(result);
4722
- }
4723
- const resumeRef = useRef(resumeWithSignature);
4724
- resumeRef.current = resumeWithSignature;
4725
- async function applyResult(result) {
4726
- setTxId(result.txId);
4727
- setProvider(result.provider);
4728
- if (result.pendingSignature) {
4729
- await resumeWithSignature(result.txId, result.pendingSignature);
4877
+ return () => controller.abort();
4878
+ }, [client, step, orderId, pollKey]);
4879
+ const awaitingPayment = step === "status" && order?.pendingAction?.kind === "FIAT_PAYMENT";
4880
+ useEffect(() => {
4881
+ if (paymentCheckUntil === null || !orderId) return;
4882
+ if (!awaitingPayment) {
4883
+ setPaymentCheckUntil(null);
4884
+ setPollKey((key) => key + 1);
4730
4885
  return;
4731
4886
  }
4732
- setKycUrl(result.kycUrl ?? null);
4733
- setTosUrl(result.tosUrl ?? null);
4734
- setOnboardingStatus(result.onboardingStatus);
4735
- setKycPending(result.kycRequired === true);
4736
- if (result.kycRequired) setKycApproved(false);
4737
- setTxStatus(result.status);
4738
- setStellarTxHash(result.stellarTxHash ?? null);
4739
- setDepositInstructions(result.depositInstructions ?? null);
4740
- setStep("status");
4741
- }
4742
- async function loadQuotes() {
4743
- const result = await client.getRampsQuote({ country, amount: Number(amount), currency, direction });
4744
- const list = result.quotes ?? [];
4745
- const locked = result.requirementsRequired ?? [];
4746
- if (list.length === 0 && locked.length === 0) {
4747
- const down = (result.unavailable ?? []).map((u) => u.provider);
4748
- setErrorMsg(
4749
- down.length > 0 ? `${down.join(", ")} ${down.length === 1 ? "is" : "are"} temporarily unavailable. Please try again later.` : `No ramp providers available for ${country} yet.`
4750
- );
4751
- setStep("error");
4752
- return false;
4887
+ const timer = setInterval(() => {
4888
+ if (Date.now() >= paymentCheckUntil) {
4889
+ setPaymentCheckUntil(null);
4890
+ return;
4891
+ }
4892
+ client.checkRampOrder(orderId).then(setOrder).catch(() => void 0);
4893
+ }, PAYMENT_CHECK_EVERY_MS);
4894
+ return () => clearInterval(timer);
4895
+ }, [client, orderId, awaitingPayment, paymentCheckUntil]);
4896
+ const approval = order?.pendingAction?.kind === "COSIGN_APPROVAL" ? order.pendingAction.actionId : null;
4897
+ useEffect(() => {
4898
+ if (!approval || promptedApproval.current === approval) return;
4899
+ promptedApproval.current = approval;
4900
+ void client.cosign.pending().catch(() => void 0).then(() => openCosignModal());
4901
+ }, [approval, client, openCosignModal]);
4902
+ const nextApproval = step === "status" && order ? rampNextApproval(order) : null;
4903
+ useEffect(() => {
4904
+ setCosignNextStep(nextApproval);
4905
+ }, [nextApproval, setCosignNextStep]);
4906
+ useEffect(() => () => setCosignNextStep(null), [setCosignNextStep]);
4907
+ const resolvedRequest = cosign.lastResolved;
4908
+ useEffect(() => {
4909
+ if (!awaitingAccount || resolvedRequest?.id !== awaitingAccount.requestId) return;
4910
+ setAwaitingAccount(null);
4911
+ if (resolvedRequest.status !== "APPROVED") {
4912
+ setErrorMsg("The new account was not approved, so this route cannot start.");
4913
+ return;
4753
4914
  }
4754
- setQuotes(list);
4755
- setRequirementsRequired(locked);
4756
- return true;
4915
+ void resumeRoute(awaitingAccount.routeKey);
4916
+ }, [awaitingAccount, resolvedRequest]);
4917
+ function handleDirectionChange(next) {
4918
+ setDirection(next);
4919
+ setErrorMsg(null);
4920
+ setAwaitingAccount(null);
4757
4921
  }
4758
4922
  async function handleFindRoute() {
4923
+ if (!fiat || !asset) return [];
4924
+ if (!DECIMAL.test(amount.trim()) || Number(amount) <= 0) {
4925
+ setErrorMsg("Enter an amount greater than zero.");
4926
+ return [];
4927
+ }
4759
4928
  setStep("loading_quote");
4760
- setIsLoading(true);
4761
4929
  setErrorMsg(null);
4930
+ setSelected(null);
4762
4931
  try {
4763
- if (!await loadQuotes()) return;
4932
+ const result = await client.quoteRamp({
4933
+ direction,
4934
+ fiat: { currency: fiat.currency, rail: fiat.rail, country: fiat.country },
4935
+ asset: { code: asset.code, identifier: asset.identifier, chain: asset.chain },
4936
+ amount: { value: amount.trim(), denomination: "in" }
4937
+ });
4938
+ setQuotes(result.quotes);
4939
+ setRejected(result.rejected);
4764
4940
  setStep("select_route");
4765
- } catch (e) {
4766
- setErrorMsg(rampErrorMessage(e, "Failed to fetch quotes."));
4941
+ return result.quotes;
4942
+ } catch (error) {
4943
+ if (isPollarApiError(error) && error.code === "SDK_RAMPS_NO_ROUTE") {
4944
+ setQuotes([]);
4945
+ setRejected(Array.isArray(error.body.rejected) ? error.body.rejected : []);
4946
+ setStep("select_route");
4947
+ return [];
4948
+ }
4949
+ setErrorMsg(rampErrorMessage(error, "Could not find a route. Please try again."));
4767
4950
  setStep("error");
4951
+ return [];
4952
+ }
4953
+ }
4954
+ function handleVerifyRoute(requirement) {
4955
+ setErrorMsg(null);
4956
+ verifyAttempt.current = requirement;
4957
+ setVerifying(requirement);
4958
+ }
4959
+ function closeVerification() {
4960
+ verifyAttempt.current = null;
4961
+ setVerifying(null);
4962
+ }
4963
+ function verificationDone(requirement) {
4964
+ if (verifyAttempt.current !== requirement) return;
4965
+ closeVerification();
4966
+ void handleFindRoute();
4967
+ }
4968
+ async function handleTrustAsset(asset2) {
4969
+ if (trustingAsset) return;
4970
+ setTrustingAsset(`${asset2.code}:${asset2.issuer}`);
4971
+ setErrorMsg(null);
4972
+ try {
4973
+ const outcome = await client.setTrustline(asset2);
4974
+ if (outcome.status === "error") {
4975
+ setErrorMsg(rampTrustlineErrorText(outcome, asset2.code));
4976
+ return;
4977
+ }
4978
+ if (outcome.status === "cosign_pending") {
4979
+ setErrorMsg(`Approve the ${asset2.code} trustline, then refresh the routes.`);
4980
+ openCosignModal();
4981
+ return;
4982
+ }
4983
+ await client.refreshAssets().catch(() => {
4984
+ });
4985
+ await handleFindRoute();
4986
+ } catch (error) {
4987
+ setErrorMsg(rampErrorMessage(error, `Could not add the ${asset2.code} trustline. Please try again.`));
4768
4988
  } finally {
4769
- setIsLoading(false);
4989
+ setTrustingAsset(null);
4770
4990
  }
4771
4991
  }
4772
- function handleSelectQuote(quote) {
4773
- setSelectedQuote(quote);
4992
+ async function handleWalletStep(walletStep, index, custody) {
4993
+ if (walletBusy !== null) return;
4994
+ setWalletBusy(index);
4774
4995
  setErrorMsg(null);
4775
- setNoticeMsg(null);
4776
- const broken = brokenLimitOf(Number(amount), quote);
4777
- if (broken) {
4778
- setErrorMsg(limitMessage(broken.limit, broken.value, currency));
4779
- return;
4996
+ try {
4997
+ const { recoveryRequired } = await client.runRampWalletStep(walletStep, custody);
4998
+ if (recoveryRequired) {
4999
+ setErrorMsg("Add a way to recover your wallet, then find the route again.");
5000
+ openRecoveryModal();
5001
+ return;
5002
+ }
5003
+ await handleFindRoute();
5004
+ } catch (error) {
5005
+ const code = isPollarApiError(error) ? error.code : null;
5006
+ if (code === "SDK_RAMPS_WALLET_NOT_READY") {
5007
+ setErrorMsg("Approve the new account, then find the route again.");
5008
+ openCosignModal();
5009
+ return;
5010
+ }
5011
+ setErrorMsg(
5012
+ code === "SDK_WALLET_PROVISIONING_DISABLED" ? "This app does not create wallets." : rampErrorMessage(error, "Could not prepare the wallet. Please try again.")
5013
+ );
5014
+ } finally {
5015
+ setWalletBusy(null);
4780
5016
  }
4781
- const fields = requiredFieldsOf(quote);
4782
- const missing = fields.some((f) => !f.optional && !(fieldValues[f.key] ?? "").trim());
4783
- if (fields.length > 0 && missing) {
4784
- setStep("contact");
5017
+ }
5018
+ async function resumeRoute(key) {
5019
+ const same = (await handleFindRoute()).find((q) => routeKey(q) === key);
5020
+ if (same) handleSelectQuote(same, true);
5021
+ else setErrorMsg("That route is no longer available. Pick another one.");
5022
+ }
5023
+ function handleSelectQuote(quote, fresh = false) {
5024
+ if (!fresh && Date.parse(quote.expiresAt) <= Date.now()) {
5025
+ void resumeRoute(routeKey(quote));
4785
5026
  return;
4786
5027
  }
4787
- void startRamp(quote);
4788
- }
4789
- function handleContactContinue() {
4790
- if (selectedQuote) void startRamp(selectedQuote);
5028
+ setSelected(quote);
5029
+ setErrorMsg(null);
5030
+ const missing = rampQuoteFields(quote).some((f) => !f.optional && !(fieldValues[f.key] ?? "").trim());
5031
+ if (missing) setStep("contact");
5032
+ else void startOrder(quote, fresh);
4791
5033
  }
4792
- async function startRamp(quote) {
5034
+ async function startOrder(quote, fresh = false) {
4793
5035
  setIsLoading(true);
5036
+ setStartingQuoteId(quote.quoteId);
4794
5037
  setErrorMsg(null);
5038
+ setAwaitingAccount(null);
4795
5039
  try {
4796
- const base = { quoteId: quote.quoteId, amount: Number(amount), currency, country };
4797
- if (walletAddress) base.walletAddress = walletAddress;
4798
- const STANDARD_BODY_KEYS = /* @__PURE__ */ new Set(["email", "fullName", "taxId", "qrCode"]);
4799
- const extraFields = {};
4800
- for (const f of requiredFieldsOf(quote)) {
4801
- const val = (fieldValues[f.key] ?? "").trim();
4802
- if (!val) continue;
4803
- if (f.bankType) base.bankDetails = { type: f.bankType, value: val };
4804
- else if (STANDARD_BODY_KEYS.has(f.key)) base[f.key] = val;
4805
- else extraFields[f.key] = val;
5040
+ const payee = Object.fromEntries(
5041
+ rampQuoteFields(quote).map((f) => [f.key, (fieldValues[f.key] ?? "").trim()]).filter(([, value]) => value)
5042
+ );
5043
+ const created = await client.createRampOrder({
5044
+ quoteId: quote.quoteId,
5045
+ ...Object.keys(payee).length ? { payee } : {}
5046
+ });
5047
+ setOrder(created);
5048
+ setStep("status");
5049
+ } catch (error) {
5050
+ if (isPollarApiError(error) && error.code === "SDK_RAMPS_WALLET_NOT_READY") {
5051
+ const pending = await client.cosign.pending().catch(() => []);
5052
+ const request = pending.find((r) => r.intent === "add_accounts");
5053
+ if (request) setAwaitingAccount({ routeKey: routeKey(quote), requestId: request.id });
5054
+ else setErrorMsg(rampErrorMessage(error, "Could not start the operation. Please try again."));
5055
+ openCosignModal();
5056
+ setStep("select_route");
5057
+ return;
4806
5058
  }
4807
- if (Object.keys(extraFields).length > 0) base.fields = extraFields;
4808
- const result = direction === "onramp" ? await client.createOnRamp(base) : await client.createOffRamp(base);
4809
- await applyResult(result);
4810
- } catch (e) {
4811
- const requirement = requiredRampKyc(e);
4812
- if (requirement) {
4813
- setStep(requiredFieldsOf(quote).length ? "contact" : "select_route");
4814
- setErrorMsg(
4815
- requirement.type === "KYC" ? "Identity verification is required before continuing." : "A few more details are required before continuing."
4816
- );
4817
- kycAttempt.current = requirement;
4818
- setPendingKyc(requirement);
5059
+ if (isPollarApiError(error) && error.code === "SDK_RAMPS_QUOTE_EXPIRED" && !fresh) {
5060
+ void resumeRoute(routeKey(quote));
4819
5061
  return;
4820
5062
  }
4821
- setErrorMsg(rampErrorMessage(e, "Failed to start the ramp."));
4822
- setStep("error");
5063
+ setErrorMsg(rampErrorMessage(error, "Could not start the operation. Please try again."));
5064
+ if (isPollarApiError(error) && error.code === "VALIDATION_ERROR" && rampQuoteFields(quote).length) setStep("contact");
5065
+ else setStep("select_route");
4823
5066
  } finally {
4824
5067
  setIsLoading(false);
5068
+ setStartingQuoteId(null);
4825
5069
  }
4826
5070
  }
4827
- async function requoteAfterKyc(completed = "KYC") {
4828
- setStep("loading_quote");
5071
+ async function handleSign() {
5072
+ const action = order?.pendingAction;
5073
+ if (!order || action?.kind !== "SIGNATURE" || signing) return;
5074
+ setSigning(true);
5075
+ setErrorMsg(null);
5076
+ try {
5077
+ setOrder(await signLegWithWallet(client, order.id, action.actionId));
5078
+ } catch (error) {
5079
+ setErrorMsg(rampErrorMessage(error, error instanceof Error && error.message ? error.message : "Signing was cancelled."));
5080
+ setOrder(await client.getRampOrder(order.id).catch(() => order));
5081
+ } finally {
5082
+ setSigning(false);
5083
+ }
5084
+ }
5085
+ async function handleResumeOrder(id) {
4829
5086
  setIsLoading(true);
4830
5087
  setErrorMsg(null);
4831
- setSelectedQuote(null);
4832
5088
  try {
4833
- if (!await loadQuotes()) return;
4834
- setNoticeMsg(
4835
- completed === "KYC" ? "Your identity is verified. Prices may have changed, so choose a route to continue." : "Thanks, your details are saved. Prices may have changed, so choose a route to continue."
4836
- );
4837
- setStep("select_route");
4838
- } catch (e) {
4839
- setErrorMsg(rampErrorMessage(e, "Failed to fetch quotes."));
4840
- setStep("error");
5089
+ const found = await client.getRampOrder(id);
5090
+ setDirection(found.direction);
5091
+ setOrder(found);
5092
+ setStep("status");
5093
+ } catch (error) {
5094
+ setErrorMsg(rampErrorMessage(error, "Could not resume the operation."));
4841
5095
  } finally {
4842
5096
  setIsLoading(false);
4843
5097
  }
4844
5098
  }
4845
- function handleVerifyRoute(requirement) {
4846
- const attempt = pendingFromQuote(requirement);
5099
+ function handleNewOrder() {
5100
+ setAwaitingAccount(null);
5101
+ setOrder(null);
5102
+ setSelected(null);
5103
+ setQuotes([]);
5104
+ setRejected([]);
4847
5105
  setErrorMsg(null);
4848
- setNoticeMsg(null);
4849
- kycAttempt.current = attempt;
4850
- setPendingKyc(attempt);
5106
+ setStep("input");
5107
+ void loadPendingOrders();
4851
5108
  }
4852
- function handleOpenKyc() {
4853
- if (kycUrl) window.open(kycUrl, "_blank", "noopener,noreferrer");
5109
+ function handleBack() {
5110
+ setErrorMsg(null);
5111
+ setAwaitingAccount(null);
5112
+ setStep(step === "contact" ? "select_route" : "input");
4854
5113
  }
4855
- function handleOpenTos() {
4856
- if (tosUrl) window.open(tosUrl, "_blank", "noopener,noreferrer");
5114
+ function handleStepSelect(target) {
5115
+ setErrorMsg(null);
5116
+ setAwaitingAccount(null);
5117
+ setStep(target === "Route" ? "select_route" : "input");
4857
5118
  }
4858
- async function handleCompleteWithdraw() {
4859
- if (!txId) return;
4860
- setCompleting(true);
5119
+ async function handleCheckPayment() {
5120
+ if (!order || paymentCheckUntil !== null) return;
4861
5121
  setErrorMsg(null);
5122
+ setPaymentCheckUntil(Date.now() + PAYMENT_CHECK_WINDOW_MS);
4862
5123
  try {
4863
- const result = await client.completeWithdraw(txId);
4864
- if (result.pendingSignature) {
4865
- await resumeWithSignature(txId, result.pendingSignature);
4866
- return;
4867
- }
4868
- setTxStatus(result.status);
4869
- setStellarTxHash(result.stellarTxHash ?? null);
4870
- } catch (e) {
4871
- const msg = e instanceof Error ? e.message : "";
4872
- setErrorMsg(
4873
- msg.includes("KYC") ? "Finish KYC at the provider first, then try again." : msg || "Failed to complete the withdrawal."
4874
- );
5124
+ setOrder(await client.checkRampOrder(order.id));
5125
+ } catch (error) {
5126
+ setPaymentCheckUntil(null);
5127
+ setErrorMsg(rampErrorMessage(error, "Could not check the payment. Please try again."));
5128
+ }
5129
+ }
5130
+ async function handleRetryOrder() {
5131
+ if (!order || retrying) return;
5132
+ setRetrying(true);
5133
+ setErrorMsg(null);
5134
+ try {
5135
+ setOrder(await client.retryRampOrder(order.id));
5136
+ setPollKey((key) => key + 1);
5137
+ } catch (error) {
5138
+ setErrorMsg(rampErrorMessage(error, "Could not try again. Please try again."));
4875
5139
  } finally {
4876
- setCompleting(false);
5140
+ setRetrying(false);
4877
5141
  }
4878
5142
  }
4879
- const kycBlocking = kycPending && !kycApproved;
4880
- const canComplete = direction === "offramp" && step === "status" && txStatus !== "completed" && !stellarTxHash && !txHash && !kycPending;
4881
- const flowSteps = flowStepsOf(quotes, selectedQuote);
4882
- const flowStepIndex = flowSteps.indexOf(STEP_LABEL[step] ?? "");
4883
- if (pendingKyc?.type === "FORM") {
4884
- return /* @__PURE__ */ jsx(
4885
- RequirementFormModal,
4886
- {
4887
- formId: pendingKyc.optionId,
4888
- ...pendingKyc.progress ? { progress: pendingKyc.progress } : {},
4889
- onClose: () => {
4890
- kycAttempt.current = null;
4891
- setPendingKyc(null);
4892
- },
4893
- onSubmitted: () => {
4894
- if (kycAttempt.current !== pendingKyc) return;
4895
- kycAttempt.current = null;
4896
- setPendingKyc(null);
4897
- void requoteAfterKyc("FORM");
4898
- }
5143
+ const heldForRecovery = step === "status" && order?.failure?.resolutionAction === "set_up_recovery";
5144
+ const wasRecoveryLocked = useRef(recoveryLocked);
5145
+ useEffect(() => {
5146
+ if (heldForRecovery && wasRecoveryLocked.current && !recoveryLocked && recovery) void handleRetryOrder();
5147
+ wasRecoveryLocked.current = recoveryLocked;
5148
+ }, [heldForRecovery, recoveryLocked, recovery]);
5149
+ async function handleRefresh() {
5150
+ if (refreshing) return;
5151
+ setRefreshing(true);
5152
+ try {
5153
+ if (step === "status" && order) {
5154
+ setOrder(await client.getRampOrder(order.id));
5155
+ setPollKey((key) => key + 1);
5156
+ } else if (step === "select_route") await handleFindRoute();
5157
+ else {
5158
+ await loadRoutes(direction);
5159
+ await loadPendingOrders();
5160
+ if (direction === "OFFRAMP") await refreshWalletBalance();
4899
5161
  }
4900
- );
4901
- }
4902
- if (pendingKyc?.type === "REGISTRY_CHECK" || pendingKyc?.type === "PROVIDER_REGISTRATION") {
4903
- const close = () => {
4904
- kycAttempt.current = null;
4905
- setPendingKyc(null);
4906
- };
4907
- const done = () => {
4908
- if (kycAttempt.current !== pendingKyc) return;
4909
- close();
4910
- void requoteAfterKyc(pendingKyc.type);
4911
- };
4912
- const progress = pendingKyc.progress ? { progress: pendingKyc.progress } : {};
4913
- return pendingKyc.type === "REGISTRY_CHECK" ? /* @__PURE__ */ jsx(RegistryCheckModal, { optionId: pendingKyc.optionId, ...progress, onClose: close, onApproved: done }) : /* @__PURE__ */ jsx(ProviderRegistrationModal, { corridorId: pendingKyc.corridorId, ...progress, onClose: close, onRegistered: done });
5162
+ } catch (error) {
5163
+ setErrorMsg(rampErrorMessage(error, "Could not refresh."));
5164
+ } finally {
5165
+ setRefreshing(false);
5166
+ }
4914
5167
  }
4915
- if (pendingKyc) {
5168
+ const flowSteps = fields.length || quotes.some((q) => rampQuoteFields(q).length) ? ["Amount", "Route", "Details", "Complete"] : ["Amount", "Route", "Complete"];
5169
+ const stepLabel = {
5170
+ input: "Amount",
5171
+ loading_quote: "Amount",
5172
+ select_route: "Route",
5173
+ contact: "Details",
5174
+ status: "Complete"
5175
+ };
5176
+ const label = stepLabel[step];
5177
+ if (verifying) {
5178
+ const progress = { progress: { position: verifying.completed + 1, total: verifying.total } };
5179
+ const done = () => verificationDone(verifying);
5180
+ if (verifying.type === "FORM")
5181
+ return /* @__PURE__ */ jsx(RequirementFormModal, { formId: verifying.optionId, ...progress, onClose: closeVerification, onSubmitted: done });
5182
+ if (verifying.type === "REGISTRY_CHECK")
5183
+ return /* @__PURE__ */ jsx(RegistryCheckModal, { optionId: verifying.optionId, ...progress, onClose: closeVerification, onApproved: done });
5184
+ if (verifying.type === "PROVIDER_REGISTRATION")
5185
+ return /* @__PURE__ */ jsx(
5186
+ ProviderRegistrationModal,
5187
+ {
5188
+ corridorId: verifying.corridorId,
5189
+ ...progress,
5190
+ onClose: closeVerification,
5191
+ onRegistered: done
5192
+ }
5193
+ );
4916
5194
  return /* @__PURE__ */ jsx(
4917
5195
  KycModal,
4918
5196
  {
4919
5197
  country,
4920
- corridorId: pendingKyc.corridorId,
4921
- providerId: pendingKyc.optionId,
4922
- onClose: () => {
4923
- kycAttempt.current = null;
4924
- setPendingKyc(null);
4925
- },
4926
- onApproved: () => {
4927
- if (kycAttempt.current !== pendingKyc) return;
4928
- kycAttempt.current = null;
4929
- setPendingKyc(null);
4930
- void requoteAfterKyc();
4931
- }
5198
+ corridorId: verifying.corridorId,
5199
+ providerId: verifying.optionId,
5200
+ onClose: closeVerification,
5201
+ onApproved: done
4932
5202
  }
4933
5203
  );
4934
5204
  }
@@ -4937,59 +5207,79 @@ function RampWidget({ onClose }) {
4937
5207
  {
4938
5208
  theme,
4939
5209
  accentColor,
4940
- styleOverrides,
5210
+ ...styleOverrides ? { styleOverrides } : {},
5211
+ network,
4941
5212
  step,
4942
5213
  flowSteps,
4943
- flowStepIndex,
4944
- startingQuoteId: isLoading && selectedQuote ? selectedQuote.quoteId : null,
5214
+ flowStepIndex: label ? flowSteps.indexOf(label) : -1,
4945
5215
  direction,
4946
- amount,
4947
- currency,
4948
- country,
4949
- requiredFields: selectedQuote ? requiredFieldsOf(selectedQuote) : [],
4950
- fieldValues,
5216
+ routesLoading,
4951
5217
  countries,
4952
- countriesLoading,
4953
- refreshing,
5218
+ country,
5219
+ fiats,
5220
+ fiat,
5221
+ assets,
5222
+ asset,
5223
+ amount,
5224
+ sellAvailable,
4954
5225
  quotes,
4955
- kycRequired: requirementsRequired,
5226
+ rejected,
5227
+ startingQuoteId,
5228
+ fields,
5229
+ fieldValues,
5230
+ order,
5231
+ pendingOrders,
4956
5232
  isLoading,
4957
- provider,
4958
- txStatus,
4959
- kycUrl,
4960
- tosUrl,
4961
- kycBlocking,
4962
- onboardingStatus: onboardingStatus ?? null,
4963
- kycJustApproved: kycPending && kycApproved,
4964
- stellarTxHash,
4965
- txHash,
4966
- txChain,
4967
- explorerUrl: explorerTxUrl2(txChain ?? "STELLAR", txHash ?? stellarTxHash, network),
4968
- fundsInTransit,
4969
- retrying,
4970
- onRetryTransit: handleRetryTransit,
4971
- depositInstructions,
4972
- canComplete,
4973
- completing,
5233
+ refreshing,
5234
+ signing,
4974
5235
  errorMsg,
4975
- noticeMsg,
4976
- onDirectionChange: setDirection,
4977
- onAmountChange: (next) => {
4978
- setAmount(next);
5236
+ notice: awaitingAccount ? "Approve the new account on your device. The order starts once it is approved." : null,
5237
+ onDirectionChange: handleDirectionChange,
5238
+ onCountryChange: (next) => {
5239
+ setCountry(next);
5240
+ setFiatChoice("");
5241
+ setAssetChoice("");
5242
+ },
5243
+ onFiatChange: (key) => {
5244
+ setFiatChoice(key);
5245
+ setAssetChoice("");
5246
+ },
5247
+ onAssetChange: setAssetChoice,
5248
+ onAmountChange: (value) => {
5249
+ setAmount(value);
4979
5250
  setErrorMsg(null);
4980
5251
  },
4981
- onFieldChange: setFieldValue,
4982
- onCountryChange: handleCountryChange,
4983
- onFindRoute: handleFindRoute,
4984
- onSelectQuote: handleSelectQuote,
5252
+ onFindRoute: () => void handleFindRoute(),
5253
+ onSelectQuote: (quote) => handleSelectQuote(quote),
4985
5254
  onVerifyRoute: handleVerifyRoute,
4986
- onContactContinue: handleContactContinue,
4987
- onOpenKyc: handleOpenKyc,
4988
- onOpenTos: handleOpenTos,
4989
- onCompleteWithdraw: handleCompleteWithdraw,
4990
- onBack: () => resetToInput({ keepMessage: true }),
4991
- onRetry: () => resetToInput(),
4992
- onRefresh: handleRefresh,
5255
+ trustingAsset,
5256
+ onTrustAsset: (asset2) => void handleTrustAsset(asset2),
5257
+ walletBusy,
5258
+ custodyChoice,
5259
+ onWalletStep: (walletStep, index, custody) => void handleWalletStep(walletStep, index, custody),
5260
+ noChains: Array.isArray(appChains) && appChains.length === 0,
5261
+ onFieldChange: (key, value) => setFieldValues((current) => ({ ...current, [key]: value })),
5262
+ onContactContinue: () => {
5263
+ if (selected) void startOrder(selected);
5264
+ },
5265
+ onSign: () => void handleSign(),
5266
+ onApprove: openCosignModal,
5267
+ retrying,
5268
+ onRetryOrder: () => void handleRetryOrder(),
5269
+ onSetUpRecovery: openRecoveryModal,
5270
+ recoveryMissing: recovery === null ? null : recoveryLocked,
5271
+ pollTrouble,
5272
+ checkingPayment: paymentCheckUntil !== null,
5273
+ onCheckPayment: () => void handleCheckPayment(),
5274
+ onResumeOrder: (id) => void handleResumeOrder(id),
5275
+ onNewOrder: handleNewOrder,
5276
+ onBack: handleBack,
5277
+ onStepSelect: handleStepSelect,
5278
+ onRetry: () => {
5279
+ setErrorMsg(null);
5280
+ setStep("input");
5281
+ },
5282
+ onRefresh: () => void handleRefresh(),
4993
5283
  onClose
4994
5284
  }
4995
5285
  ) });
@@ -5089,6 +5379,8 @@ function ReceiveModalTemplate({
5089
5379
  onSelectChain,
5090
5380
  copied,
5091
5381
  notReadyReason,
5382
+ recoveryLocked,
5383
+ onSetupRecovery,
5092
5384
  onCopy,
5093
5385
  onClose
5094
5386
  }) {
@@ -5107,7 +5399,10 @@ function ReceiveModalTemplate({
5107
5399
  /* @__PURE__ */ jsx("div", { className: "pollar-modal-header-actions", children: /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-modal-close", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": true, children: /* @__PURE__ */ jsx("path", { d: "M2 2l12 12M14 2L2 14", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) }) }) })
5108
5400
  ] }),
5109
5401
  chains && onSelectChain && /* @__PURE__ */ jsx(ChainSelect, { value: selectedChain, options: chains, onChange: onSelectChain }),
5110
- walletAddress ? /* @__PURE__ */ jsxs(Fragment, { children: [
5402
+ recoveryLocked ? /* @__PURE__ */ jsxs("div", { className: "pollar-receive-locked", role: "alert", children: [
5403
+ /* @__PURE__ */ jsx("p", { children: "Set up a way to recover this wallet before receiving funds. Without one, losing this device means losing whatever you receive, and nobody can get it back." }),
5404
+ onSetupRecovery && /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-receive-copy-btn", onClick: onSetupRecovery, children: "Set up recovery" })
5405
+ ] }) : walletAddress ? /* @__PURE__ */ jsxs(Fragment, { children: [
5111
5406
  /* @__PURE__ */ jsxs("div", { className: "pollar-receive-qr", children: [
5112
5407
  /* @__PURE__ */ jsxs("div", { className: "pollar-receive-network-badge", children: [
5113
5408
  /* @__PURE__ */ jsx("span", { className: "pollar-receive-network-dot", "aria-hidden": true }),
@@ -5181,7 +5476,7 @@ function ReceiveModalTemplate({
5181
5476
  );
5182
5477
  }
5183
5478
  function ReceiveModal({ onClose }) {
5184
- const { wallet, wallets, styles } = usePollar();
5479
+ const { wallet, wallets, styles, recoveryLocked, openRecoveryModal } = usePollar();
5185
5480
  const { theme, accentColor, styleOverrides, overlayStyle } = modalChrome(styles);
5186
5481
  const [copied, setCopied] = useState(false);
5187
5482
  const copyTimerRef = useRef(null);
@@ -5223,6 +5518,11 @@ function ReceiveModal({ onClose }) {
5223
5518
  setSelectedChain(chain);
5224
5519
  },
5225
5520
  notReadyReason,
5521
+ recoveryLocked,
5522
+ onSetupRecovery: () => {
5523
+ onClose();
5524
+ openRecoveryModal();
5525
+ },
5226
5526
  copied,
5227
5527
  onCopy: handleCopy,
5228
5528
  onClose
@@ -5438,7 +5738,7 @@ function formatBalance(balance) {
5438
5738
  const n = parseFloat(balance);
5439
5739
  return isNaN(n) ? balance : n.toLocaleString(void 0, { maximumFractionDigits: 7 });
5440
5740
  }
5441
- function assetKey3(record) {
5741
+ function assetKey4(record) {
5442
5742
  return `${record.code}:${record.issuer ?? "native"}`;
5443
5743
  }
5444
5744
  function chainLabel(chain) {
@@ -5509,8 +5809,8 @@ function SendAssetSelector({ assets, selectedAsset, loading, onSelectAsset }) {
5509
5809
  }
5510
5810
  ),
5511
5811
  open && assets.length > 0 && /* @__PURE__ */ jsx("div", { className: "pollar-send-asset-menu", role: "listbox", "aria-label": "Assets", children: assets.map((asset) => {
5512
- const key = assetKey3(asset);
5513
- const selected = selectedAsset ? key === assetKey3(selectedAsset) : false;
5812
+ const key = assetKey4(asset);
5813
+ const selected = selectedAsset ? key === assetKey4(selectedAsset) : false;
5514
5814
  return /* @__PURE__ */ jsxs(
5515
5815
  "button",
5516
5816
  {
@@ -5554,6 +5854,9 @@ function SendModalTemplate({
5554
5854
  destination,
5555
5855
  formError,
5556
5856
  isLoadingBalance,
5857
+ isCheckingDestination = false,
5858
+ minStartingBalance = 1,
5859
+ onCreateAccount,
5557
5860
  transaction,
5558
5861
  showXdr,
5559
5862
  copied,
@@ -5583,8 +5886,10 @@ function SendModalTemplate({
5583
5886
  const cssVars = buildModalCssVars(theme, accentColor, styleOverrides);
5584
5887
  const parsedAmount = amount ? Number(amount) : 0;
5585
5888
  const availableAmount = selectedAsset?.available === null || selectedAsset?.available === void 0 ? 0 : Number(selectedAsset.available);
5586
- const canSubmit = canSendOnChain && !notReadyReason && !!selectedAsset && Number.isFinite(parsedAmount) && parsedAmount > 0 && parsedAmount <= availableAmount && !!destination.trim() && !isLoadingBalance;
5587
- const title = step === "form" ? "Send" : txTitle;
5889
+ const canSubmit = canSendOnChain && !notReadyReason && !!selectedAsset && Number.isFinite(parsedAmount) && parsedAmount > 0 && parsedAmount <= availableAmount && !!destination.trim() && !isLoadingBalance && !isCheckingDestination;
5890
+ const title = step === "form" ? "Send" : step === "create-account" ? "Create account?" : txTitle;
5891
+ const createsWithNative = selectedAsset?.type === "native";
5892
+ const meetsMinimum = Number.isFinite(parsedAmount) && parsedAmount >= minStartingBalance;
5588
5893
  return /* @__PURE__ */ jsxs(
5589
5894
  "div",
5590
5895
  {
@@ -5634,51 +5939,25 @@ function SendModalTemplate({
5634
5939
  onSelectAsset
5635
5940
  }
5636
5941
  ),
5942
+ /* @__PURE__ */ jsx(
5943
+ AmountField,
5944
+ {
5945
+ id: "pollar-send-amount",
5946
+ label: "Amount",
5947
+ value: amount,
5948
+ onChange: onAmountChange,
5949
+ unit: selectedAsset?.code,
5950
+ available: selectedAsset ? selectedAsset.available : void 0,
5951
+ onMax
5952
+ }
5953
+ ),
5637
5954
  /* @__PURE__ */ jsxs("div", { className: "pollar-send-field", children: [
5638
- /* @__PURE__ */ jsxs("div", { className: "pollar-send-label-row", children: [
5639
- /* @__PURE__ */ jsx("label", { className: "pollar-send-label", children: "Amount" }),
5640
- selectedAsset && /* @__PURE__ */ jsxs("span", { className: "pollar-send-hint", children: [
5641
- "Available: ",
5642
- formatBalance(selectedAsset.available),
5643
- " ",
5644
- selectedAsset.code
5645
- ] })
5646
- ] }),
5955
+ /* @__PURE__ */ jsx("label", { className: "pollar-send-label", children: "Destination wallet" }),
5647
5956
  /* @__PURE__ */ jsxs("div", { className: "pollar-send-input-shell", children: [
5648
5957
  /* @__PURE__ */ jsx(
5649
5958
  "input",
5650
5959
  {
5651
- className: "pollar-input pollar-send-amount-input",
5652
- type: "text",
5653
- inputMode: "decimal",
5654
- placeholder: "0.00",
5655
- value: amount,
5656
- onChange: (e) => onAmountChange(e.target.value),
5657
- "aria-label": "Amount"
5658
- }
5659
- ),
5660
- /* @__PURE__ */ jsxs("div", { className: "pollar-send-input-suffix", children: [
5661
- /* @__PURE__ */ jsx(
5662
- "button",
5663
- {
5664
- type: "button",
5665
- className: "pollar-send-inline-action",
5666
- onClick: onMax,
5667
- disabled: !selectedAsset?.available,
5668
- children: "Max"
5669
- }
5670
- ),
5671
- /* @__PURE__ */ jsx("span", { children: selectedAsset?.code ?? "" })
5672
- ] })
5673
- ] })
5674
- ] }),
5675
- /* @__PURE__ */ jsxs("div", { className: "pollar-send-field", children: [
5676
- /* @__PURE__ */ jsx("label", { className: "pollar-send-label", children: "Destination wallet" }),
5677
- /* @__PURE__ */ jsxs("div", { className: "pollar-send-input-shell", children: [
5678
- /* @__PURE__ */ jsx(
5679
- "input",
5680
- {
5681
- className: "pollar-input pollar-send-destination-input",
5960
+ className: "pollar-input pollar-send-destination-input",
5682
5961
  type: "text",
5683
5962
  placeholder: destinationPlaceholder(selectedChain),
5684
5963
  value: destination,
@@ -5723,7 +6002,41 @@ function SendModalTemplate({
5723
6002
  ] })
5724
6003
  ] }),
5725
6004
  formError && /* @__PURE__ */ jsx("div", { className: "pollar-modal-error", children: formError }),
5726
- /* @__PURE__ */ jsx("div", { className: "pollar-modal-actions", children: /* @__PURE__ */ jsx("button", { className: "pollar-btn-primary", onClick: onSubmit, disabled: !canSubmit, children: "Continue" }) })
6005
+ /* @__PURE__ */ jsx("div", { className: "pollar-modal-actions", children: /* @__PURE__ */ jsx("button", { className: "pollar-btn-primary", onClick: onSubmit, disabled: !canSubmit, children: isCheckingDestination ? "Checking destination\u2026" : "Continue" }) })
6006
+ ] }),
6007
+ step === "create-account" && /* @__PURE__ */ jsxs(Fragment, { children: [
6008
+ /* @__PURE__ */ jsxs("div", { className: "pollar-send-notice", children: [
6009
+ /* @__PURE__ */ jsx("p", { className: "pollar-send-notice-title", children: "This account doesn't exist yet" }),
6010
+ /* @__PURE__ */ jsx("p", { className: "pollar-send-notice-address", children: destination.trim() }),
6011
+ createsWithNative ? /* @__PURE__ */ jsxs("p", { children: [
6012
+ "A Stellar account has to be created and funded before it can receive payments. Instead of a payment, this will",
6013
+ " ",
6014
+ /* @__PURE__ */ jsx("strong", { children: "create the account" }),
6015
+ " with ",
6016
+ amount,
6017
+ " XLM as its starting balance."
6018
+ ] }) : /* @__PURE__ */ jsxs("p", { children: [
6019
+ "It can't receive ",
6020
+ selectedAsset?.code ?? "this asset",
6021
+ " yet: it has to be created and funded with XLM first, and then add a trustline for ",
6022
+ selectedAsset?.code ?? "the asset",
6023
+ "."
6024
+ ] }),
6025
+ createsWithNative && !meetsMinimum && /* @__PURE__ */ jsxs("p", { className: "pollar-send-notice-warning", children: [
6026
+ "A new account needs a starting balance of at least ",
6027
+ minStartingBalance,
6028
+ " XLM. Go back and raise the amount to create it."
6029
+ ] }),
6030
+ /* @__PURE__ */ jsx("p", { children: "Or wait until another wallet funds it, and send afterwards." })
6031
+ ] }),
6032
+ /* @__PURE__ */ jsxs("div", { className: "pollar-send-account-actions", children: [
6033
+ createsWithNative && onCreateAccount && /* @__PURE__ */ jsxs("button", { className: "pollar-btn-primary", onClick: onCreateAccount, disabled: !meetsMinimum, children: [
6034
+ "Create account with ",
6035
+ amount,
6036
+ " XLM"
6037
+ ] }),
6038
+ /* @__PURE__ */ jsx("button", { className: "pollar-btn-secondary", onClick: onBack, children: "I'll wait until it's funded" })
6039
+ ] })
5727
6040
  ] }),
5728
6041
  step === "tx" && /* @__PURE__ */ jsx(
5729
6042
  TxStatusView,
@@ -5746,6 +6059,11 @@ function SendModalTemplate({
5746
6059
  );
5747
6060
  }
5748
6061
  var MAX_MEMO_ID = 18446744073709551615n;
6062
+ var SOLANA_FEE_MARGIN_LAMPORTS = 10000n;
6063
+ function solanaMaxAfterFee(available) {
6064
+ const lamports = BigInt(toBaseUnits(available, 9)) - SOLANA_FEE_MARGIN_LAMPORTS;
6065
+ return fromBaseUnits((lamports > 0n ? lamports : 0n).toString(), 9);
6066
+ }
5749
6067
  function parseMemo(type, raw) {
5750
6068
  const value = type === "id" ? raw.trim() : raw;
5751
6069
  if (value.trim() === "") return void 0;
@@ -5755,6 +6073,7 @@ function parseMemo(type, raw) {
5755
6073
  if (!/^\d+$/.test(value) || BigInt(value) > MAX_MEMO_ID) return "The memo ID must be a whole number up to 2^64 - 1.";
5756
6074
  return { type, value };
5757
6075
  }
6076
+ var MIN_STARTING_BALANCE_XLM = 1;
5758
6077
  function assetParam(record) {
5759
6078
  if (record.type === "native") return { type: "native" };
5760
6079
  if (record.type === "credit_alphanum4") {
@@ -5772,11 +6091,14 @@ function SendModal({ onClose }) {
5772
6091
  tx: transaction,
5773
6092
  wallet,
5774
6093
  network,
5775
- styles
6094
+ styles,
6095
+ getClient
5776
6096
  } = usePollar();
5777
6097
  const walletType = wallet?.custody === "external" ? wallet.provider : null;
5778
6098
  const { theme, accentColor, styleOverrides, overlayStyle } = modalChrome(styles);
5779
6099
  const [step, setStep] = useState("form");
6100
+ const [createsAccount, setCreatesAccount] = useState(false);
6101
+ const [checkingDestination, setCheckingDestination] = useState(false);
5780
6102
  const [amount, setAmount] = useState("");
5781
6103
  const [destination, setDestination] = useState("");
5782
6104
  const [memo, setMemo] = useState("");
@@ -5832,8 +6154,15 @@ function SendModal({ onClose }) {
5832
6154
  "building-signing-submitting"
5833
6155
  ];
5834
6156
  const isInProgress = IN_FLIGHT_STEPS3.includes(transaction.step);
5835
- const showBack = step === "tx" && (transaction.step === "error" || transaction.step === "success") && !isInProgress;
5836
- const txTitle = isInProgress ? "Sending\u2026" : transaction.step === "success" ? "Sent!" : transaction.step === "error" ? "Send failed" : "Confirm Send";
6157
+ const showBack = step === "create-account" || step === "tx" && (transaction.step === "error" || transaction.step === "success") && !isInProgress;
6158
+ const txTitle = createsAccount ? isInProgress ? "Creating account\u2026" : transaction.step === "success" ? "Account created!" : transaction.step === "error" ? "Create account failed" : "Confirm Create Account" : isInProgress ? "Sending\u2026" : transaction.step === "success" ? "Sent!" : transaction.step === "error" ? "Send failed" : "Confirm Send";
6159
+ async function stellarAccountExists(address) {
6160
+ try {
6161
+ return (await getClient().getWalletBalance(address)).exists;
6162
+ } catch {
6163
+ return null;
6164
+ }
6165
+ }
5837
6166
  async function handleSubmit() {
5838
6167
  setFormError("");
5839
6168
  if (!canSendOnChain) {
@@ -5880,6 +6209,16 @@ function SendModal({ onClose }) {
5880
6209
  return;
5881
6210
  }
5882
6211
  }
6212
+ if (selectedChain === "STELLAR") {
6213
+ setCheckingDestination(true);
6214
+ const exists = await stellarAccountExists(destination.trim());
6215
+ setCheckingDestination(false);
6216
+ if (exists === false) {
6217
+ setStep("create-account");
6218
+ return;
6219
+ }
6220
+ }
6221
+ setCreatesAccount(false);
5883
6222
  setStep("tx");
5884
6223
  if (selectedChain === "SOLANA" || selectedChain === "POLYGON") {
5885
6224
  await sendPayment({
@@ -5898,6 +6237,17 @@ function SendModal({ onClose }) {
5898
6237
  parsedMemo ? { memo: parsedMemo } : void 0
5899
6238
  );
5900
6239
  }
6240
+ async function handleCreateAccount() {
6241
+ const parsedMemo = parseMemo(memoType, memo);
6242
+ if (typeof parsedMemo === "string" || parseFloat(amount) < MIN_STARTING_BALANCE_XLM) return;
6243
+ setCreatesAccount(true);
6244
+ setStep("tx");
6245
+ await buildTx(
6246
+ "create_account",
6247
+ { destination: destination.trim(), startingBalance: amount },
6248
+ parsedMemo ? { memo: parsedMemo } : void 0
6249
+ );
6250
+ }
5901
6251
  function handleSignAndSend() {
5902
6252
  if (transaction.step === "built") {
5903
6253
  void signAndSubmitTx(transaction.buildData.unsignedXdr);
@@ -5905,7 +6255,15 @@ function SendModal({ onClose }) {
5905
6255
  }
5906
6256
  function handleMax() {
5907
6257
  if (selectedAsset?.available !== null && selectedAsset?.available !== void 0) {
5908
- setAmount(selectedAsset.available);
6258
+ const paysOwnSolFee = selectedChain === "SOLANA" && selectedAsset.type === "native" && selectedAsset.sponsored !== true;
6259
+ let max = selectedAsset.available;
6260
+ if (paysOwnSolFee) {
6261
+ try {
6262
+ max = solanaMaxAfterFee(selectedAsset.available);
6263
+ } catch {
6264
+ }
6265
+ }
6266
+ setAmount(max);
5909
6267
  setFormError("");
5910
6268
  }
5911
6269
  }
@@ -5944,6 +6302,7 @@ function SendModal({ onClose }) {
5944
6302
  }
5945
6303
  function handleBack() {
5946
6304
  setStep("form");
6305
+ setCreatesAccount(false);
5947
6306
  setShowXdr(false);
5948
6307
  setCopied(false);
5949
6308
  }
@@ -5966,6 +6325,9 @@ function SendModal({ onClose }) {
5966
6325
  destination,
5967
6326
  formError,
5968
6327
  isLoadingBalance: walletBalance.step === "loading",
6328
+ isCheckingDestination: checkingDestination,
6329
+ minStartingBalance: MIN_STARTING_BALANCE_XLM,
6330
+ onCreateAccount: () => void handleCreateAccount(),
5969
6331
  transaction,
5970
6332
  showXdr,
5971
6333
  copied,
@@ -6253,7 +6615,7 @@ function SwapModalTemplate({
6253
6615
  ] })
6254
6616
  ] }),
6255
6617
  /* @__PURE__ */ jsxs("div", { className: "pollar-swap-panel-main", children: [
6256
- /* @__PURE__ */ jsx("div", { className: `pollar-swap-output${quoteLoading ? " is-loading" : ""}`, "aria-live": "polite", children: quoteLoading ? /* @__PURE__ */ jsx("span", { className: "pollar-spinner pollar-spinner-sm" }) : formatAmount2(quote?.amountOut) }),
6618
+ /* @__PURE__ */ jsx("div", { className: `pollar-swap-output${quoteLoading ? " is-loading" : ""}`, "aria-live": "polite", children: quoteLoading ? /* @__PURE__ */ jsx("span", { className: "pollar-spinner pollar-spinner-sm" }) : quote ? `~${formatAmount2(quote.amountOut)}` : "\u2014" }),
6257
6619
  /* @__PURE__ */ jsxs("div", { className: "pollar-swap-panel-selector", children: [
6258
6620
  /* @__PURE__ */ jsx(
6259
6621
  AssetPill,
@@ -6717,7 +7079,6 @@ function EarnModal({ onClose }) {
6717
7079
  earnWithdraw,
6718
7080
  tx: transaction,
6719
7081
  wallet,
6720
- walletBalance,
6721
7082
  refreshWalletBalance,
6722
7083
  enabledAssets,
6723
7084
  refreshAssets,
@@ -6799,13 +7160,9 @@ function EarnModal({ onClose }) {
6799
7160
  const withdrawUnit = position?.withdrawUnit ?? "asset";
6800
7161
  const assetCode = selectedOpportunity?.asset.code ?? "";
6801
7162
  const amountUnitLabel = tab === "deposit" ? assetCode : withdrawUnit === "shares" ? "shares" : assetCode;
6802
- const walletAvailable = (() => {
6803
- if (!selectedOpportunity) return null;
6804
- const balances = walletBalance.step === "loaded" ? walletBalance.data.balances : [];
6805
- const issuer = selectedOpportunity.asset.issuer;
6806
- const rec = balances.find((b) => issuer === null ? b.type === "native" : b.code === assetCode && b.issuer === issuer);
6807
- return rec ? rec.available : null;
6808
- })();
7163
+ const walletAvailable = useWalletAvailable(
7164
+ selectedOpportunity ? { chain: "STELLAR", code: assetCode, issuer: selectedOpportunity.asset.issuer } : null
7165
+ );
6809
7166
  const depositAssetRecord = (() => {
6810
7167
  if (!selectedOpportunity?.asset.issuer || enabledAssets.step !== "loaded") return void 0;
6811
7168
  return enabledAssets.data.assets.find(
@@ -7039,37 +7396,17 @@ function EarnModal({ onClose }) {
7039
7396
  ] })
7040
7397
  ] })
7041
7398
  ] }),
7042
- /* @__PURE__ */ jsxs("div", { className: "pollar-send-field", children: [
7043
- /* @__PURE__ */ jsxs("div", { className: "pollar-send-label-row", children: [
7044
- /* @__PURE__ */ jsx("label", { className: "pollar-send-label", children: tab === "deposit" ? "Amount" : "Withdraw amount" }),
7045
- tab === "deposit" && walletAvailable != null && /* @__PURE__ */ jsxs("button", { type: "button", className: "pollar-swap-custom-toggle", onClick: () => setAmount(walletAvailable), children: [
7046
- "Max: ",
7047
- formatAmount3(walletAvailable),
7048
- " ",
7049
- amountUnitLabel
7050
- ] }),
7051
- tab === "withdraw" && position && /* @__PURE__ */ jsxs("button", { type: "button", className: "pollar-swap-custom-toggle", onClick: () => setAmount(position.withdrawable), children: [
7052
- "Max: ",
7053
- formatAmount3(position.withdrawable),
7054
- " ",
7055
- amountUnitLabel
7056
- ] })
7057
- ] }),
7058
- /* @__PURE__ */ jsxs("div", { className: "pollar-earn-amount", children: [
7059
- /* @__PURE__ */ jsx(
7060
- "input",
7061
- {
7062
- className: "pollar-input",
7063
- type: "text",
7064
- inputMode: "decimal",
7065
- placeholder: "0.00",
7066
- value: amount,
7067
- onChange: (e) => setAmount(e.target.value)
7068
- }
7069
- ),
7070
- amountUnitLabel && /* @__PURE__ */ jsx("span", { className: "pollar-earn-amount-unit", children: amountUnitLabel })
7071
- ] })
7072
- ] }),
7399
+ /* @__PURE__ */ jsx(
7400
+ AmountField,
7401
+ {
7402
+ id: "pollar-earn-amount",
7403
+ label: tab === "deposit" ? "Amount" : "Withdraw amount",
7404
+ value: amount,
7405
+ onChange: setAmount,
7406
+ unit: amountUnitLabel,
7407
+ available: tab === "deposit" ? walletAvailable : position ? position.withdrawable : void 0
7408
+ }
7409
+ ),
7073
7410
  formError && /* @__PURE__ */ jsx("div", { className: "pollar-modal-error", children: formError }),
7074
7411
  depositNeedsTrustline && selectedOpportunity && /* @__PURE__ */ jsxs("div", { className: "pollar-swap-trustline-notice", children: [
7075
7412
  "To deposit ",
@@ -7101,13 +7438,16 @@ function EarnModal({ onClose }) {
7101
7438
  }
7102
7439
 
7103
7440
  // src/components/card-modal/card-kyc.ts
7104
- var nonEmpty2 = (value) => typeof value === "string" && !!value.trim();
7441
+ var nonEmpty = (value) => typeof value === "string" && !!value.trim();
7105
7442
  function requiredCardStep(error) {
7106
7443
  if (!error || typeof error !== "object") return null;
7107
- const { code, body } = error;
7108
- if (code !== "SDK_CARDS_REQUIREMENT_REQUIRED" || !body || typeof body !== "object") return null;
7109
- const { cardProviderId, requirementType, optionId } = body;
7110
- if (!nonEmpty2(cardProviderId) || !nonEmpty2(optionId)) return null;
7444
+ if (!("code" in error) || error.code !== "SDK_CARDS_REQUIREMENT_REQUIRED") return null;
7445
+ const body = "body" in error ? error.body : void 0;
7446
+ if (typeof body !== "object" || body === null) return null;
7447
+ const cardProviderId = "cardProviderId" in body ? body.cardProviderId : void 0;
7448
+ const requirementType = "requirementType" in body ? body.requirementType : void 0;
7449
+ const optionId = "optionId" in body ? body.optionId : void 0;
7450
+ if (!nonEmpty(cardProviderId) || !nonEmpty(optionId)) return null;
7111
7451
  if (requirementType === "KYC" || requirementType === "FORM" || requirementType === "REGISTRY_CHECK" || requirementType === "PROVIDER_REGISTRATION") {
7112
7452
  return { cardProviderId, type: requirementType, optionId };
7113
7453
  }
@@ -7262,6 +7602,8 @@ function CardModal({ onClose }) {
7262
7602
  const [error, setError] = useState("");
7263
7603
  const revealTimer = useRef(null);
7264
7604
  const provider = providers?.[0] ?? null;
7605
+ const fundingAsset = provider?.fundingAssets[0] ?? "USDC";
7606
+ const fundAvailable = useWalletAvailable(panel === "fund" ? { chain: "STELLAR", code: fundingAsset } : null);
7265
7607
  const card = cards?.[0] ?? null;
7266
7608
  const loadHolder = useCallback(async () => {
7267
7609
  const h = await getClient().getCardHolder();
@@ -7760,15 +8102,17 @@ function CardModal({ onClose }) {
7760
8102
  transactions === null && /* @__PURE__ */ jsx("p", { className: "pollar-card-note", children: "Loading..." }),
7761
8103
  transactions?.length === 0 && /* @__PURE__ */ jsx("p", { className: "pollar-card-note", children: "No movements yet." }),
7762
8104
  transactions?.map((tx) => {
7763
- const purchase = tx.purchase;
7764
- const local = purchase?.localAmount && purchase.localCurrency && purchase.localCurrency !== "USD" ? money(String(purchase.localAmount), purchase.localCurrency) : null;
8105
+ const localAmount = typeof tx.purchase?.localAmount === "number" ? tx.purchase.localAmount : null;
8106
+ const localCurrency = typeof tx.purchase?.localCurrency === "string" ? tx.purchase.localCurrency : null;
8107
+ const purchaseStatus = typeof tx.purchase?.status === "string" ? tx.purchase.status : null;
8108
+ const local = localAmount && localCurrency && localCurrency !== "USD" ? money(String(localAmount), localCurrency) : null;
7765
8109
  return /* @__PURE__ */ jsxs("div", { className: "pollar-card-tx", children: [
7766
8110
  /* @__PURE__ */ jsxs("div", { children: [
7767
8111
  tx.merchantName ?? TX_LABELS[tx.type] ?? tx.type,
7768
8112
  /* @__PURE__ */ jsxs("span", { className: "pollar-card-tx-meta", children: [
7769
8113
  new Date(tx.occurredAt).toLocaleDateString(),
7770
8114
  tx.merchantName ? ` \xB7 ${TX_LABELS[tx.type] ?? tx.type}` : "",
7771
- purchase?.status && purchase.status !== "completed" ? ` \xB7 ${purchase.status}` : ""
8115
+ purchaseStatus && purchaseStatus !== "completed" ? ` \xB7 ${purchaseStatus}` : ""
7772
8116
  ] })
7773
8117
  ] }),
7774
8118
  /* @__PURE__ */ jsxs("div", { className: "pollar-card-tx-amount", "data-direction": tx.direction ?? void 0, children: [
@@ -7785,19 +8129,18 @@ function CardModal({ onClose }) {
7785
8129
  provider.fundingAssets.join(" or ") || "USDC",
7786
8130
  " from your wallet to your card. It takes a few minutes to cross to the card network."
7787
8131
  ] }),
7788
- /* @__PURE__ */ jsxs("label", { className: "pollar-send-field", children: [
7789
- /* @__PURE__ */ jsx("span", { className: "pollar-send-label", children: "Amount (USDC)" }),
7790
- /* @__PURE__ */ jsx(
7791
- "input",
7792
- {
7793
- className: "pollar-input",
7794
- inputMode: "decimal",
7795
- placeholder: "10",
7796
- value: fundAmount,
7797
- onChange: (e) => setFundAmount(e.target.value.replace(/[^0-9.]/g, ""))
7798
- }
7799
- )
7800
- ] }),
8132
+ /* @__PURE__ */ jsx(
8133
+ AmountField,
8134
+ {
8135
+ id: "pollar-card-fund-amount",
8136
+ label: "Amount",
8137
+ value: fundAmount,
8138
+ onChange: (value) => setFundAmount(value.replace(/[^0-9.]/g, "")),
8139
+ unit: fundingAsset,
8140
+ available: fundAvailable,
8141
+ placeholder: "10"
8142
+ }
8143
+ ),
7801
8144
  /* @__PURE__ */ jsx("div", { className: "pollar-modal-actions", children: /* @__PURE__ */ jsx(
7802
8145
  "button",
7803
8146
  {
@@ -8167,7 +8510,8 @@ function useCosign() {
8167
8510
  revokeDevice: useCallback(
8168
8511
  (deviceId, options) => getClient().cosign.revokeDevice(deviceId, options),
8169
8512
  [getClient]
8170
- )
8513
+ ),
8514
+ switchCustody: useCallback((to) => getClient().cosign.switchCustody(to), [getClient])
8171
8515
  };
8172
8516
  }
8173
8517
  var OPERATION_LABELS = {
@@ -8189,6 +8533,15 @@ function expiresIn(request) {
8189
8533
  const m = Math.floor(ms % 36e5 / 6e4);
8190
8534
  return h > 0 ? `${h}h ${m}m` : `${m}m`;
8191
8535
  }
8536
+ function readableAmount(details) {
8537
+ const { amount, decimals, symbol } = details;
8538
+ if (typeof amount !== "string" || typeof decimals !== "number" || typeof symbol !== "string") return null;
8539
+ try {
8540
+ return { amount: fromBaseUnits(amount, decimals), symbol };
8541
+ } catch {
8542
+ return null;
8543
+ }
8544
+ }
8192
8545
  function detailRows(request) {
8193
8546
  const details = request.details ?? {};
8194
8547
  const rows = [];
@@ -8200,14 +8553,21 @@ function detailRows(request) {
8200
8553
  ]);
8201
8554
  };
8202
8555
  push("Destination", details.destination);
8203
- push("Amount", details.amount);
8204
- push("Asset", details.asset ?? details.assetCode);
8556
+ const readable = readableAmount(details);
8557
+ push("Amount", readable ? `${readable.amount} ${readable.symbol}` : details.amount);
8558
+ push("Asset", details.asset ?? details.assetCode ?? readable?.symbol);
8205
8559
  push("Network", request.network);
8206
8560
  return rows;
8207
8561
  }
8208
8562
  function CloseIcon2() {
8209
8563
  return /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", "aria-hidden": true, children: /* @__PURE__ */ jsx("path", { d: "m3 3 8 8M11 3l-8 8", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round" }) });
8210
8564
  }
8565
+ function VerifiedIcon() {
8566
+ return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", "aria-hidden": true, children: [
8567
+ /* @__PURE__ */ jsx("circle", { cx: "7", cy: "7", r: "7", fill: "currentColor" }),
8568
+ /* @__PURE__ */ jsx("path", { d: "M3.5 7l2.5 2.5 4.5-5", stroke: "white", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round" })
8569
+ ] });
8570
+ }
8211
8571
  function CosignModalTemplate({
8212
8572
  theme,
8213
8573
  accentColor,
@@ -8220,6 +8580,8 @@ function CosignModalTemplate({
8220
8580
  approveBlocked = false,
8221
8581
  lastResolved,
8222
8582
  explorerUrl,
8583
+ closesIn = null,
8584
+ nextStep = null,
8223
8585
  onSelect,
8224
8586
  onApprove,
8225
8587
  onReject,
@@ -8228,6 +8590,8 @@ function CosignModalTemplate({
8228
8590
  const cssVars = buildModalCssVars(theme, accentColor, styleOverrides);
8229
8591
  const verified = verification?.ok === true;
8230
8592
  const canVote = !!current && verified && !busy && !approveBlocked;
8593
+ const [verifiedOpenFor, setVerifiedOpenFor] = useState(null);
8594
+ const verifiedOpen = !!current && verifiedOpenFor === current.id;
8231
8595
  return /* @__PURE__ */ jsxs(
8232
8596
  "div",
8233
8597
  {
@@ -8240,27 +8604,48 @@ function CosignModalTemplate({
8240
8604
  /* @__PURE__ */ jsx("h2", { className: "pollar-modal-title", children: "Confirm with your device" }),
8241
8605
  /* @__PURE__ */ jsx("div", { className: "pollar-modal-header-actions", children: /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-modal-close", onClick: onClose, "aria-label": "Close", title: "Close", children: /* @__PURE__ */ jsx(CloseIcon2, {}) }) })
8242
8606
  ] }),
8243
- pending.length === 0 && /* @__PURE__ */ jsx("div", { className: "pollar-cosign-empty", children: lastResolved ? /* @__PURE__ */ jsxs(Fragment, { children: [
8244
- lastResolved.status === "APPROVED" && /* @__PURE__ */ jsxs("svg", { className: "pollar-cosign-approved-icon", width: "44", height: "44", viewBox: "0 0 14 14", fill: "none", "aria-hidden": true, children: [
8245
- /* @__PURE__ */ jsx("circle", { cx: "7", cy: "7", r: "7", fill: "currentColor" }),
8246
- /* @__PURE__ */ jsx(
8247
- "path",
8248
- {
8249
- d: "M3.5 7l2.5 2.5 4.5-5",
8250
- stroke: "white",
8251
- strokeWidth: "1.2",
8252
- strokeLinecap: "round",
8253
- strokeLinejoin: "round"
8254
- }
8255
- )
8607
+ pending.length === 0 && /* @__PURE__ */ jsxs("div", { className: "pollar-cosign-empty", children: [
8608
+ lastResolved ? /* @__PURE__ */ jsxs(Fragment, { children: [
8609
+ lastResolved.status === "APPROVED" && /* @__PURE__ */ jsxs("svg", { className: "pollar-cosign-approved-icon", width: "44", height: "44", viewBox: "0 0 14 14", fill: "none", "aria-hidden": true, children: [
8610
+ /* @__PURE__ */ jsx("circle", { cx: "7", cy: "7", r: "7", fill: "currentColor" }),
8611
+ /* @__PURE__ */ jsx(
8612
+ "path",
8613
+ {
8614
+ d: "M3.5 7l2.5 2.5 4.5-5",
8615
+ stroke: "white",
8616
+ strokeWidth: "1.2",
8617
+ strokeLinecap: "round",
8618
+ strokeLinejoin: "round"
8619
+ }
8620
+ )
8621
+ ] }),
8622
+ /* @__PURE__ */ jsx("p", { className: "pollar-cosign-empty-title", children: lastResolved.status === "APPROVED" ? "Approved" : lastResolved.status === "REJECTED" ? "Rejected" : lastResolved.status.toLowerCase() }),
8623
+ /* @__PURE__ */ jsx("p", { className: "pollar-cosign-muted", children: lastResolved.submit?.hash ? /* @__PURE__ */ jsxs(Fragment, { children: [
8624
+ "Sent. Transaction",
8625
+ " ",
8626
+ explorerUrl ? /* @__PURE__ */ jsx("a", { className: "pollar-cosign-tx-link", href: explorerUrl, target: "_blank", rel: "noopener noreferrer", children: cropAddress(lastResolved.submit.hash) }) : cropAddress(lastResolved.submit.hash),
8627
+ /* @__PURE__ */ jsx(
8628
+ CopyButton,
8629
+ {
8630
+ value: lastResolved.submit.hash,
8631
+ label: "Copy transaction hash",
8632
+ className: "pollar-copy-btn-sm pollar-cosign-copy"
8633
+ }
8634
+ )
8635
+ ] }) : lastResolved.signedXdr ? "Signed. The app will send it." : "Nothing was signed." })
8636
+ ] }) : /* @__PURE__ */ jsx("p", { className: "pollar-cosign-muted", children: "Nothing waits for your approval." }),
8637
+ nextStep && /* @__PURE__ */ jsxs("p", { className: "pollar-cosign-next", children: [
8638
+ /* @__PURE__ */ jsx("span", { className: "pollar-spinner pollar-spinner-sm", "aria-hidden": true }),
8639
+ " Next: ",
8640
+ nextStep,
8641
+ ". It appears here in a few seconds."
8256
8642
  ] }),
8257
- /* @__PURE__ */ jsx("p", { className: "pollar-cosign-empty-title", children: lastResolved.status === "APPROVED" ? "Approved" : lastResolved.status === "REJECTED" ? "Rejected" : lastResolved.status.toLowerCase() }),
8258
- /* @__PURE__ */ jsx("p", { className: "pollar-cosign-muted", children: lastResolved.submit?.hash ? /* @__PURE__ */ jsxs(Fragment, { children: [
8259
- "Sent. Transaction",
8260
- " ",
8261
- explorerUrl ? /* @__PURE__ */ jsx("a", { className: "pollar-cosign-tx-link", href: explorerUrl, target: "_blank", rel: "noopener noreferrer", children: cropAddress(lastResolved.submit.hash) }) : cropAddress(lastResolved.submit.hash)
8262
- ] }) : lastResolved.signedXdr ? "Signed. The app will send it." : "Nothing was signed." })
8263
- ] }) : /* @__PURE__ */ jsx("p", { className: "pollar-cosign-muted", children: "Nothing waits for your approval." }) }),
8643
+ closesIn !== null && /* @__PURE__ */ jsxs("p", { className: "pollar-cosign-muted pollar-cosign-closing", children: [
8644
+ "Closing in ",
8645
+ closesIn,
8646
+ "s"
8647
+ ] })
8648
+ ] }),
8264
8649
  pending.length > 1 && /* @__PURE__ */ jsx("ul", { className: "pollar-cosign-list", children: pending.map((r) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
8265
8650
  "button",
8266
8651
  {
@@ -8274,9 +8659,27 @@ function CosignModalTemplate({
8274
8659
  }
8275
8660
  ) }, r.id)) }),
8276
8661
  current && /* @__PURE__ */ jsxs("div", { className: "pollar-cosign-detail", children: [
8277
- /* @__PURE__ */ jsx("p", { className: "pollar-cosign-detail-title", children: labelOf(current) }),
8662
+ /* @__PURE__ */ jsxs("div", { className: "pollar-cosign-detail-head", children: [
8663
+ /* @__PURE__ */ jsx("p", { className: "pollar-cosign-detail-title", children: labelOf(current) }),
8664
+ !verification && /* @__PURE__ */ jsxs("span", { className: "pollar-cosign-chip", children: [
8665
+ /* @__PURE__ */ jsx("span", { className: "pollar-spinner pollar-spinner-sm", "aria-hidden": true }),
8666
+ "Checking..."
8667
+ ] }),
8668
+ verification?.ok && /* @__PURE__ */ jsxs(
8669
+ "button",
8670
+ {
8671
+ type: "button",
8672
+ className: "pollar-cosign-chip pollar-cosign-chip-ok",
8673
+ "aria-expanded": verifiedOpen,
8674
+ onClick: () => setVerifiedOpenFor(verifiedOpen ? null : current.id),
8675
+ children: [
8676
+ /* @__PURE__ */ jsx(VerifiedIcon, {}),
8677
+ "Verified"
8678
+ ]
8679
+ }
8680
+ )
8681
+ ] }),
8278
8682
  current.summary && /* @__PURE__ */ jsx("p", { className: "pollar-cosign-muted", children: current.summary }),
8279
- /* @__PURE__ */ jsx(GroupSteps, { request: current }),
8280
8683
  /* @__PURE__ */ jsxs("dl", { className: "pollar-cosign-rows", children: [
8281
8684
  detailRows(current).map(([k, v]) => /* @__PURE__ */ jsxs("div", { className: "pollar-cosign-row", children: [
8282
8685
  /* @__PURE__ */ jsx("dt", { children: k }),
@@ -8287,140 +8690,637 @@ function CosignModalTemplate({
8287
8690
  /* @__PURE__ */ jsx("dd", { children: expiresIn(current) })
8288
8691
  ] })
8289
8692
  ] }),
8290
- /* @__PURE__ */ jsxs(
8291
- "div",
8292
- {
8293
- className: `pollar-cosign-verify pollar-cosign-verify-${verification ? verification.ok ? "ok" : "bad" : "pending"}`,
8294
- children: [
8295
- !verification && "Checking the transaction against Turnkey...",
8296
- verification?.ok && "Verified: this is exactly what will be signed.",
8297
- verification && !verification.ok && `Not verified: ${verification.reason}`
8298
- ]
8299
- }
8300
- ),
8693
+ verification?.ok && verifiedOpen && /* @__PURE__ */ jsx("div", { className: "pollar-cosign-verify pollar-cosign-verify-ok", children: "Checked against Turnkey: this is exactly what will be signed." }),
8694
+ verification && !verification.ok && /* @__PURE__ */ jsxs("div", { className: "pollar-cosign-verify pollar-cosign-verify-bad", children: [
8695
+ "Not verified: ",
8696
+ verification.reason
8697
+ ] }),
8301
8698
  error && /* @__PURE__ */ jsx("div", { className: "pollar-cosign-error", children: error }),
8302
8699
  /* @__PURE__ */ jsxs("div", { className: "pollar-cosign-actions", children: [
8303
8700
  /* @__PURE__ */ jsx(
8304
- "button",
8701
+ "button",
8702
+ {
8703
+ type: "button",
8704
+ className: "pollar-cosign-btn pollar-cosign-btn-secondary",
8705
+ disabled: !!busy,
8706
+ onClick: () => onReject(current),
8707
+ children: busy === "rejecting" ? "Rejecting..." : "Reject"
8708
+ }
8709
+ ),
8710
+ /* @__PURE__ */ jsx(
8711
+ "button",
8712
+ {
8713
+ type: "button",
8714
+ className: "pollar-cosign-btn pollar-cosign-btn-primary",
8715
+ disabled: !canVote,
8716
+ onClick: () => onApprove(current),
8717
+ children: busy === "approving" ? "Approving..." : "Approve"
8718
+ }
8719
+ )
8720
+ ] })
8721
+ ] }),
8722
+ /* @__PURE__ */ jsx(PollarModalFooter, {})
8723
+ ]
8724
+ }
8725
+ );
8726
+ }
8727
+ var CLOSE_AFTER_SECONDS = 3;
8728
+ var CLOSE_AFTER_SENT_SECONDS = 5;
8729
+ var NEXT_REQUEST_EVERY_MS = 2e3;
8730
+ function CosignModal({ nextStep = null, onClose }) {
8731
+ const { getClient, styles, network } = usePollar();
8732
+ const { theme, accentColor, styleOverrides, overlayStyle } = modalChrome(styles);
8733
+ const { pending, lastResolved, loaded, refresh, approve, reject, verify, inspectQuorum } = useCosign();
8734
+ const [selectedId, setSelectedId] = useState(null);
8735
+ const [verification, setVerification] = useState(null);
8736
+ const [busy, setBusy] = useState(null);
8737
+ const [error, setError] = useState(null);
8738
+ const [approveFailedFor, setApproveFailedFor] = useState(null);
8739
+ const [quorumFailure, setQuorumFailure] = useState(null);
8740
+ useEffect(() => {
8741
+ let cancelled = false;
8742
+ inspectQuorum().then((q) => {
8743
+ if (!cancelled && !q.ok) setQuorumFailure(q.reason);
8744
+ }).catch(() => void 0);
8745
+ return () => {
8746
+ cancelled = true;
8747
+ };
8748
+ }, [inspectQuorum]);
8749
+ const current = pending.find((r) => r.id === selectedId) ?? pending[0] ?? null;
8750
+ const currentRef = useRef(current);
8751
+ currentRef.current = current;
8752
+ const currentId = current?.id ?? null;
8753
+ useEffect(() => {
8754
+ const request = currentRef.current;
8755
+ if (!request) return;
8756
+ setVerification(null);
8757
+ setError(null);
8758
+ setApproveFailedFor(null);
8759
+ let cancelled = false;
8760
+ verify(request).then((v) => {
8761
+ if (!cancelled) setVerification(v);
8762
+ }).catch((err) => {
8763
+ if (!cancelled) setVerification({ ok: false, reason: err instanceof Error ? err.message : String(err) });
8764
+ });
8765
+ return () => {
8766
+ cancelled = true;
8767
+ };
8768
+ }, [currentId, verify]);
8769
+ const onCloseRef = useRef(onClose);
8770
+ onCloseRef.current = onClose;
8771
+ useEffect(() => {
8772
+ return getClient().onAuthStateChange((authState) => {
8773
+ if (authState.step === "idle") onCloseRef.current();
8774
+ });
8775
+ }, [getClient]);
8776
+ const silent = pending.length === 0 && (lastResolved ? lastResolved.status === "APPROVED" && !lastResolved.submit?.hash && !lastResolved.signedXdr : loaded);
8777
+ const sent = pending.length === 0 && lastResolved?.status === "APPROVED" && !!lastResolved.submit?.hash;
8778
+ const awaitingNext = sent && !!nextStep;
8779
+ const closeAfter = awaitingNext ? null : sent ? CLOSE_AFTER_SENT_SECONDS : silent ? CLOSE_AFTER_SECONDS : null;
8780
+ useEffect(() => {
8781
+ if (!awaitingNext) return;
8782
+ const timer = setInterval(() => void refresh().catch(() => void 0), NEXT_REQUEST_EVERY_MS);
8783
+ return () => clearInterval(timer);
8784
+ }, [awaitingNext, refresh]);
8785
+ const [closesIn, setClosesIn] = useState(null);
8786
+ useEffect(() => {
8787
+ if (closeAfter === null) {
8788
+ setClosesIn(null);
8789
+ return;
8790
+ }
8791
+ let left = closeAfter;
8792
+ setClosesIn(left);
8793
+ const timer = setInterval(() => {
8794
+ left -= 1;
8795
+ if (left > 0) setClosesIn(left);
8796
+ else {
8797
+ clearInterval(timer);
8798
+ onCloseRef.current();
8799
+ }
8800
+ }, 1e3);
8801
+ return () => clearInterval(timer);
8802
+ }, [closeAfter, lastResolved?.id]);
8803
+ const run = useCallback(
8804
+ async (kind, request, action) => {
8805
+ setBusy(kind);
8806
+ setError(null);
8807
+ try {
8808
+ await action(request);
8809
+ } catch (err) {
8810
+ setError(transactionErrorMessage(isPollarApiError(err) ? err.code : null, err instanceof Error ? err.message : null));
8811
+ if (kind === "approving") setApproveFailedFor(request.id);
8812
+ } finally {
8813
+ setBusy(null);
8814
+ }
8815
+ },
8816
+ []
8817
+ );
8818
+ return /* @__PURE__ */ jsx("div", { className: "pollar-overlay", style: overlayStyle, onClick: onClose, children: /* @__PURE__ */ jsx(
8819
+ CosignModalTemplate,
8820
+ {
8821
+ theme,
8822
+ accentColor,
8823
+ styleOverrides,
8824
+ pending,
8825
+ current,
8826
+ verification: current ? quorumFailure ? { ok: false, reason: quorumFailure } : verification : null,
8827
+ busy,
8828
+ error,
8829
+ approveBlocked: !!current && approveFailedFor === current.id,
8830
+ ...lastResolved ? { lastResolved } : {},
8831
+ closesIn,
8832
+ nextStep: awaitingNext ? nextStep : null,
8833
+ explorerUrl: lastResolved?.submit?.hash ? explorerTxUrl(lastResolved.network, lastResolved.submit.hash, network) : null,
8834
+ onSelect: (r) => setSelectedId(r.id),
8835
+ onApprove: (r) => void run("approving", r, approve),
8836
+ onReject: (r) => void run("rejecting", r, reject),
8837
+ onClose
8838
+ }
8839
+ ) });
8840
+ }
8841
+ var CHAIN_NAME3 = { STELLAR: "Stellar", SOLANA: "Solana", POLYGON: "Polygon" };
8842
+ var short = (address) => `${address.slice(0, 6)}...${address.slice(-4)}`;
8843
+ var SELF_CUSTODY_RISKS = "In a self-custody wallet only your device can sign. If you lose this device and every recovery method, nobody can recover the wallet or its funds, not the app and not Pollar. Nobody can undo or freeze a transaction you sign.";
8844
+ function CustodyMigrationModalTemplate(p) {
8845
+ const cssVars = buildModalCssVars(p.theme, p.accentColor, p.styleOverrides);
8846
+ const [pick, setPick] = useState(p.state.choice ? null : p.state.toCustody);
8847
+ const [risksAccepted, setRisksAccepted] = useState(false);
8848
+ const custody = p.state.choice ? pick : p.state.toCustody;
8849
+ const canMove = !!custody && (custody !== "SELF_CUSTODY" || risksAccepted) && !p.busy;
8850
+ const moving = p.state.wallets.filter((w) => w.required);
8851
+ const changing = moving.filter((w) => w.addressChanges);
8852
+ const failed = moving.some((w) => w.status === "FAILED");
8853
+ return /* @__PURE__ */ jsxs("div", { className: "pollar-modal-card pollar-recovery-modal", "data-theme": p.theme, style: cssVars, children: [
8854
+ /* @__PURE__ */ jsx("div", { className: "pollar-modal-header", children: /* @__PURE__ */ jsx("h2", { className: "pollar-modal-title", children: "Move your wallets to this device" }) }),
8855
+ /* @__PURE__ */ jsxs("div", { className: "pollar-recovery-body", children: [
8856
+ /* @__PURE__ */ jsx("p", { className: "pollar-recovery-muted", children: "Until now Pollar held the keys of your wallets. This app now keeps wallets on its users' devices, so they move here, with everything they hold." }),
8857
+ /* @__PURE__ */ jsxs("section", { className: "pollar-recovery-section", children: [
8858
+ /* @__PURE__ */ jsx("h3", { children: "What moves" }),
8859
+ moving.map((w) => /* @__PURE__ */ jsxs("div", { className: "pollar-recovery-item", children: [
8860
+ /* @__PURE__ */ jsxs("span", { children: [
8861
+ CHAIN_NAME3[w.chain] ?? w.chain,
8862
+ " \xB7 ",
8863
+ short(w.address)
8864
+ ] }),
8865
+ /* @__PURE__ */ jsx("span", { className: "pollar-recovery-tag", children: w.addressChanges ? "New address" : "Same address" })
8866
+ ] }, w.chain))
8867
+ ] }),
8868
+ changing.length > 0 && /* @__PURE__ */ jsxs("div", { className: "pollar-recovery-banner pollar-recovery-banner-warn", role: "alert", children: [
8869
+ "Your ",
8870
+ changing.map((w) => CHAIN_NAME3[w.chain] ?? w.chain).join(" and "),
8871
+ " address changes: every token and the balance go to a new address on this device. Stop sharing the old one; anything sent there later needs help from support to recover."
8872
+ ] }),
8873
+ p.state.choice && /* @__PURE__ */ jsxs("section", { className: "pollar-recovery-section", children: [
8874
+ /* @__PURE__ */ jsx("h3", { children: "Choose how your wallet signs" }),
8875
+ /* @__PURE__ */ jsxs("label", { className: "pollar-recovery-check", children: [
8876
+ /* @__PURE__ */ jsx("input", { type: "radio", name: "custody", checked: pick === "COSIGNED", onChange: () => setPick("COSIGNED") }),
8877
+ " ",
8878
+ "Co-signed: you and the app approve every signature together, and the wallet can be recovered with help."
8879
+ ] }),
8880
+ /* @__PURE__ */ jsxs("label", { className: "pollar-recovery-check", children: [
8881
+ /* @__PURE__ */ jsx("input", { type: "radio", name: "custody", checked: pick === "SELF_CUSTODY", onChange: () => setPick("SELF_CUSTODY") }),
8882
+ " ",
8883
+ "Self-custody: only your device signs. Nobody else can move or recover your funds."
8884
+ ] })
8885
+ ] }),
8886
+ custody === "SELF_CUSTODY" && /* @__PURE__ */ jsxs(Fragment, { children: [
8887
+ /* @__PURE__ */ jsx("div", { className: "pollar-recovery-banner pollar-recovery-banner-warn", role: "alert", children: SELF_CUSTODY_RISKS }),
8888
+ /* @__PURE__ */ jsxs("label", { className: "pollar-recovery-check", children: [
8889
+ /* @__PURE__ */ jsx("input", { type: "checkbox", checked: risksAccepted, onChange: (e) => setRisksAccepted(e.target.checked) }),
8890
+ " I understand and accept these risks"
8891
+ ] })
8892
+ ] }),
8893
+ failed && !p.error && /* @__PURE__ */ jsx("p", { className: "pollar-recovery-error", role: "alert", children: "The last attempt did not finish. What did not move is still where it was; try again." }),
8894
+ p.blockedReasons && /* @__PURE__ */ jsxs("div", { className: "pollar-recovery-banner pollar-recovery-banner-warn", role: "alert", children: [
8895
+ "A wallet cannot move while something still uses it: ",
8896
+ p.blockedReasons.join(", "),
8897
+ ". Let it finish and try again."
8898
+ ] }),
8899
+ /* @__PURE__ */ jsx(
8900
+ "button",
8901
+ {
8902
+ type: "button",
8903
+ className: "pollar-recovery-btn pollar-recovery-btn-primary",
8904
+ onClick: () => custody && p.onMove(p.state.choice ? custody : void 0),
8905
+ disabled: !canMove,
8906
+ children: p.busy ? "Moving your wallets..." : "Move my wallets"
8907
+ }
8908
+ ),
8909
+ p.blockedReasons && /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-recovery-btn pollar-recovery-btn-quiet", onClick: p.onLater, disabled: p.busy, children: "Continue for now" }),
8910
+ /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-recovery-btn pollar-recovery-btn-quiet", onClick: p.onLogout, disabled: p.busy, children: "Log out" }),
8911
+ p.error && /* @__PURE__ */ jsx("p", { className: "pollar-recovery-error", role: "alert", children: p.error })
8912
+ ] }),
8913
+ /* @__PURE__ */ jsx(PollarModalFooter, {})
8914
+ ] });
8915
+ }
8916
+ function CustodyMigrationModal({ state, onMoved, onLater }) {
8917
+ const { getClient, styles } = usePollar();
8918
+ const { theme, accentColor, styleOverrides, overlayStyle } = modalChrome(styles);
8919
+ const [busy, setBusy] = useState(false);
8920
+ const [error, setError] = useState(null);
8921
+ const [blockedReasons, setBlockedReasons] = useState(null);
8922
+ async function move(custody) {
8923
+ setBusy(true);
8924
+ setError(null);
8925
+ setBlockedReasons(null);
8926
+ try {
8927
+ await getClient().cosign.migrateWallets(custody);
8928
+ onMoved();
8929
+ } catch (err) {
8930
+ if (isPollarApiError(err) && err.code === "SDK_CUSTODY_MIGRATION_BLOCKED") {
8931
+ const reasons = err.body.reasons;
8932
+ setBlockedReasons(Array.isArray(reasons) ? reasons.map(String) : []);
8933
+ return;
8934
+ }
8935
+ setError(transactionErrorMessage(isPollarApiError(err) ? err.code : null, err instanceof Error ? err.message : null));
8936
+ } finally {
8937
+ setBusy(false);
8938
+ }
8939
+ }
8940
+ return /* @__PURE__ */ jsx("div", { className: "pollar-overlay", style: overlayStyle, children: /* @__PURE__ */ jsx(
8941
+ CustodyMigrationModalTemplate,
8942
+ {
8943
+ theme,
8944
+ accentColor,
8945
+ styleOverrides,
8946
+ state,
8947
+ busy,
8948
+ error,
8949
+ blockedReasons,
8950
+ onMove: (custody) => void move(custody),
8951
+ onLater,
8952
+ onLogout: () => void getClient().logout()
8953
+ }
8954
+ ) });
8955
+ }
8956
+ function CloseIcon3() {
8957
+ return /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", "aria-hidden": true, children: /* @__PURE__ */ jsx("path", { d: "m3 3 8 8M11 3l-8 8", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round" }) });
8958
+ }
8959
+ var shortKey = (key) => `${key.slice(0, 6)}...${key.slice(-4)}`;
8960
+ function Button(props) {
8961
+ const running = props.busy === props.id;
8962
+ return /* @__PURE__ */ jsx(
8963
+ "button",
8964
+ {
8965
+ type: "button",
8966
+ className: `pollar-recovery-btn pollar-recovery-btn-${props.kind ?? "quiet"}`,
8967
+ onClick: props.onClick,
8968
+ disabled: props.disabled || props.busy !== null,
8969
+ children: running ? props.busyLabel ?? "Working..." : props.label
8970
+ }
8971
+ );
8972
+ }
8973
+ function CodeForm(props) {
8974
+ const [code, setCode] = useState("");
8975
+ return /* @__PURE__ */ jsxs(
8976
+ "form",
8977
+ {
8978
+ className: "pollar-recovery-row",
8979
+ onSubmit: (e) => {
8980
+ e.preventDefault();
8981
+ if (code.trim()) props.onSubmit(code);
8982
+ },
8983
+ children: [
8984
+ /* @__PURE__ */ jsx(
8985
+ "input",
8986
+ {
8987
+ className: "pollar-recovery-input",
8988
+ value: code,
8989
+ onChange: (e) => setCode(e.target.value),
8990
+ placeholder: "Code from the email",
8991
+ autoComplete: "one-time-code",
8992
+ "aria-label": "Code from the email"
8993
+ }
8994
+ ),
8995
+ /* @__PURE__ */ jsx(
8996
+ Button,
8997
+ {
8998
+ busy: props.busy,
8999
+ id: props.id,
9000
+ label: props.label,
9001
+ kind: "primary",
9002
+ onClick: () => code.trim() && props.onSubmit(code)
9003
+ }
9004
+ )
9005
+ ]
9006
+ }
9007
+ );
9008
+ }
9009
+ function RecoveryModalTemplate(p) {
9010
+ const cssVars = buildModalCssVars(p.theme, p.accentColor, p.styleOverrides);
9011
+ const [exportAccepted, setExportAccepted] = useState(false);
9012
+ const [risksAccepted, setRisksAccepted] = useState(false);
9013
+ const state = p.state;
9014
+ const locked = !!state && state.required && !state.configured;
9015
+ const risksPending = locked && p.selfCustodyRisks && !risksAccepted;
9016
+ const lastMethod = !!state && state.passkeys.length + (state.email.verified ? 1 : 0) + (state.devices.length >= 2 ? 1 : 0) <= 1;
9017
+ let body;
9018
+ if (!state) {
9019
+ body = /* @__PURE__ */ jsx("p", { className: "pollar-recovery-muted", children: "Loading your wallet's recovery methods..." });
9020
+ } else if (!state.required) {
9021
+ body = /* @__PURE__ */ jsx("p", { className: "pollar-recovery-muted", children: "This wallet doesn't need recovery methods: it isn't secured by your device." });
9022
+ } else if (!p.thisDeviceEnrolled && p.view !== "recover-email") {
9023
+ body = /* @__PURE__ */ jsxs(Fragment, { children: [
9024
+ /* @__PURE__ */ jsx("div", { className: "pollar-recovery-banner pollar-recovery-banner-warn", role: "status", children: "This device isn't part of your wallet yet. Get back in with one of your recovery methods, or ask a device that already has the wallet to add this one." }),
9025
+ state.passkeys.length > 0 && p.passkeyAvailable && /* @__PURE__ */ jsx(Button, { busy: p.busy, id: "recover-passkey", label: "Use a passkey", kind: "primary", onClick: p.onRecoverWithPasskey }),
9026
+ state.email.verified && /* @__PURE__ */ jsx(
9027
+ Button,
9028
+ {
9029
+ busy: p.busy,
9030
+ id: "recover-send",
9031
+ label: `Email a code to ${state.email.address ?? "your email"}`,
9032
+ onClick: () => p.onView("recover-email")
9033
+ }
9034
+ ),
9035
+ /* @__PURE__ */ jsx(Button, { busy: p.busy, id: "enroll", label: "Ask another device to add this one", onClick: p.onRequestEnrollment })
9036
+ ] });
9037
+ } else if (risksPending && p.view === "overview") {
9038
+ body = /* @__PURE__ */ jsxs(Fragment, { children: [
9039
+ /* @__PURE__ */ jsx("div", { className: "pollar-recovery-banner pollar-recovery-banner-warn", role: "alert", children: "This is a self-custody wallet: only your device can sign. If you lose this device and every recovery method, nobody can recover the wallet or its funds, not the app and not Pollar. Nobody can undo or freeze a transaction you sign." }),
9040
+ /* @__PURE__ */ jsx(
9041
+ Button,
9042
+ {
9043
+ busy: p.busy,
9044
+ id: "accept-risks",
9045
+ label: "I understand and accept these risks",
9046
+ kind: "primary",
9047
+ onClick: () => setRisksAccepted(true)
9048
+ }
9049
+ )
9050
+ ] });
9051
+ } else if (p.view === "recover-email") {
9052
+ body = /* @__PURE__ */ jsxs(Fragment, { children: [
9053
+ /* @__PURE__ */ jsxs("p", { className: "pollar-recovery-muted", children: [
9054
+ "We'll email a code to ",
9055
+ state.email.address,
9056
+ ". The code goes straight to the wallet provider, encrypted on this device."
9057
+ ] }),
9058
+ !p.codeSent ? /* @__PURE__ */ jsx(Button, { busy: p.busy, id: "recover-send", label: "Send code", kind: "primary", onClick: p.onSendRecoveryCode }) : /* @__PURE__ */ jsx(CodeForm, { busy: p.busy, id: "recover-code", label: "Recover", onSubmit: p.onRecoverWithCode }),
9059
+ /* @__PURE__ */ jsx(Button, { busy: p.busy, id: "back", label: "Back", onClick: () => p.onView("overview") })
9060
+ ] });
9061
+ } else if (p.view === "email") {
9062
+ body = /* @__PURE__ */ jsxs(Fragment, { children: [
9063
+ /* @__PURE__ */ jsx("p", { className: "pollar-recovery-muted", children: "Confirm an email you control. If you lose this device, a code sent there lets you add a new one. Anyone who controls this inbox could do the same, so use one that is well protected." }),
9064
+ !p.codeSent ? /* @__PURE__ */ jsxs(
9065
+ "form",
9066
+ {
9067
+ className: "pollar-recovery-row",
9068
+ onSubmit: (e) => {
9069
+ e.preventDefault();
9070
+ p.onSendEmailCode();
9071
+ },
9072
+ children: [
9073
+ /* @__PURE__ */ jsx(
9074
+ "input",
8305
9075
  {
8306
- type: "button",
8307
- className: "pollar-cosign-btn pollar-cosign-btn-secondary",
8308
- disabled: !!busy,
8309
- onClick: () => onReject(current),
8310
- children: busy === "rejecting" ? "Rejecting..." : "Reject"
9076
+ className: "pollar-recovery-input",
9077
+ type: "email",
9078
+ value: p.emailDraft,
9079
+ onChange: (e) => p.onEmailDraft(e.target.value),
9080
+ placeholder: "you@example.com",
9081
+ "aria-label": "Email"
8311
9082
  }
8312
9083
  ),
8313
9084
  /* @__PURE__ */ jsx(
8314
- "button",
9085
+ Button,
8315
9086
  {
8316
- type: "button",
8317
- className: "pollar-cosign-btn pollar-cosign-btn-primary",
8318
- disabled: !canVote,
8319
- onClick: () => onApprove(current),
8320
- children: busy === "approving" ? "Approving..." : "Approve"
9087
+ busy: p.busy,
9088
+ id: "email-send",
9089
+ label: "Send code",
9090
+ kind: "primary",
9091
+ onClick: p.onSendEmailCode,
9092
+ disabled: !p.emailDraft.trim()
8321
9093
  }
8322
9094
  )
9095
+ ]
9096
+ }
9097
+ ) : /* @__PURE__ */ jsx(CodeForm, { busy: p.busy, id: "email-confirm", label: "Confirm", onSubmit: p.onConfirmEmail }),
9098
+ /* @__PURE__ */ jsx(Button, { busy: p.busy, id: "back", label: "Back", onClick: () => p.onView("overview") })
9099
+ ] });
9100
+ } else if (p.view === "export") {
9101
+ body = p.phrase ? /* @__PURE__ */ jsxs(Fragment, { children: [
9102
+ /* @__PURE__ */ jsx("div", { className: "pollar-recovery-banner pollar-recovery-banner-warn", role: "alert", children: "Anyone with these words controls every account in this wallet. Write them down offline and never share them." }),
9103
+ /* @__PURE__ */ jsx("pre", { className: "pollar-recovery-phrase", children: p.phrase }),
9104
+ /* @__PURE__ */ jsx(Button, { busy: p.busy, id: "hide", label: "Hide", kind: "primary", onClick: () => p.onView("overview") })
9105
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
9106
+ /* @__PURE__ */ jsx("p", { className: "pollar-recovery-muted", children: "Your recovery phrase lets you take this wallet to another app. It is decrypted only on this device; the app and Pollar never see it." }),
9107
+ /* @__PURE__ */ jsxs("label", { className: "pollar-recovery-check", children: [
9108
+ /* @__PURE__ */ jsx("input", { type: "checkbox", checked: exportAccepted, onChange: (e) => setExportAccepted(e.target.checked) }),
9109
+ " I understand that whoever sees the phrase controls the wallet"
9110
+ ] }),
9111
+ /* @__PURE__ */ jsx(
9112
+ Button,
9113
+ {
9114
+ busy: p.busy,
9115
+ id: "export",
9116
+ label: "Show recovery phrase",
9117
+ kind: "danger",
9118
+ onClick: p.onExport,
9119
+ disabled: !exportAccepted
9120
+ }
9121
+ ),
9122
+ /* @__PURE__ */ jsx(Button, { busy: p.busy, id: "back", label: "Back", onClick: () => p.onView("overview") })
9123
+ ] });
9124
+ } else {
9125
+ const enrollments = p.devices?.enrollments ?? [];
9126
+ body = /* @__PURE__ */ jsxs(Fragment, { children: [
9127
+ locked ? /* @__PURE__ */ jsx("div", { className: "pollar-recovery-banner pollar-recovery-banner-warn", role: "alert", children: "Your wallet is locked until you add a way to recover it. Without one, losing this device means losing the funds, and nobody can get them back, not the app and not Pollar. Add a passkey, a second device or your email." }) : /* @__PURE__ */ jsx("div", { className: "pollar-recovery-banner pollar-recovery-banner-ok", role: "status", children: "Your wallet can be recovered if you lose this device." }),
9128
+ /* @__PURE__ */ jsxs("section", { className: "pollar-recovery-section", children: [
9129
+ /* @__PURE__ */ jsx("h3", { children: "Passkeys" }),
9130
+ state.passkeys.length === 0 && /* @__PURE__ */ jsx("p", { className: "pollar-recovery-muted", children: "No passkey yet." }),
9131
+ state.passkeys.map((pk) => /* @__PURE__ */ jsxs("div", { className: "pollar-recovery-item", children: [
9132
+ /* @__PURE__ */ jsx("span", { children: pk.name }),
9133
+ /* @__PURE__ */ jsx(
9134
+ Button,
9135
+ {
9136
+ busy: p.busy,
9137
+ id: `passkey:${pk.authenticatorId}`,
9138
+ label: "Remove",
9139
+ kind: "danger",
9140
+ onClick: () => p.onRemovePasskey(pk.authenticatorId),
9141
+ disabled: lastMethod
9142
+ }
9143
+ )
9144
+ ] }, pk.authenticatorId)),
9145
+ p.passkeyAvailable ? /* @__PURE__ */ jsx(Button, { busy: p.busy, id: "passkey", label: "Add a passkey", onClick: p.onAddPasskey }) : /* @__PURE__ */ jsx("p", { className: "pollar-recovery-muted", children: "Passkeys aren't available on this device." })
9146
+ ] }),
9147
+ /* @__PURE__ */ jsxs("section", { className: "pollar-recovery-section", children: [
9148
+ /* @__PURE__ */ jsx("h3", { children: "Email" }),
9149
+ state.email.verified ? /* @__PURE__ */ jsxs("div", { className: "pollar-recovery-item", children: [
9150
+ /* @__PURE__ */ jsx("span", { children: state.email.address }),
9151
+ /* @__PURE__ */ jsx("span", { className: "pollar-recovery-tag", children: "Confirmed" })
9152
+ ] }) : /* @__PURE__ */ jsx(Button, { busy: p.busy, id: "email", label: "Confirm an email", onClick: () => p.onView("email") })
9153
+ ] }),
9154
+ /* @__PURE__ */ jsxs("section", { className: "pollar-recovery-section", children: [
9155
+ /* @__PURE__ */ jsx("h3", { children: "Devices" }),
9156
+ (p.devices?.devices ?? []).map((d) => /* @__PURE__ */ jsxs("div", { className: "pollar-recovery-item", children: [
9157
+ /* @__PURE__ */ jsx("span", { children: d.label ?? shortKey(d.publicKey) }),
9158
+ d.publicKey.toLowerCase() === p.thisDeviceKey && /* @__PURE__ */ jsx("span", { className: "pollar-recovery-tag", children: "This device" })
9159
+ ] }, d.id)),
9160
+ enrollments.map((e) => /* @__PURE__ */ jsxs("div", { className: "pollar-recovery-item pollar-recovery-item-pending", children: [
9161
+ /* @__PURE__ */ jsxs("span", { children: [
9162
+ e.label ?? "A device",
9163
+ " wants to join (",
9164
+ shortKey(e.publicKey),
9165
+ ")"
9166
+ ] }),
9167
+ /* @__PURE__ */ jsxs("span", { className: "pollar-recovery-actions", children: [
9168
+ /* @__PURE__ */ jsx(
9169
+ Button,
9170
+ {
9171
+ busy: p.busy,
9172
+ id: `approve:${e.id}`,
9173
+ label: "Approve",
9174
+ kind: "primary",
9175
+ onClick: () => p.onApproveDevice(e.id)
9176
+ }
9177
+ ),
9178
+ /* @__PURE__ */ jsx(Button, { busy: p.busy, id: `reject:${e.id}`, label: "Reject", onClick: () => p.onRejectDevice(e.id) })
8323
9179
  ] })
9180
+ ] }, e.id)),
9181
+ /* @__PURE__ */ jsx("p", { className: "pollar-recovery-muted", children: 'To add another device, sign in there and choose "Ask another device to add this one"; it shows up here to approve. Only approve a device you are holding.' })
9182
+ ] }),
9183
+ /* @__PURE__ */ jsxs("section", { className: "pollar-recovery-section", children: [
9184
+ /* @__PURE__ */ jsx("h3", { children: "Recovery phrase" }),
9185
+ /* @__PURE__ */ jsx(Button, { busy: p.busy, id: "export", label: "Export recovery phrase", onClick: () => p.onView("export") })
9186
+ ] })
9187
+ ] });
9188
+ }
9189
+ return /* @__PURE__ */ jsxs(
9190
+ "div",
9191
+ {
9192
+ className: "pollar-modal-card pollar-recovery-modal",
9193
+ "data-theme": p.theme,
9194
+ style: cssVars,
9195
+ onClick: (e) => e.stopPropagation(),
9196
+ children: [
9197
+ /* @__PURE__ */ jsxs("div", { className: "pollar-modal-header", children: [
9198
+ /* @__PURE__ */ jsx("h2", { className: "pollar-modal-title", children: "Wallet recovery" }),
9199
+ /* @__PURE__ */ jsx("div", { className: "pollar-modal-header-actions", children: /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-modal-close", onClick: p.onClose, "aria-label": "Close", title: "Close", children: /* @__PURE__ */ jsx(CloseIcon3, {}) }) })
9200
+ ] }),
9201
+ /* @__PURE__ */ jsxs("div", { className: "pollar-recovery-body", children: [
9202
+ body,
9203
+ p.notice && /* @__PURE__ */ jsx("p", { className: "pollar-recovery-notice", role: "status", children: p.notice }),
9204
+ p.error && /* @__PURE__ */ jsx("p", { className: "pollar-recovery-error", role: "alert", children: p.error })
8324
9205
  ] }),
8325
9206
  /* @__PURE__ */ jsx(PollarModalFooter, {})
8326
9207
  ]
8327
9208
  }
8328
9209
  );
8329
9210
  }
8330
- function GroupSteps({ request }) {
8331
- const group = cosignGroupOf(request);
8332
- if (!group || group.steps.length < 2) return null;
8333
- const stepKey = cosignStepKeyOf(request);
8334
- return /* @__PURE__ */ jsxs("div", { className: "pollar-cosign-group", children: [
8335
- /* @__PURE__ */ jsx("p", { className: "pollar-cosign-group-title", children: group.title }),
8336
- /* @__PURE__ */ jsx("p", { className: "pollar-cosign-muted", children: "Approving covers these steps; each one is checked before it is signed." }),
8337
- /* @__PURE__ */ jsx("ol", { className: "pollar-cosign-group-steps", children: group.steps.map((step) => {
8338
- const isThis = stepKey !== null && stepInGroup({ steps: [step] }, stepKey);
8339
- return /* @__PURE__ */ jsxs("li", { className: isThis ? "pollar-cosign-group-step-current" : void 0, children: [
8340
- step.label,
8341
- isThis && /* @__PURE__ */ jsx("span", { className: "pollar-cosign-muted", children: " (this one)" })
8342
- ] }, step.key);
8343
- }) })
8344
- ] });
9211
+ function errorText2(err) {
9212
+ return cosignRefusalMessage(err) ?? transactionErrorMessage(isPollarApiError(err) ? err.code : null, err instanceof Error ? err.message : null);
8345
9213
  }
8346
- function CosignModal({ onClose }) {
8347
- const { getClient, styles, network } = usePollar();
9214
+ function RecoveryModal({ onClose }) {
9215
+ const { getClient, styles, refreshRecovery, appConfig } = usePollar();
8348
9216
  const { theme, accentColor, styleOverrides, overlayStyle } = modalChrome(styles);
8349
- const { pending, lastResolved, approve, reject, verify, inspectQuorum } = useCosign();
8350
- const [selectedId, setSelectedId] = useState(null);
8351
- const [verification, setVerification] = useState(null);
9217
+ const recovery = getClient().cosign.recovery;
9218
+ const [state, setState] = useState(null);
9219
+ const [devices, setDevices] = useState(null);
9220
+ const [thisDeviceKey, setThisDeviceKey] = useState(null);
9221
+ const [view, setView] = useState("overview");
8352
9222
  const [busy, setBusy] = useState(null);
8353
9223
  const [error, setError] = useState(null);
8354
- const [approveFailedFor, setApproveFailedFor] = useState(null);
8355
- const [quorumFailure, setQuorumFailure] = useState(null);
8356
- useEffect(() => {
8357
- let cancelled = false;
8358
- inspectQuorum().then((q) => {
8359
- if (!cancelled && !q.ok) setQuorumFailure(q.reason);
8360
- }).catch(() => void 0);
8361
- return () => {
8362
- cancelled = true;
8363
- };
8364
- }, [inspectQuorum]);
8365
- const current = pending.find((r) => r.id === selectedId) ?? pending[0] ?? null;
8366
- const currentRef = useRef(current);
8367
- currentRef.current = current;
8368
- const currentId = current?.id ?? null;
9224
+ const [notice, setNotice] = useState(null);
9225
+ const [emailDraft, setEmailDraft] = useState("");
9226
+ const [codeSent, setCodeSent] = useState(false);
9227
+ const [phrase, setPhrase] = useState(null);
9228
+ const load = useCallback(async () => {
9229
+ const client = getClient();
9230
+ const [nextState, nextDevices, key] = await Promise.all([
9231
+ client.cosign.recovery.status(),
9232
+ client.cosign.devices(),
9233
+ client.cosign.publicKeyHex()
9234
+ ]);
9235
+ setState(nextState);
9236
+ setDevices(nextDevices);
9237
+ setThisDeviceKey(key.toLowerCase());
9238
+ if (nextState.email.address) setEmailDraft((d) => d || nextState.email.address);
9239
+ refreshRecovery();
9240
+ }, [getClient, refreshRecovery]);
8369
9241
  useEffect(() => {
8370
- const request = currentRef.current;
8371
- if (!request) return;
8372
- setVerification(null);
8373
- setError(null);
8374
- setApproveFailedFor(null);
8375
- let cancelled = false;
8376
- verify(request).then((v) => {
8377
- if (!cancelled) setVerification(v);
8378
- }).catch((err) => {
8379
- if (!cancelled) setVerification({ ok: false, reason: err instanceof Error ? err.message : String(err) });
8380
- });
8381
- return () => {
8382
- cancelled = true;
8383
- };
8384
- }, [currentId, verify]);
9242
+ load().catch((err) => setError(errorText2(err)));
9243
+ }, [load]);
8385
9244
  const onCloseRef = useRef(onClose);
8386
9245
  onCloseRef.current = onClose;
8387
- useEffect(() => {
8388
- return getClient().onAuthStateChange((authState) => {
9246
+ useEffect(
9247
+ () => getClient().onAuthStateChange((authState) => {
8389
9248
  if (authState.step === "idle") onCloseRef.current();
8390
- });
8391
- }, [getClient]);
9249
+ }),
9250
+ [getClient]
9251
+ );
8392
9252
  const run = useCallback(
8393
- async (kind, request, action) => {
8394
- setBusy(kind);
9253
+ async (key, action, after) => {
9254
+ setBusy(key);
8395
9255
  setError(null);
9256
+ setNotice(null);
8396
9257
  try {
8397
- await action(request);
9258
+ await action();
9259
+ await load();
9260
+ if (after?.notice) setNotice(after.notice);
9261
+ if (after?.view) setView(after.view);
8398
9262
  } catch (err) {
8399
- setError(transactionErrorMessage(isPollarApiError(err) ? err.code : null, err instanceof Error ? err.message : null));
8400
- if (kind === "approving") setApproveFailedFor(request.id);
9263
+ setError(errorText2(err));
8401
9264
  } finally {
8402
9265
  setBusy(null);
8403
9266
  }
8404
9267
  },
8405
- []
9268
+ [load]
8406
9269
  );
9270
+ const enrolled = !!thisDeviceKey && !!devices?.devices.some((d) => d.publicKey.toLowerCase() === thisDeviceKey);
8407
9271
  return /* @__PURE__ */ jsx("div", { className: "pollar-overlay", style: overlayStyle, onClick: onClose, children: /* @__PURE__ */ jsx(
8408
- CosignModalTemplate,
9272
+ RecoveryModalTemplate,
8409
9273
  {
8410
9274
  theme,
8411
9275
  accentColor,
8412
9276
  styleOverrides,
8413
- pending,
8414
- current,
8415
- verification: current ? quorumFailure ? { ok: false, reason: quorumFailure } : verification : null,
9277
+ state,
9278
+ devices,
9279
+ thisDeviceKey,
9280
+ thisDeviceEnrolled: enrolled,
9281
+ passkeyAvailable: recovery.passkeyAvailable,
9282
+ selfCustodyRisks: appConfig.application?.walletCustodyMode === "SELF_CUSTODY" && devices?.custody === "SELF_CUSTODY",
9283
+ view,
8416
9284
  busy,
8417
9285
  error,
8418
- approveBlocked: !!current && approveFailedFor === current.id,
8419
- ...lastResolved ? { lastResolved } : {},
8420
- explorerUrl: lastResolved?.submit?.hash ? explorerTxUrl(lastResolved.network, lastResolved.submit.hash, network) : null,
8421
- onSelect: (r) => setSelectedId(r.id),
8422
- onApprove: (r) => void run("approving", r, approve),
8423
- onReject: (r) => void run("rejecting", r, reject),
9286
+ notice,
9287
+ emailDraft,
9288
+ codeSent,
9289
+ phrase,
9290
+ onView: (next) => {
9291
+ setError(null);
9292
+ setNotice(null);
9293
+ setCodeSent(false);
9294
+ if (next !== "export") setPhrase(null);
9295
+ setView(next);
9296
+ },
9297
+ onEmailDraft: setEmailDraft,
9298
+ onAddPasskey: () => void run("passkey", () => recovery.addPasskey(), { notice: "Passkey added." }),
9299
+ onRemovePasskey: (id) => void run(`passkey:${id}`, () => recovery.removePasskey(id)),
9300
+ onSendEmailCode: () => void run("email-send", async () => {
9301
+ await recovery.startEmail(emailDraft.trim());
9302
+ setCodeSent(true);
9303
+ }),
9304
+ onConfirmEmail: (code) => void run("email-confirm", () => recovery.confirmEmail(code.trim()), { notice: "Email confirmed.", view: "overview" }),
9305
+ onApproveDevice: (id) => void run(`approve:${id}`, () => recovery.approveDevice(id), { notice: "Device added." }),
9306
+ onRejectDevice: (id) => void run(`reject:${id}`, () => recovery.rejectDevice(id)),
9307
+ onRequestEnrollment: () => void run("enroll", () => recovery.requestDeviceEnrollment(), {
9308
+ notice: "Request sent. Approve it from a device that already has this wallet within 15 minutes."
9309
+ }),
9310
+ onRecoverWithPasskey: () => void run("recover-passkey", () => recovery.recoverWithPasskey(), {
9311
+ notice: "This device is part of your wallet again."
9312
+ }),
9313
+ onSendRecoveryCode: () => void run("recover-send", async () => {
9314
+ await recovery.startEmailRecovery();
9315
+ setCodeSent(true);
9316
+ }),
9317
+ onRecoverWithCode: (code) => void run("recover-code", () => recovery.recoverWithEmailCode(code.trim()), {
9318
+ notice: "This device is part of your wallet again.",
9319
+ view: "overview"
9320
+ }),
9321
+ onExport: () => void run("export", async () => {
9322
+ setPhrase(await recovery.exportWallet());
9323
+ }),
8424
9324
  onClose
8425
9325
  }
8426
9326
  ) });
@@ -8451,7 +9351,7 @@ function detailRows2(confirmation) {
8451
9351
  push("Network", confirmation.network);
8452
9352
  return rows;
8453
9353
  }
8454
- function CloseIcon3() {
9354
+ function CloseIcon4() {
8455
9355
  return /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", "aria-hidden": true, children: /* @__PURE__ */ jsx("path", { d: "m3 3 8 8M11 3l-8 8", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round" }) });
8456
9356
  }
8457
9357
  function ConfirmModal({ confirmation }) {
@@ -8475,7 +9375,7 @@ function ConfirmModal({ confirmation }) {
8475
9375
  children: [
8476
9376
  /* @__PURE__ */ jsxs("div", { className: "pollar-modal-header", children: [
8477
9377
  /* @__PURE__ */ jsx("h2", { className: "pollar-modal-title", children: "Confirm transaction" }),
8478
- /* @__PURE__ */ jsx("div", { className: "pollar-modal-header-actions", children: /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-modal-close", onClick: confirmation.reject, "aria-label": "Close", title: "Close", children: /* @__PURE__ */ jsx(CloseIcon3, {}) }) })
9378
+ /* @__PURE__ */ jsx("div", { className: "pollar-modal-header-actions", children: /* @__PURE__ */ jsx("button", { type: "button", className: "pollar-modal-close", onClick: confirmation.reject, "aria-label": "Close", title: "Close", children: /* @__PURE__ */ jsx(CloseIcon4, {}) }) })
8479
9379
  ] }),
8480
9380
  /* @__PURE__ */ jsxs("div", { className: "pollar-cosign-detail", children: [
8481
9381
  /* @__PURE__ */ jsx("p", { className: "pollar-cosign-detail-title", children: labelOf2(confirmation) }),
@@ -8612,6 +9512,25 @@ function formatDate(iso) {
8612
9512
  function truncateMiddle(value, head = 4, tail = 4) {
8613
9513
  return value.length <= head + tail + 1 ? value : `${value.slice(0, head)}\u2026${value.slice(-tail)}`;
8614
9514
  }
9515
+ function formatFee(fee) {
9516
+ if (fee.unit === "lamports") {
9517
+ try {
9518
+ return `${fromBaseUnits(fee.amount, 9)} SOL`;
9519
+ } catch {
9520
+ }
9521
+ }
9522
+ return `${fee.amount} ${fee.unit}`;
9523
+ }
9524
+ function formatAmount4(details) {
9525
+ const { amount, decimals, symbol } = details ?? {};
9526
+ if (typeof amount !== "string" || typeof decimals !== "number") return null;
9527
+ try {
9528
+ const value = fromBaseUnits(amount, decimals);
9529
+ return typeof symbol === "string" ? `${value} ${symbol}` : value;
9530
+ } catch {
9531
+ return null;
9532
+ }
9533
+ }
8615
9534
  var STELLAR_ADDRESS = /\b[GC][A-Z2-7]{55}\b/g;
8616
9535
  function AddressChip({ value, label }) {
8617
9536
  return /* @__PURE__ */ jsxs("span", { className: "pollar-hist-item-issuer", children: [
@@ -8749,15 +9668,17 @@ function TxHistoryModalTemplate({
8749
9668
  ] }),
8750
9669
  /* @__PURE__ */ jsxs("span", { className: "pollar-hist-item-meta", children: [
8751
9670
  /* @__PURE__ */ jsx("span", { children: record.operation }),
9671
+ record.chain !== "STELLAR" && formatAmount4(record.details) && /* @__PURE__ */ jsxs("span", { children: [
9672
+ "\xB7 ",
9673
+ formatAmount4(record.details)
9674
+ ] }),
8752
9675
  typeof record.details?.sponsored === "boolean" && /* @__PURE__ */ jsxs("span", { children: [
8753
9676
  "\xB7 ",
8754
9677
  record.details.sponsored ? "Sponsored" : "Self-paid"
8755
9678
  ] }),
8756
9679
  record.fee && /* @__PURE__ */ jsxs("span", { children: [
8757
9680
  "\xB7 ",
8758
- record.fee.amount,
8759
- " ",
8760
- record.fee.unit
9681
+ formatFee(record.fee)
8761
9682
  ] })
8762
9683
  ] }),
8763
9684
  /* @__PURE__ */ jsxs("div", { className: "pollar-hist-item-footer", children: [
@@ -9068,6 +9989,48 @@ var browserPasskeySigner = async ({ credentialId, challenge }) => {
9068
9989
  signature: response.signature
9069
9990
  };
9070
9991
  };
9992
+ var browserRecoveryPasskey = {
9993
+ async create({ challenge, userName }) {
9994
+ const rpId = window.location.hostname;
9995
+ const response = await startRegistration({
9996
+ optionsJSON: {
9997
+ challenge,
9998
+ rp: { id: rpId, name: rpId },
9999
+ user: { id: randomUserId(), name: userName, displayName: userName },
10000
+ pubKeyCredParams: [
10001
+ { type: "public-key", alg: -7 },
10002
+ { type: "public-key", alg: -257 }
10003
+ ],
10004
+ authenticatorSelection: { residentKey: "preferred", userVerification: "preferred" },
10005
+ attestation: "none",
10006
+ timeout: 6e4
10007
+ }
10008
+ });
10009
+ return {
10010
+ credentialId: response.id,
10011
+ clientDataJson: response.response.clientDataJSON,
10012
+ attestationObject: response.response.attestationObject,
10013
+ transports: response.response.transports ?? []
10014
+ };
10015
+ },
10016
+ async get({ challenge, allowCredentials }) {
10017
+ const rpId = window.location.hostname;
10018
+ const response = await startAuthentication({
10019
+ optionsJSON: {
10020
+ challenge,
10021
+ rpId,
10022
+ allowCredentials: allowCredentials.map((id) => ({ id, type: "public-key" })),
10023
+ userVerification: "preferred"
10024
+ }
10025
+ });
10026
+ return {
10027
+ credentialId: response.id,
10028
+ authenticatorData: response.response.authenticatorData,
10029
+ clientDataJson: response.response.clientDataJSON,
10030
+ signature: response.response.signature
10031
+ };
10032
+ }
10033
+ };
9071
10034
  var DEFAULT_APP_CONFIG = {
9072
10035
  application: { name: "", network: "testnet", chains: [] },
9073
10036
  styles: {}
@@ -9092,7 +10055,11 @@ function PollarProvider({
9092
10055
  }) {
9093
10056
  const [pollarClient] = useState(() => {
9094
10057
  if (isPollarClient(client)) {
9095
- client.setPasskeyDefaults({ passkey: browserPasskeyCeremony, passkeySign: browserPasskeySigner });
10058
+ client.setPasskeyDefaults({
10059
+ passkey: browserPasskeyCeremony,
10060
+ passkeySign: browserPasskeySigner,
10061
+ recoveryPasskey: browserRecoveryPasskey
10062
+ });
9096
10063
  return client;
9097
10064
  }
9098
10065
  const alreadyBuilt = clientByConfig.get(client);
@@ -9100,7 +10067,8 @@ function PollarProvider({
9100
10067
  const built = new PollarClient({
9101
10068
  ...client,
9102
10069
  passkey: client.passkey ?? browserPasskeyCeremony,
9103
- passkeySign: client.passkeySign ?? browserPasskeySigner
10070
+ passkeySign: client.passkeySign ?? browserPasskeySigner,
10071
+ recoveryPasskey: client.recoveryPasskey ?? browserRecoveryPasskey
9104
10072
  });
9105
10073
  clientByConfig.set(client, built);
9106
10074
  return built;
@@ -9255,6 +10223,8 @@ function PollarProvider({
9255
10223
  const [receiveModalOpen, setReceiveModalOpen] = useState(false);
9256
10224
  const [sessionsModalOpen, setSessionsModalOpen] = useState(false);
9257
10225
  const [cosignModalOpen, setCosignModalOpen] = useState(false);
10226
+ const [cosignNextStep, setCosignNextStep] = useState(null);
10227
+ const [recoveryModalOpen, setRecoveryModalOpen] = useState(false);
9258
10228
  const cosignPendingCount = useRef(0);
9259
10229
  useEffect(() => {
9260
10230
  if (cosign.pending.length > cosignPendingCount.current) setCosignModalOpen(true);
@@ -9277,6 +10247,7 @@ function PollarProvider({
9277
10247
  setReceiveModalOpen(false);
9278
10248
  setSessionsModalOpen(false);
9279
10249
  setCosignModalOpen(false);
10250
+ setRecoveryModalOpen(false);
9280
10251
  setDistributionRulesModalOpen(false);
9281
10252
  }),
9282
10253
  [pollarClient]
@@ -9285,6 +10256,45 @@ function PollarProvider({
9285
10256
  const walletProvisioning = sessionState?.wallet?.provisioning;
9286
10257
  const hasSession = sessionState !== null;
9287
10258
  const getClient = useCallback(() => pollarClient, [pollarClient]);
10259
+ const [recovery, setRecovery] = useState(null);
10260
+ const [recoveryTick, setRecoveryTick] = useState(0);
10261
+ const refreshRecovery = useCallback(() => setRecoveryTick((t) => t + 1), []);
10262
+ useEffect(() => {
10263
+ if (!hasSession) {
10264
+ setRecovery(null);
10265
+ return;
10266
+ }
10267
+ let cancelled = false;
10268
+ pollarClient.cosign.recovery.status().then((next) => {
10269
+ if (!cancelled) setRecovery(next);
10270
+ }).catch(() => {
10271
+ if (!cancelled) setRecovery(null);
10272
+ });
10273
+ return () => {
10274
+ cancelled = true;
10275
+ };
10276
+ }, [pollarClient, hasSession, walletAddress, recoveryTick]);
10277
+ const recoveryLocked = !!recovery && recovery.required && !recovery.configured;
10278
+ const [custodyMigration, setCustodyMigration] = useState(null);
10279
+ const [custodyMigrationTick, setCustodyMigrationTick] = useState(0);
10280
+ const refreshCustodyMigration = useCallback(() => setCustodyMigrationTick((t) => t + 1), []);
10281
+ const [custodyMigrationLater, setCustodyMigrationLater] = useState(false);
10282
+ useEffect(() => {
10283
+ if (!hasSession) {
10284
+ setCustodyMigration(null);
10285
+ setCustodyMigrationLater(false);
10286
+ return;
10287
+ }
10288
+ let cancelled = false;
10289
+ pollarClient.cosign.custodyMigration().then((next) => {
10290
+ if (!cancelled) setCustodyMigration(next);
10291
+ }).catch(() => {
10292
+ if (!cancelled) setCustodyMigration(null);
10293
+ });
10294
+ return () => {
10295
+ cancelled = true;
10296
+ };
10297
+ }, [pollarClient, hasSession, walletAddress, custodyMigrationTick]);
9288
10298
  const refreshWalletBalance = useCallback(() => pollarClient.refreshBalance(), [pollarClient, walletAddress]);
9289
10299
  const refreshAssets = useCallback(() => pollarClient.refreshAssets(), [pollarClient, walletAddress]);
9290
10300
  const contextValue = useMemo(() => {
@@ -9295,7 +10305,7 @@ function PollarProvider({
9295
10305
  wallets: pollarClient.getWallets(),
9296
10306
  isAuthenticated: hasSession,
9297
10307
  hasWallet: pollarClient.hasWallet(),
9298
- createWallet: () => pollarClient.createWallet(),
10308
+ createWallet: (options) => pollarClient.createWallet(options),
9299
10309
  verified,
9300
10310
  // client
9301
10311
  getClient,
@@ -9364,6 +10374,13 @@ function PollarProvider({
9364
10374
  // cosign
9365
10375
  cosign,
9366
10376
  openCosignModal: () => setCosignModalOpen(true),
10377
+ setCosignNextStep,
10378
+ recovery,
10379
+ recoveryLocked,
10380
+ refreshRecovery,
10381
+ openRecoveryModal: () => setRecoveryModalOpen(true),
10382
+ custodyMigration,
10383
+ refreshCustodyMigration,
9367
10384
  confirmations,
9368
10385
  // distribution
9369
10386
  openDistributionRulesModal: () => setDistributionRulesModalOpen(true),
@@ -9387,6 +10404,11 @@ function PollarProvider({
9387
10404
  }, [
9388
10405
  walletAddress,
9389
10406
  walletProvisioning,
10407
+ recovery,
10408
+ recoveryLocked,
10409
+ refreshRecovery,
10410
+ custodyMigration,
10411
+ refreshCustodyMigration,
9390
10412
  hasSession,
9391
10413
  verified,
9392
10414
  pollarClient,
@@ -9443,7 +10465,32 @@ function PollarProvider({
9443
10465
  /* @__PURE__ */ jsx(ModalErrorBoundary, { onClose: () => setCardModalOpen(false), children: /* @__PURE__ */ jsx(CardModal, { onClose: () => setCardModalOpen(false) }) }, `${cardModalKey}:${walletAddress}`),
9444
10466
  receiveModalOpen && /* @__PURE__ */ jsx(ModalErrorBoundary, { onClose: () => setReceiveModalOpen(false), children: /* @__PURE__ */ jsx(ReceiveModal, { onClose: () => setReceiveModalOpen(false) }) }),
9445
10467
  sessionsModalOpen && /* @__PURE__ */ jsx(ModalErrorBoundary, { onClose: () => setSessionsModalOpen(false), children: /* @__PURE__ */ jsx(SessionsModal, { onClose: () => setSessionsModalOpen(false) }) }),
9446
- cosignModalOpen && /* @__PURE__ */ jsx(ModalErrorBoundary, { onClose: () => setCosignModalOpen(false), children: /* @__PURE__ */ jsx(CosignModal, { onClose: () => setCosignModalOpen(false) }) }),
10468
+ cosignModalOpen && /* @__PURE__ */ jsx(ModalErrorBoundary, { onClose: () => setCosignModalOpen(false), children: /* @__PURE__ */ jsx(CosignModal, { nextStep: cosignNextStep, onClose: () => setCosignModalOpen(false) }) }),
10469
+ custodyMigration?.required && !custodyMigrationLater && /* @__PURE__ */ jsx(ModalErrorBoundary, { onClose: refreshCustodyMigration, children: /* @__PURE__ */ jsx(
10470
+ CustodyMigrationModal,
10471
+ {
10472
+ state: custodyMigration,
10473
+ onMoved: () => {
10474
+ refreshCustodyMigration();
10475
+ refreshRecovery();
10476
+ setRecoveryModalOpen(true);
10477
+ },
10478
+ onLater: () => {
10479
+ setCustodyMigrationLater(true);
10480
+ refreshCustodyMigration();
10481
+ refreshRecovery();
10482
+ }
10483
+ }
10484
+ ) }),
10485
+ recoveryModalOpen && /* @__PURE__ */ jsx(ModalErrorBoundary, { onClose: () => setRecoveryModalOpen(false), children: /* @__PURE__ */ jsx(
10486
+ RecoveryModal,
10487
+ {
10488
+ onClose: () => {
10489
+ setRecoveryModalOpen(false);
10490
+ refreshRecovery();
10491
+ }
10492
+ }
10493
+ ) }),
9447
10494
  firstConfirmation && /* @__PURE__ */ jsx(ModalErrorBoundary, { onClose: firstConfirmation.reject, children: /* @__PURE__ */ jsx(ConfirmModal, { confirmation: firstConfirmation }, firstConfirmation.id) }),
9448
10495
  distributionRulesModalOpen && /* @__PURE__ */ jsx(ModalErrorBoundary, { onClose: () => setDistributionRulesModalOpen(false), children: /* @__PURE__ */ jsx(DistributionRulesModal, { onClose: () => setDistributionRulesModalOpen(false) }) })
9449
10496
  ] });
@@ -9475,8 +10522,9 @@ function createPollarAdapterHook(key) {
9475
10522
  );
9476
10523
  };
9477
10524
  }
9478
- function ButtonLogo() {
9479
- return /* @__PURE__ */ jsx("img", { src: LOGO_POLLAR, alt: "Pollar", width: 22, height: 22, className: "pollar-wallet-btn-logo" });
10525
+ function ButtonLogo({ accentColor }) {
10526
+ const src = readableTextOn(accentColor) === "#ffffff" ? LOGO_POLLAR_MARK_ON_DARK : LOGO_POLLAR_MARK_ON_LIGHT;
10527
+ return /* @__PURE__ */ jsx("img", { src, alt: "Pollar", width: 22, height: 22, className: "pollar-wallet-btn-logo" });
9480
10528
  }
9481
10529
  function cropWallet(address) {
9482
10530
  if (address.length <= 12) return address;
@@ -9484,16 +10532,17 @@ function cropWallet(address) {
9484
10532
  }
9485
10533
  function WalletButtonTemplate({
9486
10534
  walletAddress,
10535
+ theme,
9487
10536
  accentColor,
10537
+ styleOverrides,
9488
10538
  open,
9489
10539
  copied,
9490
- dropdownBg,
9491
- dropdownBorder,
9492
- itemColor,
9493
10540
  wrapperRef,
9494
10541
  isInProgress,
9495
10542
  showCreateAccount,
9496
10543
  notReadyReason,
10544
+ recoveryLocked,
10545
+ onRecovery,
9497
10546
  creatingAccount,
9498
10547
  onToggleOpen,
9499
10548
  onCreateAccount,
@@ -9511,11 +10560,15 @@ function WalletButtonTemplate({
9511
10560
  signedIn,
9512
10561
  creatingWallet,
9513
10562
  createWalletError,
10563
+ custodyChoice,
9514
10564
  onCreateWallet
9515
10565
  }) {
10566
+ const [selfCustodyNotice, setSelfCustodyNotice] = useState(false);
10567
+ const [risksAccepted, setRisksAccepted] = useState(false);
10568
+ const cssVars = buildModalCssVars(theme, accentColor, styleOverrides);
9516
10569
  if (!walletAddress && signedIn) {
9517
- return /* @__PURE__ */ jsxs("div", { className: "pollar-wallet-wrapper", ref: wrapperRef, children: [
9518
- /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-btn", style: { backgroundColor: accentColor }, onClick: onToggleOpen, children: [
10570
+ return /* @__PURE__ */ jsxs("div", { className: "pollar-wallet-wrapper", ref: wrapperRef, style: cssVars, children: [
10571
+ /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-btn", onClick: onToggleOpen, children: [
9519
10572
  /* @__PURE__ */ jsx("span", { className: "pollar-wallet-btn-label", children: onCreateWallet ? "No wallet" : "Signed in" }),
9520
10573
  /* @__PURE__ */ jsx(
9521
10574
  "svg",
@@ -9531,42 +10584,93 @@ function WalletButtonTemplate({
9531
10584
  }
9532
10585
  )
9533
10586
  ] }),
9534
- open && /* @__PURE__ */ jsxs("div", { className: "pollar-wallet-dropdown", style: { backgroundColor: dropdownBg, borderColor: dropdownBorder }, children: [
10587
+ open && /* @__PURE__ */ jsxs("div", { className: "pollar-wallet-dropdown", children: [
9535
10588
  createWalletError && /* @__PURE__ */ jsxs(Fragment, { children: [
9536
- /* @__PURE__ */ jsx("div", { className: "pollar-wallet-dropdown-notice", style: { color: itemColor }, role: "status", children: createWalletError }),
10589
+ /* @__PURE__ */ jsx("div", { className: "pollar-wallet-dropdown-notice", role: "status", children: createWalletError }),
10590
+ /* @__PURE__ */ jsx("div", { className: "pollar-wallet-dropdown-divider" })
10591
+ ] }),
10592
+ onCreateWallet && !custodyChoice && /* @__PURE__ */ jsxs(Fragment, { children: [
10593
+ /* @__PURE__ */ jsx("button", { className: "pollar-wallet-dropdown-item", onClick: () => onCreateWallet(), disabled: creatingWallet, children: creatingWallet ? "Creating wallet\u2026" : "Create wallet" }),
10594
+ /* @__PURE__ */ jsx("div", { className: "pollar-wallet-dropdown-divider" })
10595
+ ] }),
10596
+ onCreateWallet && custodyChoice && !selfCustodyNotice && /* @__PURE__ */ jsxs(Fragment, { children: [
10597
+ /* @__PURE__ */ jsx(
10598
+ "button",
10599
+ {
10600
+ className: "pollar-wallet-dropdown-item",
10601
+ onClick: () => onCreateWallet("COSIGNED"),
10602
+ disabled: creatingWallet,
10603
+ children: creatingWallet ? "Creating wallet\u2026" : "Create co-signed wallet"
10604
+ }
10605
+ ),
10606
+ /* @__PURE__ */ jsx("div", { className: "pollar-wallet-dropdown-notice", children: "You and the app approve every signature together. Your wallet can be recovered with help." }),
10607
+ /* @__PURE__ */ jsx(
10608
+ "button",
10609
+ {
10610
+ className: "pollar-wallet-dropdown-item",
10611
+ onClick: () => setSelfCustodyNotice(true),
10612
+ disabled: creatingWallet,
10613
+ children: "Create self-custody wallet"
10614
+ }
10615
+ ),
10616
+ /* @__PURE__ */ jsx("div", { className: "pollar-wallet-dropdown-notice", children: "Only your device signs. Nobody else can move or recover your funds." }),
9537
10617
  /* @__PURE__ */ jsx("div", { className: "pollar-wallet-dropdown-divider" })
9538
10618
  ] }),
9539
- onCreateWallet && /* @__PURE__ */ jsxs(Fragment, { children: [
10619
+ onCreateWallet && custodyChoice && selfCustodyNotice && /* @__PURE__ */ jsxs(Fragment, { children: [
10620
+ /* @__PURE__ */ jsx("div", { className: "pollar-wallet-dropdown-notice", role: "alert", children: "In a self-custody wallet only your device can sign. If you lose this device and every recovery method, nobody can recover the wallet or its funds, not the app and not Pollar. Nobody can undo or freeze a transaction you sign." }),
10621
+ /* @__PURE__ */ jsxs("label", { className: "pollar-wallet-dropdown-notice", children: [
10622
+ /* @__PURE__ */ jsx(
10623
+ "input",
10624
+ {
10625
+ type: "checkbox",
10626
+ checked: risksAccepted,
10627
+ onChange: (e) => setRisksAccepted(e.target.checked),
10628
+ disabled: creatingWallet
10629
+ }
10630
+ ),
10631
+ " ",
10632
+ "I understand and accept these risks"
10633
+ ] }),
10634
+ /* @__PURE__ */ jsx(
10635
+ "button",
10636
+ {
10637
+ className: "pollar-wallet-dropdown-item",
10638
+ onClick: () => onCreateWallet("SELF_CUSTODY"),
10639
+ disabled: creatingWallet || !risksAccepted,
10640
+ children: creatingWallet ? "Creating wallet\u2026" : "Create self-custody wallet"
10641
+ }
10642
+ ),
9540
10643
  /* @__PURE__ */ jsx(
9541
10644
  "button",
9542
10645
  {
9543
10646
  className: "pollar-wallet-dropdown-item",
9544
- style: { color: itemColor },
9545
- onClick: onCreateWallet,
10647
+ onClick: () => {
10648
+ setSelfCustodyNotice(false);
10649
+ setRisksAccepted(false);
10650
+ },
9546
10651
  disabled: creatingWallet,
9547
- children: creatingWallet ? "Creating wallet\u2026" : "Create wallet"
10652
+ children: "Back"
9548
10653
  }
9549
10654
  ),
9550
10655
  /* @__PURE__ */ jsx("div", { className: "pollar-wallet-dropdown-divider" })
9551
10656
  ] }),
9552
- /* @__PURE__ */ jsx("button", { className: "pollar-wallet-dropdown-item", style: { color: itemColor }, onClick: onSessions, children: "Active sessions" }),
10657
+ /* @__PURE__ */ jsx("button", { className: "pollar-wallet-dropdown-item", onClick: onSessions, children: "Active sessions" }),
9553
10658
  /* @__PURE__ */ jsx("div", { className: "pollar-wallet-dropdown-divider" }),
9554
10659
  /* @__PURE__ */ jsx("button", { className: "pollar-wallet-dropdown-item pollar-danger", onClick: onLogout, children: "Log out" })
9555
10660
  ] })
9556
10661
  ] });
9557
10662
  }
9558
10663
  if (!walletAddress) {
9559
- return /* @__PURE__ */ jsxs("button", { type: "button", className: "pollar-wallet-login-btn", style: { backgroundColor: accentColor }, onClick: onLogin, children: [
9560
- /* @__PURE__ */ jsx(ButtonLogo, {}),
10664
+ return /* @__PURE__ */ jsxs("button", { type: "button", className: "pollar-wallet-login-btn", style: cssVars, onClick: onLogin, children: [
10665
+ /* @__PURE__ */ jsx(ButtonLogo, { accentColor }),
9561
10666
  "Login with Pollar"
9562
10667
  ] });
9563
10668
  }
9564
- return /* @__PURE__ */ jsxs("div", { className: "pollar-wallet-wrapper", ref: wrapperRef, children: [
10669
+ return /* @__PURE__ */ jsxs("div", { className: "pollar-wallet-wrapper", ref: wrapperRef, style: cssVars, children: [
9565
10670
  /* @__PURE__ */ jsxs(
9566
10671
  "button",
9567
10672
  {
9568
10673
  className: "pollar-wallet-btn",
9569
- style: { backgroundColor: accentColor },
9570
10674
  onClick: isInProgress ? void 0 : onToggleOpen,
9571
10675
  disabled: isInProgress,
9572
10676
  "aria-busy": isInProgress,
@@ -9603,46 +10707,41 @@ function WalletButtonTemplate({
9603
10707
  ]
9604
10708
  }
9605
10709
  ),
9606
- open && /* @__PURE__ */ jsxs("div", { className: "pollar-wallet-dropdown", style: { backgroundColor: dropdownBg, borderColor: dropdownBorder }, children: [
10710
+ open && /* @__PURE__ */ jsxs("div", { className: "pollar-wallet-dropdown", children: [
10711
+ recoveryLocked && /* @__PURE__ */ jsxs(Fragment, { children: [
10712
+ /* @__PURE__ */ jsx("div", { className: "pollar-wallet-dropdown-notice pollar-danger", role: "alert", children: "Locked: add a way to recover this wallet before sending or receiving. If you lose this device without one, the funds are lost." }),
10713
+ /* @__PURE__ */ jsx("div", { className: "pollar-wallet-dropdown-divider" })
10714
+ ] }),
9607
10715
  notReadyReason && /* @__PURE__ */ jsxs(Fragment, { children: [
9608
- /* @__PURE__ */ jsx("div", { className: "pollar-wallet-dropdown-notice", style: { color: itemColor }, role: "status", children: notReadyReason }),
10716
+ /* @__PURE__ */ jsx("div", { className: "pollar-wallet-dropdown-notice", role: "status", children: notReadyReason }),
9609
10717
  /* @__PURE__ */ jsx("div", { className: "pollar-wallet-dropdown-divider" })
9610
10718
  ] }),
9611
10719
  showCreateAccount && /* @__PURE__ */ jsxs(Fragment, { children: [
9612
- /* @__PURE__ */ jsxs(
9613
- "button",
9614
- {
9615
- className: "pollar-wallet-dropdown-item",
9616
- style: { color: itemColor },
9617
- onClick: onCreateAccount,
9618
- disabled: creatingAccount,
9619
- children: [
9620
- /* @__PURE__ */ jsxs(
9621
- "svg",
9622
- {
9623
- width: "14",
9624
- height: "14",
9625
- viewBox: "0 0 24 24",
9626
- fill: "none",
9627
- stroke: "currentColor",
9628
- strokeWidth: "2",
9629
- strokeLinecap: "round",
9630
- strokeLinejoin: "round",
9631
- children: [
9632
- /* @__PURE__ */ jsx("path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }),
9633
- /* @__PURE__ */ jsx("circle", { cx: "9", cy: "7", r: "4" }),
9634
- /* @__PURE__ */ jsx("line", { x1: "19", y1: "8", x2: "19", y2: "14" }),
9635
- /* @__PURE__ */ jsx("line", { x1: "22", y1: "11", x2: "16", y2: "11" })
9636
- ]
9637
- }
9638
- ),
9639
- creatingAccount ? "Creating account\u2026" : "Create account"
9640
- ]
9641
- }
9642
- ),
10720
+ /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-dropdown-item", onClick: onCreateAccount, disabled: creatingAccount, children: [
10721
+ /* @__PURE__ */ jsxs(
10722
+ "svg",
10723
+ {
10724
+ width: "14",
10725
+ height: "14",
10726
+ viewBox: "0 0 24 24",
10727
+ fill: "none",
10728
+ stroke: "currentColor",
10729
+ strokeWidth: "2",
10730
+ strokeLinecap: "round",
10731
+ strokeLinejoin: "round",
10732
+ children: [
10733
+ /* @__PURE__ */ jsx("path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }),
10734
+ /* @__PURE__ */ jsx("circle", { cx: "9", cy: "7", r: "4" }),
10735
+ /* @__PURE__ */ jsx("line", { x1: "19", y1: "8", x2: "19", y2: "14" }),
10736
+ /* @__PURE__ */ jsx("line", { x1: "22", y1: "11", x2: "16", y2: "11" })
10737
+ ]
10738
+ }
10739
+ ),
10740
+ creatingAccount ? "Creating account\u2026" : "Create account"
10741
+ ] }),
9643
10742
  /* @__PURE__ */ jsx("div", { className: "pollar-wallet-dropdown-divider" })
9644
10743
  ] }),
9645
- /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-dropdown-item", style: { color: itemColor }, onClick: onSend, children: [
10744
+ /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-dropdown-item", onClick: onSend, children: [
9646
10745
  /* @__PURE__ */ jsxs(
9647
10746
  "svg",
9648
10747
  {
@@ -9662,7 +10761,7 @@ function WalletButtonTemplate({
9662
10761
  ),
9663
10762
  "Send"
9664
10763
  ] }),
9665
- /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-dropdown-item", style: { color: itemColor }, onClick: onReceive, children: [
10764
+ /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-dropdown-item", onClick: onReceive, children: [
9666
10765
  /* @__PURE__ */ jsxs(
9667
10766
  "svg",
9668
10767
  {
@@ -9684,7 +10783,7 @@ function WalletButtonTemplate({
9684
10783
  "Receive"
9685
10784
  ] }),
9686
10785
  /* @__PURE__ */ jsx("div", { className: "pollar-wallet-dropdown-divider" }),
9687
- /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-dropdown-item", style: { color: itemColor }, onClick: onCopy, children: [
10786
+ /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-dropdown-item", onClick: onCopy, children: [
9688
10787
  /* @__PURE__ */ jsxs(
9689
10788
  "svg",
9690
10789
  {
@@ -9704,7 +10803,7 @@ function WalletButtonTemplate({
9704
10803
  ),
9705
10804
  copied ? "Copied!" : "Copy address"
9706
10805
  ] }),
9707
- /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-dropdown-item", style: { color: itemColor }, onClick: onWalletBalance, children: [
10806
+ /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-dropdown-item", onClick: onWalletBalance, children: [
9708
10807
  /* @__PURE__ */ jsxs(
9709
10808
  "svg",
9710
10809
  {
@@ -9725,7 +10824,7 @@ function WalletButtonTemplate({
9725
10824
  ),
9726
10825
  "Wallet balance"
9727
10826
  ] }),
9728
- /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-dropdown-item", style: { color: itemColor }, onClick: onTxHistory, children: [
10827
+ /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-dropdown-item", onClick: onTxHistory, children: [
9729
10828
  /* @__PURE__ */ jsxs(
9730
10829
  "svg",
9731
10830
  {
@@ -9749,7 +10848,7 @@ function WalletButtonTemplate({
9749
10848
  "Transaction history"
9750
10849
  ] }),
9751
10850
  /* @__PURE__ */ jsx("div", { className: "pollar-wallet-dropdown-divider" }),
9752
- /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-dropdown-item", style: { color: itemColor }, onClick: onRamp, children: [
10851
+ /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-dropdown-item", onClick: onRamp, children: [
9753
10852
  /* @__PURE__ */ jsxs(
9754
10853
  "svg",
9755
10854
  {
@@ -9771,7 +10870,7 @@ function WalletButtonTemplate({
9771
10870
  ),
9772
10871
  "Buy / Sell"
9773
10872
  ] }),
9774
- /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-dropdown-item", style: { color: itemColor }, onClick: onKyc, children: [
10873
+ /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-dropdown-item", onClick: onKyc, children: [
9775
10874
  /* @__PURE__ */ jsxs(
9776
10875
  "svg",
9777
10876
  {
@@ -9792,7 +10891,7 @@ function WalletButtonTemplate({
9792
10891
  ),
9793
10892
  "Identity verification"
9794
10893
  ] }),
9795
- /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-dropdown-item", style: { color: itemColor }, onClick: onDistributionRules, children: [
10894
+ /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-dropdown-item", onClick: onDistributionRules, children: [
9796
10895
  /* @__PURE__ */ jsxs(
9797
10896
  "svg",
9798
10897
  {
@@ -9815,7 +10914,7 @@ function WalletButtonTemplate({
9815
10914
  ),
9816
10915
  "Distribution rules"
9817
10916
  ] }),
9818
- /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-dropdown-item", style: { color: itemColor }, onClick: onSessions, children: [
10917
+ /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-dropdown-item", onClick: onSessions, children: [
9819
10918
  /* @__PURE__ */ jsxs(
9820
10919
  "svg",
9821
10920
  {
@@ -9836,6 +10935,23 @@ function WalletButtonTemplate({
9836
10935
  ),
9837
10936
  "Active sessions"
9838
10937
  ] }),
10938
+ onRecovery && /* @__PURE__ */ jsxs("button", { className: `pollar-wallet-dropdown-item${recoveryLocked ? " pollar-danger" : ""}`, onClick: onRecovery, children: [
10939
+ /* @__PURE__ */ jsx(
10940
+ "svg",
10941
+ {
10942
+ width: "14",
10943
+ height: "14",
10944
+ viewBox: "0 0 24 24",
10945
+ fill: "none",
10946
+ stroke: "currentColor",
10947
+ strokeWidth: "2",
10948
+ strokeLinecap: "round",
10949
+ strokeLinejoin: "round",
10950
+ children: /* @__PURE__ */ jsx("path", { d: "M12 2 4 5v6c0 5 3.4 9.3 8 11 4.6-1.7 8-6 8-11V5l-8-3z" })
10951
+ }
10952
+ ),
10953
+ recoveryLocked ? "Set up wallet recovery" : "Wallet recovery"
10954
+ ] }),
9839
10955
  /* @__PURE__ */ jsx("div", { className: "pollar-wallet-dropdown-divider" }),
9840
10956
  /* @__PURE__ */ jsxs("button", { className: "pollar-wallet-dropdown-item pollar-danger", onClick: onLogout, children: [
9841
10957
  /* @__PURE__ */ jsxs(
@@ -9878,7 +10994,10 @@ function WalletButton() {
9878
10994
  tx: transaction,
9879
10995
  isAuthenticated,
9880
10996
  createWallet,
9881
- appConfig
10997
+ appConfig,
10998
+ recovery,
10999
+ recoveryLocked,
11000
+ openRecoveryModal
9882
11001
  } = usePollar();
9883
11002
  const { primaryChain, primaryAddress } = useChains();
9884
11003
  const walletAddress = primaryAddress || (wallet?.address ?? "");
@@ -9893,11 +11012,7 @@ function WalletButton() {
9893
11012
  const isInProgress = transaction.step === "building" || transaction.step === "signing";
9894
11013
  const canCreateAccount = !created && wallet?.custody === "external" && wallet.existsOnStellar === false && wallet.fundingMode === "IMMEDIATE";
9895
11014
  const notReadyReason = walletNotReadyReason(wallet, primaryChain);
9896
- const { theme = "light", accentColor = "#005DB4" } = styles;
9897
- const isDark = theme === "dark";
9898
- const dropdownBg = isDark ? "#18181b" : "#fff";
9899
- const dropdownBorder = isDark ? "#3f3f46" : "#e4e4e7";
9900
- const itemColor = isDark ? "#fafafa" : "#18181b";
11015
+ const { theme, accentColor, styleOverrides } = modalChrome(styles);
9901
11016
  useEffect(() => {
9902
11017
  function handleClickOutside(e) {
9903
11018
  if (wrapperRef.current && !wrapperRef.current.contains(e.target)) {
@@ -9941,12 +11056,12 @@ function WalletButton() {
9941
11056
  setCreating(false);
9942
11057
  }
9943
11058
  }
9944
- async function handleCreateWallet() {
11059
+ async function handleCreateWallet(custody) {
9945
11060
  if (creatingWallet) return;
9946
11061
  setCreatingWallet(true);
9947
11062
  setCreateWalletError(null);
9948
11063
  try {
9949
- await createWallet();
11064
+ await createWallet(custody ? { custody } : void 0);
9950
11065
  setOpen(false);
9951
11066
  } catch (err) {
9952
11067
  setCreateWalletError(
@@ -9976,6 +11091,10 @@ function WalletButton() {
9976
11091
  setOpen(false);
9977
11092
  openSessionsModal();
9978
11093
  }
11094
+ function handleRecovery() {
11095
+ setOpen(false);
11096
+ openRecoveryModal();
11097
+ }
9979
11098
  function handleKyc() {
9980
11099
  setOpen(false);
9981
11100
  openKycModal();
@@ -9992,18 +11111,18 @@ function WalletButton() {
9992
11111
  WalletButtonTemplate,
9993
11112
  {
9994
11113
  walletAddress: walletAddress ?? null,
11114
+ theme,
9995
11115
  accentColor,
11116
+ styleOverrides,
9996
11117
  open,
9997
11118
  copied,
9998
- dropdownBg,
9999
- dropdownBorder,
10000
- itemColor,
10001
11119
  wrapperRef,
10002
11120
  isInProgress,
10003
11121
  showCreateAccount: canCreateAccount,
10004
11122
  signedIn: isAuthenticated,
10005
11123
  creatingWallet,
10006
11124
  createWalletError,
11125
+ custodyChoice: appConfig.application?.walletCustodyMode === "USER_CHOICE",
10007
11126
  ...appConfig.application?.walletProvisioning === "NONE" ? {} : { onCreateWallet: handleCreateWallet },
10008
11127
  notReadyReason,
10009
11128
  creatingAccount: creating,
@@ -10015,6 +11134,8 @@ function WalletButton() {
10015
11134
  onSend: handleSend,
10016
11135
  onReceive: handleReceive,
10017
11136
  onSessions: handleSessions,
11137
+ recoveryLocked,
11138
+ ...recovery?.required ? { onRecovery: handleRecovery } : {},
10018
11139
  onKyc: handleKyc,
10019
11140
  onRamp: handleRamp,
10020
11141
  onDistributionRules: handleDistributionRules,
@@ -10024,6 +11145,6 @@ function WalletButton() {
10024
11145
  );
10025
11146
  }
10026
11147
 
10027
- export { CardModal, ChainSelect, CosignModal, CosignModalTemplate, DistributionRulesModal, DistributionRulesModalTemplate, EarnModal, EnabledAssetsModal, EnabledAssetsModalTemplate, KycModal, KycModalTemplate, KycStatus, LoginModalTemplate, PollarProvider, ProviderRegistrationModal, RampWidget, RampWidgetTemplate, ReceiveModal, ReceiveModalTemplate, RegistryCheckModal, RequirementFormModal, RouteDisplay, SendModal, SendModalTemplate, SessionsModal, SessionsModalTemplate, SwapModal, SwapModalTemplate, TransactionModalTemplate, TxHistoryModalTemplate, TxStatusView, WalletBalanceModal, WalletBalanceModalTemplate, WalletButton, WalletButtonTemplate, addressForChain, browserPasskeyCeremony, browserPasskeySigner, buildModalCssVars, chainsOf, createPollarAdapterHook, modalChrome, readableTextOn, resolveChain, useChains, useCosign, usePollar, walletNotReadyReason };
11148
+ export { CardModal, ChainSelect, CosignModal, CosignModalTemplate, CustodyMigrationModal, CustodyMigrationModalTemplate, DistributionRulesModal, DistributionRulesModalTemplate, EarnModal, EnabledAssetsModal, EnabledAssetsModalTemplate, KycModal, KycModalTemplate, KycStatus, LoginModalTemplate, PollarProvider, ProviderRegistrationModal, RampWidget, RampWidgetTemplate, ReceiveModal, ReceiveModalTemplate, RecoveryModal, RecoveryModalTemplate, RegistryCheckModal, RejectedRouteDisplay, RequirementFormModal, RouteDisplay, SendModal, SendModalTemplate, SessionsModal, SessionsModalTemplate, SwapModal, SwapModalTemplate, TransactionModalTemplate, TxHistoryModalTemplate, TxStatusView, WalletBalanceModal, WalletBalanceModalTemplate, WalletButton, WalletButtonTemplate, addressForChain, browserPasskeyCeremony, browserPasskeySigner, browserRecoveryPasskey, buildModalCssVars, chainsOf, createPollarAdapterHook, modalChrome, readableTextOn, resolveChain, useChains, useCosign, usePollar, walletNotReadyReason };
10028
11149
  //# sourceMappingURL=index.mjs.map
10029
11150
  //# sourceMappingURL=index.mjs.map