@pollar/core 0.7.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -26,10 +26,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
 
27
27
  // ../../node_modules/@stellar/freighter-api/build/index.min.js
28
28
  var require_index_min = __commonJS({
29
- "../../node_modules/@stellar/freighter-api/build/index.min.js"(exports$1, module) {
29
+ "../../node_modules/@stellar/freighter-api/build/index.min.js"(exports, module) {
30
30
  !(function(e, r) {
31
- "object" == typeof exports$1 && "object" == typeof module ? module.exports = r() : "function" == typeof define && define.amd ? define([], r) : "object" == typeof exports$1 ? exports$1.freighterApi = r() : e.freighterApi = r();
32
- })(exports$1, (() => (() => {
31
+ "object" == typeof exports && "object" == typeof module ? module.exports = r() : "function" == typeof define && define.amd ? define([], r) : "object" == typeof exports ? exports.freighterApi = r() : e.freighterApi = r();
32
+ })(exports, (() => (() => {
33
33
  var e, r, E = { d: (e2, r2) => {
34
34
  for (var o2 in r2) E.o(r2, o2) && !E.o(e2, o2) && Object.defineProperty(e2, o2, { enumerable: true, get: r2[o2] });
35
35
  }, o: (e2, r2) => Object.prototype.hasOwnProperty.call(e2, r2), r: (e2) => {
@@ -323,9 +323,7 @@ var WebCryptoKeyManager = class {
323
323
  */
324
324
  this._initPromise = null;
325
325
  if (typeof globalThis.crypto === "undefined" || !globalThis.crypto.subtle) {
326
- throw new Error(
327
- "[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost)."
328
- );
326
+ throw new Error("[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost).");
329
327
  }
330
328
  this.apiKey = apiKey;
331
329
  }
@@ -898,14 +896,18 @@ function createApiClient(baseUrl) {
898
896
  async function listDistributionRules(api) {
899
897
  const { data, error } = await api.GET("/distribution/rules");
900
898
  if (!data?.content || error) {
901
- throw new Error(error?.error ?? "Failed to list distribution rules");
899
+ throw new Error(
900
+ error?.code ?? error?.error ?? "Failed to list distribution rules"
901
+ );
902
902
  }
903
903
  return data.content.rules;
904
904
  }
905
905
  async function claimDistributionRule(api, body) {
906
906
  const { data, error } = await api.POST("/distribution/claim", { body });
907
907
  if (!data?.content || error) {
908
- throw new Error(error?.error ?? "Failed to claim distribution rule");
908
+ throw new Error(
909
+ error?.code ?? error?.error ?? "Failed to claim distribution rule"
910
+ );
909
911
  }
910
912
  return data.content;
911
913
  }
@@ -916,18 +918,18 @@ async function getKycStatus(api, providerId) {
916
918
  params: { query: providerId ? { providerId } : {} }
917
919
  });
918
920
  if (!data?.content || error) {
919
- throw new Error(error?.error ?? "Failed to get KYC status");
921
+ throw new Error(error?.code ?? error?.error ?? "Failed to get KYC status");
920
922
  }
921
923
  return data.content;
922
924
  }
923
925
  async function getKycProviders(api, country) {
924
926
  const { data, error } = await api.GET("/kyc/providers", { params: { query: { country } } });
925
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to get KYC providers");
927
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get KYC providers");
926
928
  return data.content;
927
929
  }
928
930
  async function startKyc(api, body) {
929
931
  const { data, error } = await api.POST("/kyc/start", { body });
930
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to start KYC");
932
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to start KYC");
931
933
  return data.content;
932
934
  }
933
935
  async function resolveKyc(api, providerId, level = "basic") {
@@ -949,22 +951,22 @@ async function pollKycStatus(api, providerId, { intervalMs = 3e3, timeoutMs = 3e
949
951
  // src/api/endpoints/ramps.ts
950
952
  async function getRampsQuote(api, query) {
951
953
  const { data, error } = await api.GET("/ramps/quote", { params: { query } });
952
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to get ramp quotes");
954
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get ramp quotes");
953
955
  return data.content;
954
956
  }
955
957
  async function createOnRamp(api, body) {
956
958
  const { data, error } = await api.POST("/ramps/onramp", { body });
957
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to create onramp");
959
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to create onramp");
958
960
  return data.content;
959
961
  }
960
962
  async function createOffRamp(api, body) {
961
963
  const { data, error } = await api.POST("/ramps/offramp", { body });
962
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to create offramp");
964
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to create offramp");
963
965
  return data.content;
964
966
  }
965
967
  async function getRampTransaction(api, txId) {
966
968
  const { data, error } = await api.GET("/ramps/transaction/{txId}", { params: { path: { txId } } });
967
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to get transaction");
969
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get transaction");
968
970
  return data.content;
969
971
  }
970
972
  async function pollRampTransaction(api, txId, { intervalMs = 5e3, timeoutMs = 6e5 } = {}) {
@@ -977,6 +979,26 @@ async function pollRampTransaction(api, txId, { intervalMs = 5e3, timeoutMs = 6e
977
979
  throw new Error("Ramp transaction polling timed out");
978
980
  }
979
981
 
982
+ // src/lib/random-uuid.ts
983
+ function randomUUID() {
984
+ const c = globalThis.crypto;
985
+ if (c && typeof c.randomUUID === "function") {
986
+ return c.randomUUID();
987
+ }
988
+ if (c && typeof c.getRandomValues === "function") {
989
+ const bytes = new Uint8Array(16);
990
+ c.getRandomValues(bytes);
991
+ bytes[6] = bytes[6] & 15 | 64;
992
+ bytes[8] = bytes[8] & 63 | 128;
993
+ const hex = [];
994
+ for (let i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, "0"));
995
+ return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
996
+ }
997
+ throw new Error(
998
+ "[PollarClient] No secure random source available (crypto.randomUUID / crypto.getRandomValues). DPoP requires a secure context (HTTPS) or, in React Native, the `react-native-get-random-values` polyfill."
999
+ );
1000
+ }
1001
+
980
1002
  // src/dpop.ts
981
1003
  async function buildProof(args, keyManager) {
982
1004
  const jwk = await keyManager.getPublicJwk();
@@ -986,7 +1008,7 @@ async function buildProof(args, keyManager) {
986
1008
  jwk
987
1009
  };
988
1010
  const payload = {
989
- jti: generateJti(),
1011
+ jti: randomUUID(),
990
1012
  htm: args.htm.toUpperCase(),
991
1013
  htu: normalizeHtu(args.htu),
992
1014
  iat: Math.floor(Date.now() / 1e3)
@@ -1020,52 +1042,6 @@ function normalizeHtu(rawUrl) {
1020
1042
  const portPart = port ? `:${port}` : "";
1021
1043
  return `${scheme}//${host}${portPart}${url.pathname}`;
1022
1044
  }
1023
- function generateJti() {
1024
- const c = globalThis.crypto;
1025
- if (c && typeof c.randomUUID === "function") {
1026
- return c.randomUUID();
1027
- }
1028
- if (c && typeof c.getRandomValues === "function") {
1029
- const bytes = new Uint8Array(16);
1030
- c.getRandomValues(bytes);
1031
- bytes[6] = bytes[6] & 15 | 64;
1032
- bytes[8] = bytes[8] & 63 | 128;
1033
- const hex = [];
1034
- for (let i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, "0"));
1035
- return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
1036
- }
1037
- throw new Error(
1038
- "[PollarClient:dpop] No secure random source available (crypto.randomUUID / crypto.getRandomValues). DPoP requires a secure context (HTTPS) or, in React Native, the `react-native-get-random-values` polyfill."
1039
- );
1040
- }
1041
-
1042
- // src/stellar/StellarClient.ts
1043
- var HORIZON_URLS = {
1044
- mainnet: "https://horizon.stellar.org",
1045
- testnet: "https://horizon-testnet.stellar.org"
1046
- };
1047
- var StellarClient = class {
1048
- constructor(config) {
1049
- this.horizonUrl = typeof config === "string" ? HORIZON_URLS[config] : config.horizonUrl;
1050
- }
1051
- async submitTransaction(signedXdr) {
1052
- try {
1053
- const response = await fetch(`${this.horizonUrl}/transactions`, {
1054
- method: "POST",
1055
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
1056
- body: new URLSearchParams({ tx: signedXdr })
1057
- });
1058
- if (!response.ok) {
1059
- const body = await response.json().catch(() => ({}));
1060
- return { success: false, errorCode: body.extras?.result_codes?.transaction ?? "HORIZON_ERROR" };
1061
- }
1062
- const data = await response.json();
1063
- return { success: true, hash: data.hash };
1064
- } catch {
1065
- return { success: false, errorCode: "NETWORK_ERROR" };
1066
- }
1067
- }
1068
- };
1069
1045
 
1070
1046
  // src/storage/web.ts
1071
1047
  var LOG_PREFIX = "[PollarClient:storage]";
@@ -1154,9 +1130,62 @@ function defaultStorage(options = {}) {
1154
1130
  return createLocalStorageAdapter(options);
1155
1131
  }
1156
1132
 
1133
+ // src/visibility/noop.ts
1134
+ function createNoopVisibilityProvider() {
1135
+ return {
1136
+ isVisible: () => true,
1137
+ onChange: () => () => {
1138
+ }
1139
+ };
1140
+ }
1141
+
1142
+ // src/visibility/web.ts
1143
+ function createWebVisibilityProvider() {
1144
+ const isVisibleNow = () => typeof document === "undefined" || document.visibilityState === "visible";
1145
+ return {
1146
+ isVisible: isVisibleNow,
1147
+ onChange: (cb) => {
1148
+ if (typeof window === "undefined" || typeof document === "undefined") {
1149
+ return () => {
1150
+ };
1151
+ }
1152
+ let last = isVisibleNow();
1153
+ const handler = () => {
1154
+ const next = isVisibleNow();
1155
+ if (next !== last) {
1156
+ last = next;
1157
+ cb(next);
1158
+ }
1159
+ };
1160
+ document.addEventListener("visibilitychange", handler);
1161
+ window.addEventListener("pageshow", handler);
1162
+ window.addEventListener("pagehide", handler);
1163
+ window.addEventListener("focus", handler);
1164
+ window.addEventListener("blur", handler);
1165
+ return () => {
1166
+ document.removeEventListener("visibilitychange", handler);
1167
+ window.removeEventListener("pageshow", handler);
1168
+ window.removeEventListener("pagehide", handler);
1169
+ window.removeEventListener("focus", handler);
1170
+ window.removeEventListener("blur", handler);
1171
+ };
1172
+ }
1173
+ };
1174
+ }
1175
+
1176
+ // src/visibility/autodetect.ts
1177
+ function defaultVisibilityProvider() {
1178
+ if (typeof document !== "undefined" && typeof window !== "undefined") {
1179
+ return createWebVisibilityProvider();
1180
+ }
1181
+ return createNoopVisibilityProvider();
1182
+ }
1183
+
1157
1184
  // src/types.ts
1158
1185
  var AUTH_ERROR_CODES = {
1159
1186
  SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
1187
+ SESSION_EXPIRED: "SESSION_EXPIRED",
1188
+ SESSION_INVALID: "SESSION_INVALID",
1160
1189
  EMAIL_SEND_FAILED: "EMAIL_SEND_FAILED",
1161
1190
  EMAIL_VERIFY_FAILED: "EMAIL_VERIFY_FAILED",
1162
1191
  EMAIL_CODE_EXPIRED: "EMAIL_CODE_EXPIRED",
@@ -1164,6 +1193,7 @@ var AUTH_ERROR_CODES = {
1164
1193
  AUTH_FAILED: "AUTH_FAILED",
1165
1194
  WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
1166
1195
  WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
1196
+ WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
1167
1197
  UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
1168
1198
  };
1169
1199
  var PollarFlowError = class extends Error {
@@ -1469,7 +1499,32 @@ async function readWalletType(storage, apiKeyHash) {
1469
1499
  return storage.get(walletTypeStorageKey(apiKeyHash));
1470
1500
  }
1471
1501
 
1502
+ // src/lib/abort.ts
1503
+ function abortError() {
1504
+ if (typeof DOMException !== "undefined") {
1505
+ return new DOMException("Aborted", "AbortError");
1506
+ }
1507
+ const err = new Error("Aborted");
1508
+ err.name = "AbortError";
1509
+ return err;
1510
+ }
1511
+ function throwIfAborted(signal) {
1512
+ if (signal?.aborted) throw abortError();
1513
+ }
1514
+
1472
1515
  // src/client/stream.ts
1516
+ var SessionStatusError = class extends Error {
1517
+ constructor(code) {
1518
+ super(`[PollarClient] Session status terminal: ${code}`);
1519
+ this.code = code;
1520
+ this.name = "SessionStatusError";
1521
+ }
1522
+ };
1523
+ function terminalStatusCode(parsed) {
1524
+ const err = parsed?.error;
1525
+ if (err === "INVALID_CLIENT_SESSION_ID" || err === "EXPIRED_CLIENT_ID") return err;
1526
+ return null;
1527
+ }
1473
1528
  function abortableDelay(ms, signal) {
1474
1529
  return new Promise((resolve, reject) => {
1475
1530
  const t = setTimeout(resolve, ms);
@@ -1477,7 +1532,7 @@ function abortableDelay(ms, signal) {
1477
1532
  "abort",
1478
1533
  () => {
1479
1534
  clearTimeout(t);
1480
- reject(new DOMException("Aborted", "AbortError"));
1535
+ reject(abortError());
1481
1536
  },
1482
1537
  { once: true }
1483
1538
  );
@@ -1492,7 +1547,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1492
1547
  else await new Promise((r) => setTimeout(r, ms));
1493
1548
  };
1494
1549
  while (true) {
1495
- signal?.throwIfAborted();
1550
+ throwIfAborted(signal);
1496
1551
  let data, error;
1497
1552
  try {
1498
1553
  ({ data, error } = await api.GET("/auth/session/status/{clientSessionId}", {
@@ -1515,7 +1570,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1515
1570
  let sawAnyChunk = false;
1516
1571
  try {
1517
1572
  while (true) {
1518
- signal?.throwIfAborted();
1573
+ throwIfAborted(signal);
1519
1574
  const { done, value } = await reader.read();
1520
1575
  if (done) {
1521
1576
  streamDone = true;
@@ -1526,17 +1581,22 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1526
1581
  for (const message of chunk.split("\n\n").filter(Boolean)) {
1527
1582
  const dataLine = message.split("\n").find((l) => l.startsWith("data:"));
1528
1583
  if (!dataLine) continue;
1584
+ let parsed;
1529
1585
  try {
1530
- const parsed = JSON.parse(dataLine.slice("data:".length).trim());
1531
- if (check(parsed)) {
1532
- return parsed;
1533
- }
1586
+ parsed = JSON.parse(dataLine.slice("data:".length).trim());
1534
1587
  } catch {
1588
+ continue;
1589
+ }
1590
+ const terminal = terminalStatusCode(parsed);
1591
+ if (terminal) throw new SessionStatusError(terminal);
1592
+ if (check(parsed)) {
1593
+ return parsed;
1535
1594
  }
1536
1595
  }
1537
1596
  }
1538
1597
  } catch (e) {
1539
1598
  if (e instanceof Error && e.name === "AbortError") throw e;
1599
+ if (e instanceof SessionStatusError) throw e;
1540
1600
  console.warn(e);
1541
1601
  } finally {
1542
1602
  reader.releaseLock();
@@ -1547,14 +1607,74 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1547
1607
  if (delay) await sleep(delay);
1548
1608
  }
1549
1609
  }
1610
+ async function pollUntilFound(baseUrl, clientSessionId, check, intervalMs = 500, signal) {
1611
+ const url = `${baseUrl}/auth/session/status/${encodeURIComponent(clientSessionId)}/poll`;
1612
+ let backoff = intervalMs;
1613
+ const sleep = async (ms) => {
1614
+ if (ms <= 0) return;
1615
+ if (signal) await abortableDelay(ms, signal);
1616
+ else await new Promise((r) => setTimeout(r, ms));
1617
+ };
1618
+ while (true) {
1619
+ throwIfAborted(signal);
1620
+ let envelope = null;
1621
+ let httpStatus = 0;
1622
+ try {
1623
+ const response = await fetch(url, { headers: { accept: "application/json" }, signal: signal ?? null });
1624
+ httpStatus = response.status;
1625
+ envelope = await response.json().catch(() => null);
1626
+ } catch (e) {
1627
+ if (e instanceof Error && e.name === "AbortError") throw e;
1628
+ console.warn(e);
1629
+ }
1630
+ if (httpStatus === 404 || envelope?.code === "INVALID_CLIENT_SESSION_ID") {
1631
+ throw new SessionStatusError("INVALID_CLIENT_SESSION_ID");
1632
+ }
1633
+ if (httpStatus === 410 || envelope?.code === "EXPIRED_CLIENT_ID") {
1634
+ throw new SessionStatusError("EXPIRED_CLIENT_ID");
1635
+ }
1636
+ if (envelope?.success && envelope.content && check(envelope.content)) {
1637
+ return envelope.content;
1638
+ }
1639
+ if (envelope) backoff = intervalMs;
1640
+ else backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
1641
+ await sleep(backoff);
1642
+ }
1643
+ }
1644
+ function waitForSessionReady(args) {
1645
+ const { api, baseUrl, clientSessionId, check, useStreaming, retryDelayMs, signal } = args;
1646
+ return useStreaming ? streamUntilFound(api, clientSessionId, check, retryDelayMs ?? 200, signal) : pollUntilFound(baseUrl, clientSessionId, check, retryDelayMs ?? 500, signal);
1647
+ }
1550
1648
 
1551
1649
  // src/client/auth/authenticate.ts
1552
1650
  async function authenticate(clientSessionId, deps, expectedWallet) {
1553
- const { api, signal, setAuthState, storeSession, clearSession } = deps;
1651
+ const { api, basePath, useStreaming, signal, setAuthState, storeSession, clearSession } = deps;
1554
1652
  setAuthState({ step: "authenticating" });
1555
- await streamUntilFound(api, clientSessionId, (data2) => data2?.status === "READY", 200, signal);
1653
+ try {
1654
+ await waitForSessionReady({
1655
+ api,
1656
+ baseUrl: basePath,
1657
+ clientSessionId,
1658
+ check: (data2) => data2?.status === "READY",
1659
+ useStreaming,
1660
+ signal
1661
+ });
1662
+ } catch (err) {
1663
+ if (err instanceof SessionStatusError) {
1664
+ const expired = err.code === "EXPIRED_CLIENT_ID";
1665
+ setAuthState({
1666
+ step: "error",
1667
+ previousStep: "authenticating",
1668
+ message: expired ? "Login session expired \u2014 please try again" : "Login session is no longer valid \u2014 please try again",
1669
+ errorCode: expired ? AUTH_ERROR_CODES.SESSION_EXPIRED : AUTH_ERROR_CODES.SESSION_INVALID
1670
+ });
1671
+ await clearSession();
1672
+ return;
1673
+ }
1674
+ throw err;
1675
+ }
1556
1676
  const dpopJwk = await deps.getPublicJwk();
1557
- const { data, error } = await api.POST("/auth/login", {
1677
+ const { data } = await api.POST("/auth/login", {
1558
1678
  body: {
1559
1679
  clientSessionId,
1560
1680
  dpopJwk,
@@ -1676,26 +1796,36 @@ function severOpener(popup) {
1676
1796
  } catch {
1677
1797
  }
1678
1798
  }
1679
- async function loginOAuth(provider, deps) {
1680
- const { setAuthState, basePath, apiKey } = deps;
1681
- const popup = window.open("about:blank", "_blank");
1799
+ var defaultWebOAuthOpener = async ({ getUrl }) => {
1800
+ const popup = typeof window !== "undefined" ? window.open("about:blank", "_blank") : null;
1682
1801
  severOpener(popup);
1683
- const clientSessionId = await createAuthSession(deps);
1684
- if (!clientSessionId) {
1802
+ const url = await getUrl();
1803
+ if (!url) {
1685
1804
  popup?.close();
1686
1805
  return;
1687
1806
  }
1688
- setAuthState({ step: "opening_oauth", provider });
1689
- const url = new URL(`${basePath}/auth/${provider}`);
1690
- url.searchParams.set("api_key", apiKey);
1691
- url.searchParams.set("client_session_id", clientSessionId);
1692
- url.searchParams.set("redirect_uri", window.location.origin);
1693
1807
  if (popup) {
1694
- popup.location.href = url.toString();
1808
+ popup.location.href = url;
1695
1809
  severOpener(popup);
1696
- } else {
1697
- window.open(url.toString(), "_blank", "noopener,noreferrer");
1810
+ } else if (typeof window !== "undefined") {
1811
+ window.open(url, "_blank", "noopener,noreferrer");
1698
1812
  }
1813
+ };
1814
+ async function loginOAuth(provider, deps) {
1815
+ const { setAuthState, basePath, apiKey, openAuthUrl, redirectUri, signal } = deps;
1816
+ let clientSessionId = null;
1817
+ const getUrl = async () => {
1818
+ clientSessionId = await createAuthSession(deps);
1819
+ if (!clientSessionId) return null;
1820
+ setAuthState({ step: "opening_oauth", provider });
1821
+ const url = new URL(`${basePath}/auth/${provider}`);
1822
+ url.searchParams.set("api_key", apiKey);
1823
+ url.searchParams.set("client_session_id", clientSessionId);
1824
+ url.searchParams.set("redirect_uri", redirectUri);
1825
+ return url.toString();
1826
+ };
1827
+ await openAuthUrl({ provider, getUrl, redirectUri, signal });
1828
+ if (!clientSessionId) return;
1699
1829
  await authenticate(clientSessionId, deps);
1700
1830
  }
1701
1831
 
@@ -1705,10 +1835,10 @@ function withSignal(promise, signal) {
1705
1835
  promise,
1706
1836
  new Promise((_, reject) => {
1707
1837
  if (signal.aborted) {
1708
- reject(new DOMException("Aborted", "AbortError"));
1838
+ reject(abortError());
1709
1839
  return;
1710
1840
  }
1711
- signal.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError")), { once: true });
1841
+ signal.addEventListener("abort", () => reject(abortError()), { once: true });
1712
1842
  })
1713
1843
  ]);
1714
1844
  }
@@ -1719,7 +1849,7 @@ async function loginWallet(type, deps) {
1719
1849
  let connectedWallet;
1720
1850
  try {
1721
1851
  setAuthState({ step: "connecting_wallet", walletType: type });
1722
- const adapter = await deps.resolveWalletAdapter(type);
1852
+ const adapter = await withSignal(deps.resolveWalletAdapter(type), signal);
1723
1853
  const available = await withSignal(adapter.isAvailable(), signal);
1724
1854
  if (!available) {
1725
1855
  setAuthState({ step: "wallet_not_installed", walletType: type });
@@ -1756,6 +1886,9 @@ async function loginWallet(type, deps) {
1756
1886
 
1757
1887
  // src/client/client.ts
1758
1888
  var isBrowser = typeof window !== "undefined" && typeof localStorage !== "undefined";
1889
+ var isReactNative = typeof navigator !== "undefined" && navigator.product === "ReactNative";
1890
+ var isClientRuntime = isBrowser || isReactNative;
1891
+ var REFRESH_SKEW_SECONDS = 60;
1759
1892
  function warnServerSide(method) {
1760
1893
  console.warn(
1761
1894
  `[PollarClient] ${method}() called server-side \u2014 browser APIs unavailable. Use PollarClient only in Client Components.`
@@ -1783,6 +1916,11 @@ var PollarClient = class {
1783
1916
  /** Singleton in-flight refresh — concurrent 401s coalesce into one /auth/refresh call. */
1784
1917
  this._refreshPromise = null;
1785
1918
  this._storageEventHandler = null;
1919
+ /** Updated by the request middleware. Read by the silent-refresh scheduler
1920
+ * to skip proactive refreshes after `maxIdleMs` of no HTTP activity. */
1921
+ this._lastRequestAt = Date.now();
1922
+ this._refreshTimer = null;
1923
+ this._visibilityUnsubscribe = null;
1786
1924
  this._transactionState = null;
1787
1925
  this._transactionStateListeners = /* @__PURE__ */ new Set();
1788
1926
  this._txHistoryState = { step: "idle" };
@@ -1793,19 +1931,38 @@ var PollarClient = class {
1793
1931
  this._authStateListeners = /* @__PURE__ */ new Set();
1794
1932
  this._networkState = { step: "idle" };
1795
1933
  this._networkStateListeners = /* @__PURE__ */ new Set();
1934
+ /**
1935
+ * Latched once the storage adapter degrades. We dedupe (the adapter only
1936
+ * fires once anyway) and use it to replay state to late-subscribers — same
1937
+ * pattern as `onAuthStateChange` replaying `_authState` on subscribe.
1938
+ * Only populated when the SDK constructed the default storage adapter; if
1939
+ * the consumer passes `config.storage`, they own degradation notifications.
1940
+ */
1941
+ this._storageDegraded = null;
1942
+ this._storageDegradeListeners = /* @__PURE__ */ new Set();
1796
1943
  this._walletAdapter = null;
1797
1944
  this._loginController = null;
1798
1945
  this.apiKey = config.apiKey;
1799
- this.id = crypto.randomUUID();
1946
+ this.id = randomUUID();
1800
1947
  this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
1801
- this._storage = config.storage ?? defaultStorage(config.onStorageDegrade ? { onDegrade: config.onStorageDegrade } : void 0);
1948
+ this._storage = config.storage ?? defaultStorage({
1949
+ onDegrade: (reason, error) => {
1950
+ config.onStorageDegrade?.(reason, error);
1951
+ this._dispatchStorageDegrade(reason, error);
1952
+ }
1953
+ });
1802
1954
  this._keyManager = config.keyManager ?? defaultKeyManager(this._storage, config.apiKey);
1803
1955
  this._walletAdapterResolver = config.walletAdapter ?? null;
1956
+ this._walletResolverTimeoutMs = config.walletResolverTimeoutMs ?? 5e3;
1804
1957
  this._deviceLabel = config.deviceLabel;
1958
+ this._visibilityProvider = config.visibilityProvider ?? defaultVisibilityProvider();
1959
+ this._maxIdleMs = config.maxIdleMs;
1960
+ this._openAuthUrl = config.openAuthUrl ?? defaultWebOAuthOpener;
1961
+ this._oauthRedirectUri = config.oauthRedirectUri ?? (isBrowser ? window.location.origin : "");
1805
1962
  this._api = createApiClient(this.basePath);
1806
1963
  this._wireMiddlewares();
1807
1964
  this._networkState = { step: "connected", network: config.stellarNetwork ?? "testnet" };
1808
- if (!isBrowser) {
1965
+ if (!isClientRuntime) {
1809
1966
  warnServerSide("constructor");
1810
1967
  this._initialized = Promise.resolve();
1811
1968
  return;
@@ -1831,7 +1988,7 @@ var PollarClient = class {
1831
1988
  // ─── Lifecycle ────────────────────────────────────────────────────────────
1832
1989
  async _initialize() {
1833
1990
  this._apiKeyHash = await hashApiKey(this.apiKey);
1834
- if (typeof window !== "undefined") {
1991
+ if (isBrowser) {
1835
1992
  const sessionKey = sessionStorageKey(this._apiKeyHash);
1836
1993
  const handler = (e) => {
1837
1994
  if (e.key === sessionKey) {
@@ -1847,15 +2004,23 @@ var PollarClient = class {
1847
2004
  console.warn("[PollarClient] KeyManager init failed; DPoP unavailable for this session", err);
1848
2005
  }
1849
2006
  await this._restoreSession();
2007
+ this._visibilityUnsubscribe = this._visibilityProvider.onChange((visible) => {
2008
+ if (visible) void this._maybeProactiveRefresh();
2009
+ });
1850
2010
  }
1851
2011
  /** Detach the cross-tab storage listener and abort any in-flight login. */
1852
2012
  destroy() {
1853
- if (this._storageEventHandler && typeof window !== "undefined") {
2013
+ if (this._storageEventHandler && isBrowser) {
1854
2014
  window.removeEventListener("storage", this._storageEventHandler);
1855
2015
  this._storageEventHandler = null;
1856
2016
  }
1857
2017
  this._loginController?.abort();
1858
2018
  this._loginController = null;
2019
+ this._clearRefreshTimer();
2020
+ if (this._visibilityUnsubscribe) {
2021
+ this._visibilityUnsubscribe();
2022
+ this._visibilityUnsubscribe = null;
2023
+ }
1859
2024
  }
1860
2025
  // ─── Middlewares (DPoP + auto-refresh) ────────────────────────────────────
1861
2026
  _wireMiddlewares() {
@@ -1863,6 +2028,7 @@ var PollarClient = class {
1863
2028
  this._api.use({
1864
2029
  onRequest: async ({ request }) => {
1865
2030
  request.headers.set("x-pollar-api-key", self.apiKey);
2031
+ self._lastRequestAt = Date.now();
1866
2032
  await self._initialized;
1867
2033
  if (request.body !== null) {
1868
2034
  try {
@@ -1893,15 +2059,22 @@ var PollarClient = class {
1893
2059
  const newNonce = response.headers.get("DPoP-Nonce");
1894
2060
  if (newNonce) self._dpopNonce = newNonce;
1895
2061
  if (response.status !== 401) return response;
1896
- if (request.url.includes("/auth/refresh")) return response;
1897
2062
  const wwwAuth = response.headers.get("WWW-Authenticate") ?? "";
1898
2063
  const isNonceChallenge = wwwAuth.includes("use_dpop_nonce");
2064
+ if (request.url.includes("/auth/refresh")) {
2065
+ if (isNonceChallenge) return self._retryRequest(request);
2066
+ return response;
2067
+ }
1899
2068
  if (!isNonceChallenge) {
1900
2069
  try {
1901
2070
  await self.refresh();
1902
2071
  } catch {
1903
2072
  return response;
1904
2073
  }
2074
+ const method = request.method.toUpperCase();
2075
+ if (method !== "GET" && method !== "HEAD") {
2076
+ return response;
2077
+ }
1905
2078
  }
1906
2079
  return self._retryRequest(request);
1907
2080
  }
@@ -1926,14 +2099,22 @@ var PollarClient = class {
1926
2099
  }
1927
2100
  async _retryRequest(originalRequest) {
1928
2101
  const headers = new Headers(originalRequest.headers);
1929
- const accessToken = this._session?.token?.accessToken;
1930
- if (accessToken) {
1931
- const proof = await this._buildProofForRequest(originalRequest, accessToken);
1932
- if (proof) {
1933
- headers.set("Authorization", `DPoP ${accessToken}`);
1934
- headers.set("DPoP", proof);
1935
- } else {
1936
- headers.set("Authorization", `Bearer ${accessToken}`);
2102
+ const isRefresh = originalRequest.url.includes("/auth/refresh");
2103
+ if (isRefresh) {
2104
+ const proof = await this._buildProofForRequest(originalRequest, void 0);
2105
+ headers.delete("Authorization");
2106
+ if (proof) headers.set("DPoP", proof);
2107
+ else headers.delete("DPoP");
2108
+ } else {
2109
+ const accessToken = this._session?.token?.accessToken;
2110
+ if (accessToken) {
2111
+ const proof = await this._buildProofForRequest(originalRequest, accessToken);
2112
+ if (proof) {
2113
+ headers.set("Authorization", `DPoP ${accessToken}`);
2114
+ headers.set("DPoP", proof);
2115
+ } else {
2116
+ headers.set("Authorization", `Bearer ${accessToken}`);
2117
+ }
1937
2118
  }
1938
2119
  }
1939
2120
  const cachedBody = this._requestBodyCache.get(originalRequest);
@@ -2004,6 +2185,65 @@ var PollarClient = class {
2004
2185
  } catch (err) {
2005
2186
  console.error("[PollarClient] Failed to persist refreshed session", err);
2006
2187
  }
2188
+ this._scheduleNextRefresh();
2189
+ }
2190
+ }
2191
+ // ─── Silent refresh scheduler ────────────────────────────────────────────────
2192
+ /**
2193
+ * Arm a single setTimeout to fire shortly before the current access token
2194
+ * expires. Idempotent — clearing any previous timer first. Safe to call
2195
+ * from any session-write site (initial login, restore-from-storage, after
2196
+ * a successful rotation). No-op if there's no session in memory.
2197
+ *
2198
+ * Browser/RN background-tab throttling makes long-running setTimeouts
2199
+ * unreliable on their own; the `visibilitychange` listener compensates by
2200
+ * re-invoking `_maybeProactiveRefresh` whenever the app comes back to the
2201
+ * foreground, catching any timer that fired late or never fired at all.
2202
+ */
2203
+ _scheduleNextRefresh() {
2204
+ this._clearRefreshTimer();
2205
+ const expiresAt = this._session?.token?.expiresAt;
2206
+ if (typeof expiresAt !== "number") return;
2207
+ const dueInMs = Math.max(0, (expiresAt - Math.floor(Date.now() / 1e3) - REFRESH_SKEW_SECONDS) * 1e3);
2208
+ this._refreshTimer = setTimeout(() => {
2209
+ void this._maybeProactiveRefresh();
2210
+ }, dueInMs);
2211
+ }
2212
+ /**
2213
+ * Decide whether to actually run a refresh right now. Called both from the
2214
+ * scheduler timer and from the visibility-change listener.
2215
+ *
2216
+ * Skip if:
2217
+ * - no session / no RT (nothing to refresh)
2218
+ * - app is hidden — wait for the visibility listener to re-trigger us
2219
+ * - `maxIdleMs` configured and no client request since that window — let
2220
+ * the next reactive 401-refresh handle it whenever the user comes back
2221
+ * - the AT still has more than `REFRESH_SKEW_SECONDS` of life — reschedule
2222
+ *
2223
+ * Otherwise call `refresh()`, which uses the existing in-flight singleton
2224
+ * so we never collide with a reactive 401-triggered refresh. On failure,
2225
+ * `_doRefresh` already calls `_clearSession`, so auth-state listeners see
2226
+ * `step:'idle'` — no extra event dispatch needed here.
2227
+ */
2228
+ async _maybeProactiveRefresh() {
2229
+ if (!this._session?.token?.refreshToken) return;
2230
+ if (!this._visibilityProvider.isVisible()) return;
2231
+ if (this._maxIdleMs !== void 0 && Date.now() - this._lastRequestAt > this._maxIdleMs) return;
2232
+ const expiresAt = this._session.token.expiresAt;
2233
+ if (Math.floor(Date.now() / 1e3) < expiresAt - REFRESH_SKEW_SECONDS) {
2234
+ this._scheduleNextRefresh();
2235
+ return;
2236
+ }
2237
+ try {
2238
+ await this.refresh();
2239
+ } catch (err) {
2240
+ console.warn("[PollarClient] Proactive refresh failed; session cleared", err);
2241
+ }
2242
+ }
2243
+ _clearRefreshTimer() {
2244
+ if (this._refreshTimer !== null) {
2245
+ clearTimeout(this._refreshTimer);
2246
+ this._refreshTimer = null;
2007
2247
  }
2008
2248
  }
2009
2249
  // ─── Auth state ──────────────────────────────────────────────────────────────
@@ -2015,13 +2255,45 @@ var PollarClient = class {
2015
2255
  cb(this._authState);
2016
2256
  return () => this._authStateListeners.delete(cb);
2017
2257
  }
2258
+ /**
2259
+ * Subscribe to persistent-storage degradation (Safari private mode,
2260
+ * sandboxed iframes, quota errors, etc.). The SDK keeps running off
2261
+ * in-memory storage after degrade, but sessions won't survive reload — a
2262
+ * host UI typically wants to show "your session won't be saved" so the
2263
+ * user isn't blindsided after a refresh.
2264
+ *
2265
+ * Fires at most once per client lifetime (the underlying adapter dedupes).
2266
+ * Late subscribers receive the latched state synchronously on subscribe.
2267
+ *
2268
+ * Only fires when the SDK constructs the default storage adapter. If you
2269
+ * pass a custom `config.storage`, wire your own notification path through
2270
+ * that adapter's API — the SDK has no hook into it.
2271
+ */
2272
+ onStorageDegrade(cb) {
2273
+ this._storageDegradeListeners.add(cb);
2274
+ if (this._storageDegraded) {
2275
+ cb(this._storageDegraded.reason, this._storageDegraded.error);
2276
+ }
2277
+ return () => this._storageDegradeListeners.delete(cb);
2278
+ }
2279
+ _dispatchStorageDegrade(reason, error) {
2280
+ if (this._storageDegraded) return;
2281
+ this._storageDegraded = { reason, error };
2282
+ for (const cb of this._storageDegradeListeners) {
2283
+ try {
2284
+ cb(reason, error);
2285
+ } catch (err) {
2286
+ console.error("[PollarClient] onStorageDegrade listener threw", err);
2287
+ }
2288
+ }
2289
+ }
2018
2290
  /** PII (email, names, avatar, providers). Held in memory only — never persisted. */
2019
2291
  getUserProfile() {
2020
2292
  return this._profile;
2021
2293
  }
2022
2294
  // ─── Login (unified entry point) ─────────────────────────────────────────
2023
2295
  login(options) {
2024
- if (!isBrowser) {
2296
+ if (!isClientRuntime) {
2025
2297
  warnServerSide("login");
2026
2298
  return;
2027
2299
  }
@@ -2032,7 +2304,9 @@ var PollarClient = class {
2032
2304
  loginOAuth(options.provider, {
2033
2305
  ...deps,
2034
2306
  basePath: this.basePath,
2035
- apiKey: this.apiKey
2307
+ apiKey: this.apiKey,
2308
+ openAuthUrl: this._openAuthUrl,
2309
+ redirectUri: this._oauthRedirectUri
2036
2310
  }).catch((err) => this._handleFlowError(err));
2037
2311
  } else if (options.provider === "email") {
2038
2312
  const { email } = options;
@@ -2048,7 +2322,7 @@ var PollarClient = class {
2048
2322
  }
2049
2323
  // ─── Email OTP flow (3 steps) ─────────────────────────────────────────────
2050
2324
  beginEmailLogin() {
2051
- if (!isBrowser) {
2325
+ if (!isClientRuntime) {
2052
2326
  warnServerSide("beginEmailLogin");
2053
2327
  return;
2054
2328
  }
@@ -2056,7 +2330,7 @@ var PollarClient = class {
2056
2330
  initEmailSession(this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
2057
2331
  }
2058
2332
  sendEmailCode(email) {
2059
- if (!isBrowser) {
2333
+ if (!isClientRuntime) {
2060
2334
  warnServerSide("sendEmailCode");
2061
2335
  return;
2062
2336
  }
@@ -2068,7 +2342,7 @@ var PollarClient = class {
2068
2342
  sendEmailCode(email, clientSessionId, this._flowDeps(signal)).catch((err) => this._handleFlowError(err));
2069
2343
  }
2070
2344
  verifyEmailCode(code) {
2071
- if (!isBrowser) {
2345
+ if (!isClientRuntime) {
2072
2346
  warnServerSide("verifyEmailCode");
2073
2347
  return;
2074
2348
  }
@@ -2086,7 +2360,7 @@ var PollarClient = class {
2086
2360
  }
2087
2361
  // ─── Wallet flow (single call) ────────────────────────────────────────────
2088
2362
  loginWallet(type) {
2089
- if (!isBrowser) {
2363
+ if (!isClientRuntime) {
2090
2364
  warnServerSide("loginWallet");
2091
2365
  return;
2092
2366
  }
@@ -2112,7 +2386,7 @@ var PollarClient = class {
2112
2386
  * across all devices.
2113
2387
  */
2114
2388
  async logout(options = {}) {
2115
- if (!isBrowser) {
2389
+ if (!isClientRuntime) {
2116
2390
  warnServerSide("logout");
2117
2391
  return;
2118
2392
  }
@@ -2142,7 +2416,7 @@ var PollarClient = class {
2142
2416
  * `current` flag identifies which entry corresponds to this client.
2143
2417
  */
2144
2418
  async listSessions() {
2145
- if (!isBrowser) {
2419
+ if (!isClientRuntime) {
2146
2420
  warnServerSide("listSessions");
2147
2421
  return [];
2148
2422
  }
@@ -2161,7 +2435,7 @@ var PollarClient = class {
2161
2435
  * does NOT clear local state — call `logout()` for that case.
2162
2436
  */
2163
2437
  async revokeSession(familyId) {
2164
- if (!isBrowser) {
2438
+ if (!isClientRuntime) {
2165
2439
  warnServerSide("revokeSession");
2166
2440
  return;
2167
2441
  }
@@ -2251,10 +2525,16 @@ var PollarClient = class {
2251
2525
  }
2252
2526
  }
2253
2527
  // ─── Transactions ─────────────────────────────────────────────────────────
2528
+ /**
2529
+ * Builds an unsigned XDR. Drives `_setTransactionState` for modal-style UIs
2530
+ * AND returns a {@link BuildOutcome} so headless callers can `await` and
2531
+ * inspect the result without subscribing to state changes.
2532
+ */
2254
2533
  async buildTx(operation, params, options) {
2255
2534
  if (!this._session?.wallet?.publicKey) {
2256
- this._setTransactionState({ step: "error", details: "No wallet connected" });
2257
- return;
2535
+ const details = "No wallet connected";
2536
+ this._setTransactionState({ step: "error", phase: "building", details });
2537
+ return { status: "error", details };
2258
2538
  }
2259
2539
  const body = {
2260
2540
  network: this.getNetwork(),
@@ -2268,40 +2548,194 @@ var PollarClient = class {
2268
2548
  const { data, error } = await this._api.POST("/tx/build", { body });
2269
2549
  if (!error && data?.success && data.content) {
2270
2550
  this._setTransactionState({ step: "built", buildData: data.content });
2271
- } else {
2272
- const details = error?.details;
2273
- this._setTransactionState({ step: "error", ...details && { details } });
2551
+ return { status: "built", buildData: data.content };
2274
2552
  }
2553
+ const details = error?.details;
2554
+ this._setTransactionState({ step: "error", phase: "building", ...details && { details } });
2555
+ return { status: "error", ...details && { details } };
2275
2556
  } catch (err) {
2276
2557
  console.error("[PollarClient] buildTx failed", err);
2277
- this._setTransactionState({ step: "error" });
2558
+ this._setTransactionState({ step: "error", phase: "building" });
2559
+ return { status: "error" };
2278
2560
  }
2279
2561
  }
2280
2562
  getWalletType() {
2281
2563
  return this._walletAdapter?.type ?? null;
2282
2564
  }
2283
- async signAndSubmitTx(unsignedXdr) {
2284
- const state = this._transactionState;
2285
- const buildData = state?.step === "built" ? state.buildData : state?.step === "error" ? state.buildData : void 0;
2286
- const stateExtra = buildData ? { buildData } : { external: true };
2287
- this._setTransactionState({ step: "signing", ...stateExtra });
2288
- const accountToSign = this._session?.wallet?.publicKey;
2565
+ /**
2566
+ * Signs the given unsigned XDR and returns the signed XDR.
2567
+ *
2568
+ * - External wallets: signs locally via the wallet adapter.
2569
+ * - Custodial wallets: posts to `/tx/sign`. The backend signs (through
2570
+ * wallet-service or the app's customer-managed adapter) and returns the
2571
+ * signed XDR plus an `idempotencyKey` the caller should echo back to
2572
+ * `submitTx`.
2573
+ *
2574
+ * Drives `_setTransactionState`: emits `signing` while in flight and
2575
+ * `signed` on success (or `error[phase: 'signing']` on failure). `buildData`
2576
+ * is threaded through if the consumer previously called `buildTx`.
2577
+ */
2578
+ async signTx(unsignedXdr) {
2579
+ const buildData = this._currentBuildData();
2580
+ this._setTransactionState({ step: "signing", ...buildData && { buildData } });
2289
2581
  if (this._walletAdapter) {
2582
+ const accountToSign = this._session?.wallet?.publicKey;
2583
+ const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
2290
2584
  try {
2291
- const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
2292
2585
  const { signedTxXdr } = await this._walletAdapter.signTransaction(unsignedXdr, signOpts);
2293
- const stellarClient = new StellarClient(this.getNetwork());
2294
- const result = await stellarClient.submitTransaction(signedTxXdr);
2295
- if (result.success) {
2296
- this._setTransactionState({ step: "success", ...stateExtra, hash: result.hash });
2297
- } else {
2298
- this._setTransactionState({ step: "error", ...stateExtra, details: result.errorCode });
2586
+ this._setTransactionState({
2587
+ step: "signed",
2588
+ signedXdr: signedTxXdr,
2589
+ ...buildData && { buildData }
2590
+ });
2591
+ return { status: "signed", signedXdr: signedTxXdr };
2592
+ } catch (err) {
2593
+ const details = err instanceof Error ? err.message : void 0;
2594
+ this._setTransactionState({
2595
+ step: "error",
2596
+ phase: "signing",
2597
+ ...buildData && { buildData },
2598
+ ...details && { details }
2599
+ });
2600
+ return { status: "error", ...details && { details } };
2601
+ }
2602
+ }
2603
+ const publicKey = this._session?.wallet?.publicKey ?? "";
2604
+ try {
2605
+ const { data, error } = await this._api.POST("/tx/sign", {
2606
+ body: { network: this.getNetwork(), publicKey, unsignedXdr }
2607
+ });
2608
+ if (!error && data?.success && data.content?.signedXdr) {
2609
+ const { signedXdr, idempotencyKey } = data.content;
2610
+ this._setTransactionState({
2611
+ step: "signed",
2612
+ signedXdr,
2613
+ submissionToken: idempotencyKey,
2614
+ ...buildData && { buildData }
2615
+ });
2616
+ return { status: "signed", signedXdr, submissionToken: idempotencyKey };
2617
+ }
2618
+ const details = error?.details;
2619
+ this._setTransactionState({
2620
+ step: "error",
2621
+ phase: "signing",
2622
+ ...buildData && { buildData },
2623
+ ...details && { details }
2624
+ });
2625
+ return { status: "error", ...details && { details } };
2626
+ } catch (err) {
2627
+ const details = err instanceof Error ? err.message : void 0;
2628
+ this._setTransactionState({
2629
+ step: "error",
2630
+ phase: "signing",
2631
+ ...buildData && { buildData },
2632
+ ...details && { details }
2633
+ });
2634
+ return { status: "error", ...details && { details } };
2635
+ }
2636
+ }
2637
+ /**
2638
+ * Submits a signed XDR via `/tx/submit` regardless of wallet type
2639
+ * (custodial or external). Routing through sdk-api gives us:
2640
+ * - End-to-end tx_records persistence with full phase lifecycle so the
2641
+ * developer dashboard can show every tx (both custodial and external
2642
+ * wallet flows) at `/apps/:id/monitor/transactions`.
2643
+ * - Idempotency tracking via `submissionToken` (returned by `signTx`).
2644
+ * - A single response shape (SUCCESS / PENDING / FAILED) shared by both
2645
+ * flows — previously external wallets could only return SUCCESS or
2646
+ * error since the direct-to-Horizon path was synchronous.
2647
+ *
2648
+ * The extra hop adds ~50–150 ms vs. the legacy direct-Horizon path; the
2649
+ * persistence + observability win is worth it.
2650
+ *
2651
+ * Drives `_setTransactionState`: emits `submitting` while in flight,
2652
+ * `submitted` on Horizon ack (pending), `success` on ledger confirmation,
2653
+ * or `error[phase: 'submitting']` on failure.
2654
+ */
2655
+ async submitTx(signedXdr, opts) {
2656
+ const buildData = this._currentBuildData();
2657
+ const outcomeExtra = buildData ? { buildData } : {};
2658
+ this._setTransactionState({ step: "submitting", signedXdr, ...buildData && { buildData } });
2659
+ const publicKey = this._session?.wallet?.publicKey ?? "";
2660
+ try {
2661
+ const { data, error } = await this._api.POST("/tx/submit", {
2662
+ body: {
2663
+ network: this.getNetwork(),
2664
+ publicKey,
2665
+ signedXdr,
2666
+ ...opts?.submissionToken && { idempotencyKey: opts.submissionToken }
2299
2667
  }
2300
- } catch {
2301
- this._setTransactionState({ step: "error", ...stateExtra });
2668
+ });
2669
+ if (!error && data?.success && data.content) {
2670
+ const { hash, status: backendStatus, resultCode } = data.content;
2671
+ if (backendStatus === "SUCCESS") {
2672
+ this._setTransactionState({ step: "success", hash, ...buildData && { buildData } });
2673
+ return { status: "success", hash, ...outcomeExtra };
2674
+ }
2675
+ if (backendStatus === "PENDING") {
2676
+ this._setTransactionState({ step: "submitted", hash, ...buildData && { buildData } });
2677
+ return { status: "pending", hash, ...outcomeExtra };
2678
+ }
2679
+ this._setTransactionState({
2680
+ step: "error",
2681
+ phase: "submitting",
2682
+ ...buildData && { buildData },
2683
+ ...resultCode && { details: resultCode }
2684
+ });
2685
+ return {
2686
+ status: "error",
2687
+ hash,
2688
+ ...outcomeExtra,
2689
+ ...resultCode && { details: resultCode, resultCode }
2690
+ };
2302
2691
  }
2303
- return;
2692
+ const details = error?.details;
2693
+ this._setTransactionState({
2694
+ step: "error",
2695
+ phase: "submitting",
2696
+ ...buildData && { buildData },
2697
+ ...details && { details }
2698
+ });
2699
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2700
+ } catch (err) {
2701
+ const details = err instanceof Error ? err.message : void 0;
2702
+ this._setTransactionState({
2703
+ step: "error",
2704
+ phase: "submitting",
2705
+ ...buildData && { buildData },
2706
+ ...details && { details }
2707
+ });
2708
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2304
2709
  }
2710
+ }
2711
+ /**
2712
+ * Signs and submits in one logical step. Returns a {@link SubmitOutcome}.
2713
+ *
2714
+ * - **External wallets**: composes `signTx` + `submitTx` client-side. State
2715
+ * machine sees the full granular sequence `signing → signed → submitting
2716
+ * → success` because the underlying methods each emit.
2717
+ * - **Custodial wallets**: atomic `/tx/sign-and-send` round-trip. State
2718
+ * machine emits the compound `signing-submitting` step (the SDK can't
2719
+ * observe when one phase ends and the next begins inside that single
2720
+ * backend call) and then transitions to `submitted` (Horizon ack only) or
2721
+ * `success` (ledger-confirmed), or `error[phase: 'signing-submitting']`.
2722
+ */
2723
+ async signAndSubmitTx(unsignedXdr) {
2724
+ if (this._walletAdapter) {
2725
+ const signed = await this.signTx(unsignedXdr);
2726
+ if (signed.status === "error") {
2727
+ const buildData2 = this._currentBuildData();
2728
+ return {
2729
+ status: "error",
2730
+ ...buildData2 && { buildData: buildData2 },
2731
+ ...signed.details && { details: signed.details }
2732
+ };
2733
+ }
2734
+ return this.submitTx(signed.signedXdr);
2735
+ }
2736
+ const buildData = this._currentBuildData();
2737
+ const outcomeExtra = buildData ? { buildData } : {};
2738
+ this._setTransactionState({ step: "signing-submitting", ...buildData && { buildData } });
2305
2739
  const body = {
2306
2740
  network: this.getNetwork(),
2307
2741
  publicKey: this._session?.wallet?.publicKey ?? "",
@@ -2310,15 +2744,129 @@ var PollarClient = class {
2310
2744
  try {
2311
2745
  const { data, error } = await this._api.POST("/tx/sign-and-send", { body });
2312
2746
  if (!error && data?.success && data.content?.hash) {
2313
- this._setTransactionState({ step: "success", ...stateExtra, hash: data.content.hash });
2314
- } else {
2315
- const details = error?.details;
2316
- this._setTransactionState({ step: "error", ...stateExtra, ...details && { details } });
2747
+ const {
2748
+ hash,
2749
+ status: backendStatus,
2750
+ resultCode
2751
+ } = data.content;
2752
+ if (backendStatus === "SUCCESS") {
2753
+ this._setTransactionState({ step: "success", hash, ...buildData && { buildData } });
2754
+ return { status: "success", hash, ...outcomeExtra };
2755
+ }
2756
+ if (backendStatus === "PENDING") {
2757
+ this._setTransactionState({ step: "submitted", hash, ...buildData && { buildData } });
2758
+ return { status: "pending", hash, ...outcomeExtra };
2759
+ }
2760
+ this._setTransactionState({
2761
+ step: "error",
2762
+ phase: "signing-submitting",
2763
+ ...buildData && { buildData },
2764
+ ...resultCode && { details: resultCode }
2765
+ });
2766
+ return {
2767
+ status: "error",
2768
+ hash,
2769
+ ...outcomeExtra,
2770
+ ...resultCode && { details: resultCode, resultCode }
2771
+ };
2317
2772
  }
2318
- } catch {
2319
- this._setTransactionState({ step: "error", ...stateExtra });
2773
+ const details = error?.details;
2774
+ this._setTransactionState({
2775
+ step: "error",
2776
+ phase: "signing-submitting",
2777
+ ...buildData && { buildData },
2778
+ ...details && { details }
2779
+ });
2780
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2781
+ } catch (err) {
2782
+ const details = err instanceof Error ? err.message : void 0;
2783
+ this._setTransactionState({
2784
+ step: "error",
2785
+ phase: "signing-submitting",
2786
+ ...buildData && { buildData },
2787
+ ...details && { details }
2788
+ });
2789
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2320
2790
  }
2321
2791
  }
2792
+ /**
2793
+ * One-shot: build → sign → submit, returning the final {@link SubmitOutcome}.
2794
+ *
2795
+ * - **External wallets**: composes `buildTx` + `signAndSubmitTx` client-side.
2796
+ * State machine sees the full granular sequence (`building → built →
2797
+ * signing → signed → submitting → success`) because each composed call
2798
+ * emits its own transitions.
2799
+ * - **Custodial wallets**: single round-trip to `/tx/build-sign-submit`. The
2800
+ * signed XDR never leaves the backend. State machine emits the compound
2801
+ * `building-signing-submitting` step (the SDK can't observe individual
2802
+ * phase boundaries inside one atomic call) and then transitions to
2803
+ * `submitted` / `success` / `error[phase: 'building-signing-submitting']`.
2804
+ *
2805
+ * If you need granular UI feedback for custodial flows (separate
2806
+ * "Building…", "Signing…", "Submitting…" indicators), call `buildTx`,
2807
+ * `signTx`, and `submitTx` separately instead.
2808
+ */
2809
+ async buildAndSignAndSubmitTx(operation, params, options) {
2810
+ if (this._walletAdapter) {
2811
+ const built = await this.buildTx(operation, params, options);
2812
+ if (built.status === "error") {
2813
+ return { status: "error", ...built.details && { details: built.details } };
2814
+ }
2815
+ return this.signAndSubmitTx(built.buildData.unsignedXdr);
2816
+ }
2817
+ if (!this._session?.wallet?.publicKey) {
2818
+ this._setTransactionState({ step: "error", phase: "building-signing-submitting", details: "No wallet connected" });
2819
+ return { status: "error", details: "No wallet connected" };
2820
+ }
2821
+ this._setTransactionState({ step: "building-signing-submitting" });
2822
+ try {
2823
+ const { data, error } = await this._api.POST("/tx/build-sign-submit", {
2824
+ body: {
2825
+ network: this.getNetwork(),
2826
+ publicKey: this._session.wallet.publicKey,
2827
+ operation,
2828
+ params,
2829
+ options: options ?? {}
2830
+ }
2831
+ });
2832
+ if (!error && data?.success && data.content) {
2833
+ const { hash, status: backendStatus, resultCode } = data.content;
2834
+ if (backendStatus === "SUCCESS") {
2835
+ this._setTransactionState({ step: "success", hash });
2836
+ return { status: "success", hash };
2837
+ }
2838
+ if (backendStatus === "PENDING") {
2839
+ this._setTransactionState({ step: "submitted", hash });
2840
+ return { status: "pending", hash };
2841
+ }
2842
+ this._setTransactionState({
2843
+ step: "error",
2844
+ phase: "building-signing-submitting",
2845
+ ...resultCode && { details: resultCode }
2846
+ });
2847
+ return { status: "error", hash, ...resultCode && { details: resultCode, resultCode } };
2848
+ }
2849
+ const details = error?.details;
2850
+ this._setTransactionState({
2851
+ step: "error",
2852
+ phase: "building-signing-submitting",
2853
+ ...details && { details }
2854
+ });
2855
+ return { status: "error", ...details && { details } };
2856
+ } catch (err) {
2857
+ const details = err instanceof Error ? err.message : void 0;
2858
+ this._setTransactionState({
2859
+ step: "error",
2860
+ phase: "building-signing-submitting",
2861
+ ...details && { details }
2862
+ });
2863
+ return { status: "error", ...details && { details } };
2864
+ }
2865
+ }
2866
+ /** Alias for {@link buildAndSignAndSubmitTx} — shorter "just do the thing" name. */
2867
+ async runTx(operation, params, options) {
2868
+ return this.buildAndSignAndSubmitTx(operation, params, options);
2869
+ }
2322
2870
  // ─── App config ───────────────────────────────────────────────────────────
2323
2871
  async getAppConfig() {
2324
2872
  try {
@@ -2385,6 +2933,11 @@ var PollarClient = class {
2385
2933
  _flowDeps(signal) {
2386
2934
  return {
2387
2935
  api: this._api,
2936
+ basePath: this.basePath,
2937
+ // SSE status streaming works on web; React Native's `fetch` has no
2938
+ // readable `response.body`, so those clients poll the non-streaming
2939
+ // status endpoint instead. `isBrowser` is false in RN and SSR alike.
2940
+ useStreaming: isBrowser,
2388
2941
  signal,
2389
2942
  setAuthState: this._setAuthState.bind(this),
2390
2943
  storeSession: this._storeSession.bind(this),
@@ -2406,7 +2959,22 @@ var PollarClient = class {
2406
2959
  */
2407
2960
  async _resolveWalletAdapter(id) {
2408
2961
  if (this._walletAdapterResolver) {
2409
- return Promise.resolve(this._walletAdapterResolver(id));
2962
+ const timeoutMs = this._walletResolverTimeoutMs;
2963
+ let timeoutHandle;
2964
+ const timeoutPromise = new Promise((_, reject) => {
2965
+ timeoutHandle = setTimeout(() => {
2966
+ reject(
2967
+ Object.assign(new Error(`[PollarClient] Wallet adapter resolver for "${id}" timed out after ${timeoutMs}ms`), {
2968
+ code: AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT
2969
+ })
2970
+ );
2971
+ }, timeoutMs);
2972
+ });
2973
+ try {
2974
+ return await Promise.race([Promise.resolve(this._walletAdapterResolver(id)), timeoutPromise]);
2975
+ } finally {
2976
+ if (timeoutHandle !== void 0) clearTimeout(timeoutHandle);
2977
+ }
2410
2978
  }
2411
2979
  if (id === "freighter" /* FREIGHTER */) return new FreighterAdapter();
2412
2980
  if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter();
@@ -2420,6 +2988,16 @@ var PollarClient = class {
2420
2988
  this._setAuthState({ step: "idle" });
2421
2989
  return;
2422
2990
  }
2991
+ if (error instanceof Error && error.code === AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT) {
2992
+ console.error("[PollarClient]", error.message);
2993
+ this._setAuthState({
2994
+ step: "error",
2995
+ previousStep: this._authState.step,
2996
+ message: error.message,
2997
+ errorCode: AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT
2998
+ });
2999
+ return;
3000
+ }
2423
3001
  console.error("[PollarClient] Unexpected error in auth flow", error);
2424
3002
  this._setAuthState({
2425
3003
  step: "error",
@@ -2441,6 +3019,7 @@ var PollarClient = class {
2441
3019
  }
2442
3020
  console.info("[PollarClient] Session restored from storage");
2443
3021
  this._setAuthState({ step: "authenticated", session: this._session });
3022
+ this._scheduleNextRefresh();
2444
3023
  } else {
2445
3024
  console.info("[PollarClient] No session in storage");
2446
3025
  }
@@ -2467,9 +3046,11 @@ var PollarClient = class {
2467
3046
  }
2468
3047
  await writeStorage(this._storage, this.apiKeyHash, persisted);
2469
3048
  this._setAuthState({ step: "authenticated", session: persisted });
3049
+ this._scheduleNextRefresh();
2470
3050
  }
2471
3051
  async _clearSession() {
2472
3052
  console.info("[PollarClient] Session cleared");
3053
+ this._clearRefreshTimer();
2473
3054
  this._session = null;
2474
3055
  this._profile = null;
2475
3056
  this._walletAdapter = null;
@@ -2502,6 +3083,46 @@ var PollarClient = class {
2502
3083
  console.info(`[PollarClient] transaction:${next.step}`);
2503
3084
  for (const cb of this._transactionStateListeners) cb(next);
2504
3085
  }
3086
+ /**
3087
+ * Threads `buildData` through state transitions. When the user has already
3088
+ * called `buildTx`, every subsequent state (signing, signed, submitting,
3089
+ * submitted, success, error) should carry the build summary so modal UIs
3090
+ * can keep showing "Send 5 USDC to G..." through the whole flow.
3091
+ */
3092
+ _currentBuildData() {
3093
+ const s = this._transactionState;
3094
+ if (!s) return void 0;
3095
+ if ("buildData" in s && s.buildData) return s.buildData;
3096
+ return void 0;
3097
+ }
3098
+ };
3099
+
3100
+ // src/stellar/StellarClient.ts
3101
+ var HORIZON_URLS = {
3102
+ mainnet: "https://horizon.stellar.org",
3103
+ testnet: "https://horizon-testnet.stellar.org"
3104
+ };
3105
+ var StellarClient = class {
3106
+ constructor(config) {
3107
+ this.horizonUrl = typeof config === "string" ? HORIZON_URLS[config] : config.horizonUrl;
3108
+ }
3109
+ async submitTransaction(signedXdr) {
3110
+ try {
3111
+ const response = await fetch(`${this.horizonUrl}/transactions`, {
3112
+ method: "POST",
3113
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
3114
+ body: new URLSearchParams({ tx: signedXdr })
3115
+ });
3116
+ if (!response.ok) {
3117
+ const body = await response.json().catch(() => ({}));
3118
+ return { success: false, errorCode: body.extras?.result_codes?.transaction ?? "HORIZON_ERROR" };
3119
+ }
3120
+ const data = await response.json();
3121
+ return { success: true, hash: data.hash };
3122
+ } catch {
3123
+ return { success: false, errorCode: "NETWORK_ERROR" };
3124
+ }
3125
+ }
2505
3126
  };
2506
3127
 
2507
3128
  // src/index.ts