@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.rn.mjs CHANGED
@@ -28,10 +28,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
 
29
29
  // ../../node_modules/@stellar/freighter-api/build/index.min.js
30
30
  var require_index_min = __commonJS({
31
- "../../node_modules/@stellar/freighter-api/build/index.min.js"(exports$1, module) {
31
+ "../../node_modules/@stellar/freighter-api/build/index.min.js"(exports, module) {
32
32
  !(function(e, r) {
33
- "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();
34
- })(exports$1, (() => (() => {
33
+ "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();
34
+ })(exports, (() => (() => {
35
35
  var e, r, E = { d: (e2, r2) => {
36
36
  for (var o2 in r2) E.o(r2, o2) && !E.o(e2, o2) && Object.defineProperty(e2, o2, { enumerable: true, get: r2[o2] });
37
37
  }, o: (e2, r2) => Object.prototype.hasOwnProperty.call(e2, r2), r: (e2) => {
@@ -450,9 +450,7 @@ var WebCryptoKeyManager = class {
450
450
  */
451
451
  this._initPromise = null;
452
452
  if (typeof globalThis.crypto === "undefined" || !globalThis.crypto.subtle) {
453
- throw new Error(
454
- "[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost)."
455
- );
453
+ throw new Error("[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost).");
456
454
  }
457
455
  this.apiKey = apiKey;
458
456
  }
@@ -1025,14 +1023,18 @@ function createApiClient(baseUrl) {
1025
1023
  async function listDistributionRules(api) {
1026
1024
  const { data, error } = await api.GET("/distribution/rules");
1027
1025
  if (!data?.content || error) {
1028
- throw new Error(error?.error ?? "Failed to list distribution rules");
1026
+ throw new Error(
1027
+ error?.code ?? error?.error ?? "Failed to list distribution rules"
1028
+ );
1029
1029
  }
1030
1030
  return data.content.rules;
1031
1031
  }
1032
1032
  async function claimDistributionRule(api, body) {
1033
1033
  const { data, error } = await api.POST("/distribution/claim", { body });
1034
1034
  if (!data?.content || error) {
1035
- throw new Error(error?.error ?? "Failed to claim distribution rule");
1035
+ throw new Error(
1036
+ error?.code ?? error?.error ?? "Failed to claim distribution rule"
1037
+ );
1036
1038
  }
1037
1039
  return data.content;
1038
1040
  }
@@ -1043,18 +1045,18 @@ async function getKycStatus(api, providerId) {
1043
1045
  params: { query: providerId ? { providerId } : {} }
1044
1046
  });
1045
1047
  if (!data?.content || error) {
1046
- throw new Error(error?.error ?? "Failed to get KYC status");
1048
+ throw new Error(error?.code ?? error?.error ?? "Failed to get KYC status");
1047
1049
  }
1048
1050
  return data.content;
1049
1051
  }
1050
1052
  async function getKycProviders(api, country) {
1051
1053
  const { data, error } = await api.GET("/kyc/providers", { params: { query: { country } } });
1052
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to get KYC providers");
1054
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get KYC providers");
1053
1055
  return data.content;
1054
1056
  }
1055
1057
  async function startKyc(api, body) {
1056
1058
  const { data, error } = await api.POST("/kyc/start", { body });
1057
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to start KYC");
1059
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to start KYC");
1058
1060
  return data.content;
1059
1061
  }
1060
1062
  async function resolveKyc(api, providerId, level = "basic") {
@@ -1076,22 +1078,22 @@ async function pollKycStatus(api, providerId, { intervalMs = 3e3, timeoutMs = 3e
1076
1078
  // src/api/endpoints/ramps.ts
1077
1079
  async function getRampsQuote(api, query) {
1078
1080
  const { data, error } = await api.GET("/ramps/quote", { params: { query } });
1079
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to get ramp quotes");
1081
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get ramp quotes");
1080
1082
  return data.content;
1081
1083
  }
1082
1084
  async function createOnRamp(api, body) {
1083
1085
  const { data, error } = await api.POST("/ramps/onramp", { body });
1084
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to create onramp");
1086
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to create onramp");
1085
1087
  return data.content;
1086
1088
  }
1087
1089
  async function createOffRamp(api, body) {
1088
1090
  const { data, error } = await api.POST("/ramps/offramp", { body });
1089
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to create offramp");
1091
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to create offramp");
1090
1092
  return data.content;
1091
1093
  }
1092
1094
  async function getRampTransaction(api, txId) {
1093
1095
  const { data, error } = await api.GET("/ramps/transaction/{txId}", { params: { path: { txId } } });
1094
- if (!data?.content || error) throw new Error(error?.error ?? "Failed to get transaction");
1096
+ if (!data?.content || error) throw new Error(error?.code ?? error?.error ?? "Failed to get transaction");
1095
1097
  return data.content;
1096
1098
  }
1097
1099
  async function pollRampTransaction(api, txId, { intervalMs = 5e3, timeoutMs = 6e5 } = {}) {
@@ -1104,6 +1106,26 @@ async function pollRampTransaction(api, txId, { intervalMs = 5e3, timeoutMs = 6e
1104
1106
  throw new Error("Ramp transaction polling timed out");
1105
1107
  }
1106
1108
 
1109
+ // src/lib/random-uuid.ts
1110
+ function randomUUID() {
1111
+ const c = globalThis.crypto;
1112
+ if (c && typeof c.randomUUID === "function") {
1113
+ return c.randomUUID();
1114
+ }
1115
+ if (c && typeof c.getRandomValues === "function") {
1116
+ const bytes = new Uint8Array(16);
1117
+ c.getRandomValues(bytes);
1118
+ bytes[6] = bytes[6] & 15 | 64;
1119
+ bytes[8] = bytes[8] & 63 | 128;
1120
+ const hex = [];
1121
+ for (let i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, "0"));
1122
+ 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("")}`;
1123
+ }
1124
+ throw new Error(
1125
+ "[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."
1126
+ );
1127
+ }
1128
+
1107
1129
  // src/dpop.ts
1108
1130
  async function buildProof(args, keyManager) {
1109
1131
  const jwk = await keyManager.getPublicJwk();
@@ -1113,7 +1135,7 @@ async function buildProof(args, keyManager) {
1113
1135
  jwk
1114
1136
  };
1115
1137
  const payload = {
1116
- jti: generateJti(),
1138
+ jti: randomUUID(),
1117
1139
  htm: args.htm.toUpperCase(),
1118
1140
  htu: normalizeHtu(args.htu),
1119
1141
  iat: Math.floor(Date.now() / 1e3)
@@ -1147,52 +1169,6 @@ function normalizeHtu(rawUrl) {
1147
1169
  const portPart = port ? `:${port}` : "";
1148
1170
  return `${scheme}//${host}${portPart}${url.pathname}`;
1149
1171
  }
1150
- function generateJti() {
1151
- const c = globalThis.crypto;
1152
- if (c && typeof c.randomUUID === "function") {
1153
- return c.randomUUID();
1154
- }
1155
- if (c && typeof c.getRandomValues === "function") {
1156
- const bytes = new Uint8Array(16);
1157
- c.getRandomValues(bytes);
1158
- bytes[6] = bytes[6] & 15 | 64;
1159
- bytes[8] = bytes[8] & 63 | 128;
1160
- const hex = [];
1161
- for (let i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, "0"));
1162
- 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("")}`;
1163
- }
1164
- throw new Error(
1165
- "[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."
1166
- );
1167
- }
1168
-
1169
- // src/stellar/StellarClient.ts
1170
- var HORIZON_URLS = {
1171
- mainnet: "https://horizon.stellar.org",
1172
- testnet: "https://horizon-testnet.stellar.org"
1173
- };
1174
- var StellarClient = class {
1175
- constructor(config) {
1176
- this.horizonUrl = typeof config === "string" ? HORIZON_URLS[config] : config.horizonUrl;
1177
- }
1178
- async submitTransaction(signedXdr) {
1179
- try {
1180
- const response = await fetch(`${this.horizonUrl}/transactions`, {
1181
- method: "POST",
1182
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
1183
- body: new URLSearchParams({ tx: signedXdr })
1184
- });
1185
- if (!response.ok) {
1186
- const body = await response.json().catch(() => ({}));
1187
- return { success: false, errorCode: body.extras?.result_codes?.transaction ?? "HORIZON_ERROR" };
1188
- }
1189
- const data = await response.json();
1190
- return { success: true, hash: data.hash };
1191
- } catch {
1192
- return { success: false, errorCode: "NETWORK_ERROR" };
1193
- }
1194
- }
1195
- };
1196
1172
 
1197
1173
  // src/storage/web.ts
1198
1174
  var LOG_PREFIX = "[PollarClient:storage]";
@@ -1281,9 +1257,62 @@ function defaultStorage(options = {}) {
1281
1257
  return createLocalStorageAdapter(options);
1282
1258
  }
1283
1259
 
1260
+ // src/visibility/noop.ts
1261
+ function createNoopVisibilityProvider() {
1262
+ return {
1263
+ isVisible: () => true,
1264
+ onChange: () => () => {
1265
+ }
1266
+ };
1267
+ }
1268
+
1269
+ // src/visibility/web.ts
1270
+ function createWebVisibilityProvider() {
1271
+ const isVisibleNow = () => typeof document === "undefined" || document.visibilityState === "visible";
1272
+ return {
1273
+ isVisible: isVisibleNow,
1274
+ onChange: (cb) => {
1275
+ if (typeof window === "undefined" || typeof document === "undefined") {
1276
+ return () => {
1277
+ };
1278
+ }
1279
+ let last = isVisibleNow();
1280
+ const handler = () => {
1281
+ const next = isVisibleNow();
1282
+ if (next !== last) {
1283
+ last = next;
1284
+ cb(next);
1285
+ }
1286
+ };
1287
+ document.addEventListener("visibilitychange", handler);
1288
+ window.addEventListener("pageshow", handler);
1289
+ window.addEventListener("pagehide", handler);
1290
+ window.addEventListener("focus", handler);
1291
+ window.addEventListener("blur", handler);
1292
+ return () => {
1293
+ document.removeEventListener("visibilitychange", handler);
1294
+ window.removeEventListener("pageshow", handler);
1295
+ window.removeEventListener("pagehide", handler);
1296
+ window.removeEventListener("focus", handler);
1297
+ window.removeEventListener("blur", handler);
1298
+ };
1299
+ }
1300
+ };
1301
+ }
1302
+
1303
+ // src/visibility/autodetect.ts
1304
+ function defaultVisibilityProvider() {
1305
+ if (typeof document !== "undefined" && typeof window !== "undefined") {
1306
+ return createWebVisibilityProvider();
1307
+ }
1308
+ return createNoopVisibilityProvider();
1309
+ }
1310
+
1284
1311
  // src/types.ts
1285
1312
  var AUTH_ERROR_CODES = {
1286
1313
  SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
1314
+ SESSION_EXPIRED: "SESSION_EXPIRED",
1315
+ SESSION_INVALID: "SESSION_INVALID",
1287
1316
  EMAIL_SEND_FAILED: "EMAIL_SEND_FAILED",
1288
1317
  EMAIL_VERIFY_FAILED: "EMAIL_VERIFY_FAILED",
1289
1318
  EMAIL_CODE_EXPIRED: "EMAIL_CODE_EXPIRED",
@@ -1291,6 +1320,7 @@ var AUTH_ERROR_CODES = {
1291
1320
  AUTH_FAILED: "AUTH_FAILED",
1292
1321
  WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
1293
1322
  WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
1323
+ WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
1294
1324
  UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
1295
1325
  };
1296
1326
  var PollarFlowError = class extends Error {
@@ -1596,7 +1626,32 @@ async function readWalletType(storage, apiKeyHash) {
1596
1626
  return storage.get(walletTypeStorageKey(apiKeyHash));
1597
1627
  }
1598
1628
 
1629
+ // src/lib/abort.ts
1630
+ function abortError() {
1631
+ if (typeof DOMException !== "undefined") {
1632
+ return new DOMException("Aborted", "AbortError");
1633
+ }
1634
+ const err = new Error("Aborted");
1635
+ err.name = "AbortError";
1636
+ return err;
1637
+ }
1638
+ function throwIfAborted(signal) {
1639
+ if (signal?.aborted) throw abortError();
1640
+ }
1641
+
1599
1642
  // src/client/stream.ts
1643
+ var SessionStatusError = class extends Error {
1644
+ constructor(code) {
1645
+ super(`[PollarClient] Session status terminal: ${code}`);
1646
+ this.code = code;
1647
+ this.name = "SessionStatusError";
1648
+ }
1649
+ };
1650
+ function terminalStatusCode(parsed) {
1651
+ const err = parsed?.error;
1652
+ if (err === "INVALID_CLIENT_SESSION_ID" || err === "EXPIRED_CLIENT_ID") return err;
1653
+ return null;
1654
+ }
1600
1655
  function abortableDelay(ms, signal) {
1601
1656
  return new Promise((resolve, reject) => {
1602
1657
  const t = setTimeout(resolve, ms);
@@ -1604,7 +1659,7 @@ function abortableDelay(ms, signal) {
1604
1659
  "abort",
1605
1660
  () => {
1606
1661
  clearTimeout(t);
1607
- reject(new DOMException("Aborted", "AbortError"));
1662
+ reject(abortError());
1608
1663
  },
1609
1664
  { once: true }
1610
1665
  );
@@ -1619,7 +1674,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1619
1674
  else await new Promise((r) => setTimeout(r, ms));
1620
1675
  };
1621
1676
  while (true) {
1622
- signal?.throwIfAborted();
1677
+ throwIfAborted(signal);
1623
1678
  let data, error;
1624
1679
  try {
1625
1680
  ({ data, error } = await api.GET("/auth/session/status/{clientSessionId}", {
@@ -1642,7 +1697,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1642
1697
  let sawAnyChunk = false;
1643
1698
  try {
1644
1699
  while (true) {
1645
- signal?.throwIfAborted();
1700
+ throwIfAborted(signal);
1646
1701
  const { done, value } = await reader.read();
1647
1702
  if (done) {
1648
1703
  streamDone = true;
@@ -1653,17 +1708,22 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1653
1708
  for (const message of chunk.split("\n\n").filter(Boolean)) {
1654
1709
  const dataLine = message.split("\n").find((l) => l.startsWith("data:"));
1655
1710
  if (!dataLine) continue;
1711
+ let parsed;
1656
1712
  try {
1657
- const parsed = JSON.parse(dataLine.slice("data:".length).trim());
1658
- if (check(parsed)) {
1659
- return parsed;
1660
- }
1713
+ parsed = JSON.parse(dataLine.slice("data:".length).trim());
1661
1714
  } catch {
1715
+ continue;
1716
+ }
1717
+ const terminal = terminalStatusCode(parsed);
1718
+ if (terminal) throw new SessionStatusError(terminal);
1719
+ if (check(parsed)) {
1720
+ return parsed;
1662
1721
  }
1663
1722
  }
1664
1723
  }
1665
1724
  } catch (e) {
1666
1725
  if (e instanceof Error && e.name === "AbortError") throw e;
1726
+ if (e instanceof SessionStatusError) throw e;
1667
1727
  console.warn(e);
1668
1728
  } finally {
1669
1729
  reader.releaseLock();
@@ -1674,14 +1734,74 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1674
1734
  if (delay) await sleep(delay);
1675
1735
  }
1676
1736
  }
1737
+ async function pollUntilFound(baseUrl, clientSessionId, check, intervalMs = 500, signal) {
1738
+ const url = `${baseUrl}/auth/session/status/${encodeURIComponent(clientSessionId)}/poll`;
1739
+ let backoff = intervalMs;
1740
+ const sleep = async (ms) => {
1741
+ if (ms <= 0) return;
1742
+ if (signal) await abortableDelay(ms, signal);
1743
+ else await new Promise((r) => setTimeout(r, ms));
1744
+ };
1745
+ while (true) {
1746
+ throwIfAborted(signal);
1747
+ let envelope = null;
1748
+ let httpStatus = 0;
1749
+ try {
1750
+ const response = await fetch(url, { headers: { accept: "application/json" }, signal: signal ?? null });
1751
+ httpStatus = response.status;
1752
+ envelope = await response.json().catch(() => null);
1753
+ } catch (e) {
1754
+ if (e instanceof Error && e.name === "AbortError") throw e;
1755
+ console.warn(e);
1756
+ }
1757
+ if (httpStatus === 404 || envelope?.code === "INVALID_CLIENT_SESSION_ID") {
1758
+ throw new SessionStatusError("INVALID_CLIENT_SESSION_ID");
1759
+ }
1760
+ if (httpStatus === 410 || envelope?.code === "EXPIRED_CLIENT_ID") {
1761
+ throw new SessionStatusError("EXPIRED_CLIENT_ID");
1762
+ }
1763
+ if (envelope?.success && envelope.content && check(envelope.content)) {
1764
+ return envelope.content;
1765
+ }
1766
+ if (envelope) backoff = intervalMs;
1767
+ else backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
1768
+ await sleep(backoff);
1769
+ }
1770
+ }
1771
+ function waitForSessionReady(args) {
1772
+ const { api, baseUrl, clientSessionId, check, useStreaming, retryDelayMs, signal } = args;
1773
+ return useStreaming ? streamUntilFound(api, clientSessionId, check, retryDelayMs ?? 200, signal) : pollUntilFound(baseUrl, clientSessionId, check, retryDelayMs ?? 500, signal);
1774
+ }
1677
1775
 
1678
1776
  // src/client/auth/authenticate.ts
1679
1777
  async function authenticate(clientSessionId, deps, expectedWallet) {
1680
- const { api, signal, setAuthState, storeSession, clearSession } = deps;
1778
+ const { api, basePath, useStreaming, signal, setAuthState, storeSession, clearSession } = deps;
1681
1779
  setAuthState({ step: "authenticating" });
1682
- await streamUntilFound(api, clientSessionId, (data2) => data2?.status === "READY", 200, signal);
1780
+ try {
1781
+ await waitForSessionReady({
1782
+ api,
1783
+ baseUrl: basePath,
1784
+ clientSessionId,
1785
+ check: (data2) => data2?.status === "READY",
1786
+ useStreaming,
1787
+ signal
1788
+ });
1789
+ } catch (err) {
1790
+ if (err instanceof SessionStatusError) {
1791
+ const expired = err.code === "EXPIRED_CLIENT_ID";
1792
+ setAuthState({
1793
+ step: "error",
1794
+ previousStep: "authenticating",
1795
+ message: expired ? "Login session expired \u2014 please try again" : "Login session is no longer valid \u2014 please try again",
1796
+ errorCode: expired ? AUTH_ERROR_CODES.SESSION_EXPIRED : AUTH_ERROR_CODES.SESSION_INVALID
1797
+ });
1798
+ await clearSession();
1799
+ return;
1800
+ }
1801
+ throw err;
1802
+ }
1683
1803
  const dpopJwk = await deps.getPublicJwk();
1684
- const { data, error } = await api.POST("/auth/login", {
1804
+ const { data } = await api.POST("/auth/login", {
1685
1805
  body: {
1686
1806
  clientSessionId,
1687
1807
  dpopJwk,
@@ -1803,26 +1923,36 @@ function severOpener(popup) {
1803
1923
  } catch {
1804
1924
  }
1805
1925
  }
1806
- async function loginOAuth(provider, deps) {
1807
- const { setAuthState, basePath, apiKey } = deps;
1808
- const popup = window.open("about:blank", "_blank");
1926
+ var defaultWebOAuthOpener = async ({ getUrl }) => {
1927
+ const popup = typeof window !== "undefined" ? window.open("about:blank", "_blank") : null;
1809
1928
  severOpener(popup);
1810
- const clientSessionId = await createAuthSession(deps);
1811
- if (!clientSessionId) {
1929
+ const url = await getUrl();
1930
+ if (!url) {
1812
1931
  popup?.close();
1813
1932
  return;
1814
1933
  }
1815
- setAuthState({ step: "opening_oauth", provider });
1816
- const url = new URL(`${basePath}/auth/${provider}`);
1817
- url.searchParams.set("api_key", apiKey);
1818
- url.searchParams.set("client_session_id", clientSessionId);
1819
- url.searchParams.set("redirect_uri", window.location.origin);
1820
1934
  if (popup) {
1821
- popup.location.href = url.toString();
1935
+ popup.location.href = url;
1822
1936
  severOpener(popup);
1823
- } else {
1824
- window.open(url.toString(), "_blank", "noopener,noreferrer");
1937
+ } else if (typeof window !== "undefined") {
1938
+ window.open(url, "_blank", "noopener,noreferrer");
1825
1939
  }
1940
+ };
1941
+ async function loginOAuth(provider, deps) {
1942
+ const { setAuthState, basePath, apiKey, openAuthUrl, redirectUri, signal } = deps;
1943
+ let clientSessionId = null;
1944
+ const getUrl = async () => {
1945
+ clientSessionId = await createAuthSession(deps);
1946
+ if (!clientSessionId) return null;
1947
+ setAuthState({ step: "opening_oauth", provider });
1948
+ const url = new URL(`${basePath}/auth/${provider}`);
1949
+ url.searchParams.set("api_key", apiKey);
1950
+ url.searchParams.set("client_session_id", clientSessionId);
1951
+ url.searchParams.set("redirect_uri", redirectUri);
1952
+ return url.toString();
1953
+ };
1954
+ await openAuthUrl({ provider, getUrl, redirectUri, signal });
1955
+ if (!clientSessionId) return;
1826
1956
  await authenticate(clientSessionId, deps);
1827
1957
  }
1828
1958
 
@@ -1832,10 +1962,10 @@ function withSignal(promise, signal) {
1832
1962
  promise,
1833
1963
  new Promise((_, reject) => {
1834
1964
  if (signal.aborted) {
1835
- reject(new DOMException("Aborted", "AbortError"));
1965
+ reject(abortError());
1836
1966
  return;
1837
1967
  }
1838
- signal.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError")), { once: true });
1968
+ signal.addEventListener("abort", () => reject(abortError()), { once: true });
1839
1969
  })
1840
1970
  ]);
1841
1971
  }
@@ -1846,7 +1976,7 @@ async function loginWallet(type, deps) {
1846
1976
  let connectedWallet;
1847
1977
  try {
1848
1978
  setAuthState({ step: "connecting_wallet", walletType: type });
1849
- const adapter = await deps.resolveWalletAdapter(type);
1979
+ const adapter = await withSignal(deps.resolveWalletAdapter(type), signal);
1850
1980
  const available = await withSignal(adapter.isAvailable(), signal);
1851
1981
  if (!available) {
1852
1982
  setAuthState({ step: "wallet_not_installed", walletType: type });
@@ -1883,6 +2013,9 @@ async function loginWallet(type, deps) {
1883
2013
 
1884
2014
  // src/client/client.ts
1885
2015
  var isBrowser = typeof window !== "undefined" && typeof localStorage !== "undefined";
2016
+ var isReactNative = typeof navigator !== "undefined" && navigator.product === "ReactNative";
2017
+ var isClientRuntime = isBrowser || isReactNative;
2018
+ var REFRESH_SKEW_SECONDS = 60;
1886
2019
  function warnServerSide(method) {
1887
2020
  console.warn(
1888
2021
  `[PollarClient] ${method}() called server-side \u2014 browser APIs unavailable. Use PollarClient only in Client Components.`
@@ -1910,6 +2043,11 @@ var PollarClient = class {
1910
2043
  /** Singleton in-flight refresh — concurrent 401s coalesce into one /auth/refresh call. */
1911
2044
  this._refreshPromise = null;
1912
2045
  this._storageEventHandler = null;
2046
+ /** Updated by the request middleware. Read by the silent-refresh scheduler
2047
+ * to skip proactive refreshes after `maxIdleMs` of no HTTP activity. */
2048
+ this._lastRequestAt = Date.now();
2049
+ this._refreshTimer = null;
2050
+ this._visibilityUnsubscribe = null;
1913
2051
  this._transactionState = null;
1914
2052
  this._transactionStateListeners = /* @__PURE__ */ new Set();
1915
2053
  this._txHistoryState = { step: "idle" };
@@ -1920,19 +2058,38 @@ var PollarClient = class {
1920
2058
  this._authStateListeners = /* @__PURE__ */ new Set();
1921
2059
  this._networkState = { step: "idle" };
1922
2060
  this._networkStateListeners = /* @__PURE__ */ new Set();
2061
+ /**
2062
+ * Latched once the storage adapter degrades. We dedupe (the adapter only
2063
+ * fires once anyway) and use it to replay state to late-subscribers — same
2064
+ * pattern as `onAuthStateChange` replaying `_authState` on subscribe.
2065
+ * Only populated when the SDK constructed the default storage adapter; if
2066
+ * the consumer passes `config.storage`, they own degradation notifications.
2067
+ */
2068
+ this._storageDegraded = null;
2069
+ this._storageDegradeListeners = /* @__PURE__ */ new Set();
1923
2070
  this._walletAdapter = null;
1924
2071
  this._loginController = null;
1925
2072
  this.apiKey = config.apiKey;
1926
- this.id = crypto.randomUUID();
2073
+ this.id = randomUUID();
1927
2074
  this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
1928
- this._storage = config.storage ?? defaultStorage(config.onStorageDegrade ? { onDegrade: config.onStorageDegrade } : void 0);
2075
+ this._storage = config.storage ?? defaultStorage({
2076
+ onDegrade: (reason, error) => {
2077
+ config.onStorageDegrade?.(reason, error);
2078
+ this._dispatchStorageDegrade(reason, error);
2079
+ }
2080
+ });
1929
2081
  this._keyManager = config.keyManager ?? defaultKeyManager(this._storage, config.apiKey);
1930
2082
  this._walletAdapterResolver = config.walletAdapter ?? null;
2083
+ this._walletResolverTimeoutMs = config.walletResolverTimeoutMs ?? 5e3;
1931
2084
  this._deviceLabel = config.deviceLabel;
2085
+ this._visibilityProvider = config.visibilityProvider ?? defaultVisibilityProvider();
2086
+ this._maxIdleMs = config.maxIdleMs;
2087
+ this._openAuthUrl = config.openAuthUrl ?? defaultWebOAuthOpener;
2088
+ this._oauthRedirectUri = config.oauthRedirectUri ?? (isBrowser ? window.location.origin : "");
1932
2089
  this._api = createApiClient(this.basePath);
1933
2090
  this._wireMiddlewares();
1934
2091
  this._networkState = { step: "connected", network: config.stellarNetwork ?? "testnet" };
1935
- if (!isBrowser) {
2092
+ if (!isClientRuntime) {
1936
2093
  warnServerSide("constructor");
1937
2094
  this._initialized = Promise.resolve();
1938
2095
  return;
@@ -1958,7 +2115,7 @@ var PollarClient = class {
1958
2115
  // ─── Lifecycle ────────────────────────────────────────────────────────────
1959
2116
  async _initialize() {
1960
2117
  this._apiKeyHash = await hashApiKey(this.apiKey);
1961
- if (typeof window !== "undefined") {
2118
+ if (isBrowser) {
1962
2119
  const sessionKey = sessionStorageKey(this._apiKeyHash);
1963
2120
  const handler = (e) => {
1964
2121
  if (e.key === sessionKey) {
@@ -1974,15 +2131,23 @@ var PollarClient = class {
1974
2131
  console.warn("[PollarClient] KeyManager init failed; DPoP unavailable for this session", err);
1975
2132
  }
1976
2133
  await this._restoreSession();
2134
+ this._visibilityUnsubscribe = this._visibilityProvider.onChange((visible) => {
2135
+ if (visible) void this._maybeProactiveRefresh();
2136
+ });
1977
2137
  }
1978
2138
  /** Detach the cross-tab storage listener and abort any in-flight login. */
1979
2139
  destroy() {
1980
- if (this._storageEventHandler && typeof window !== "undefined") {
2140
+ if (this._storageEventHandler && isBrowser) {
1981
2141
  window.removeEventListener("storage", this._storageEventHandler);
1982
2142
  this._storageEventHandler = null;
1983
2143
  }
1984
2144
  this._loginController?.abort();
1985
2145
  this._loginController = null;
2146
+ this._clearRefreshTimer();
2147
+ if (this._visibilityUnsubscribe) {
2148
+ this._visibilityUnsubscribe();
2149
+ this._visibilityUnsubscribe = null;
2150
+ }
1986
2151
  }
1987
2152
  // ─── Middlewares (DPoP + auto-refresh) ────────────────────────────────────
1988
2153
  _wireMiddlewares() {
@@ -1990,6 +2155,7 @@ var PollarClient = class {
1990
2155
  this._api.use({
1991
2156
  onRequest: async ({ request }) => {
1992
2157
  request.headers.set("x-pollar-api-key", self.apiKey);
2158
+ self._lastRequestAt = Date.now();
1993
2159
  await self._initialized;
1994
2160
  if (request.body !== null) {
1995
2161
  try {
@@ -2020,15 +2186,22 @@ var PollarClient = class {
2020
2186
  const newNonce = response.headers.get("DPoP-Nonce");
2021
2187
  if (newNonce) self._dpopNonce = newNonce;
2022
2188
  if (response.status !== 401) return response;
2023
- if (request.url.includes("/auth/refresh")) return response;
2024
2189
  const wwwAuth = response.headers.get("WWW-Authenticate") ?? "";
2025
2190
  const isNonceChallenge = wwwAuth.includes("use_dpop_nonce");
2191
+ if (request.url.includes("/auth/refresh")) {
2192
+ if (isNonceChallenge) return self._retryRequest(request);
2193
+ return response;
2194
+ }
2026
2195
  if (!isNonceChallenge) {
2027
2196
  try {
2028
2197
  await self.refresh();
2029
2198
  } catch {
2030
2199
  return response;
2031
2200
  }
2201
+ const method = request.method.toUpperCase();
2202
+ if (method !== "GET" && method !== "HEAD") {
2203
+ return response;
2204
+ }
2032
2205
  }
2033
2206
  return self._retryRequest(request);
2034
2207
  }
@@ -2053,14 +2226,22 @@ var PollarClient = class {
2053
2226
  }
2054
2227
  async _retryRequest(originalRequest) {
2055
2228
  const headers = new Headers(originalRequest.headers);
2056
- const accessToken = this._session?.token?.accessToken;
2057
- if (accessToken) {
2058
- const proof = await this._buildProofForRequest(originalRequest, accessToken);
2059
- if (proof) {
2060
- headers.set("Authorization", `DPoP ${accessToken}`);
2061
- headers.set("DPoP", proof);
2062
- } else {
2063
- headers.set("Authorization", `Bearer ${accessToken}`);
2229
+ const isRefresh = originalRequest.url.includes("/auth/refresh");
2230
+ if (isRefresh) {
2231
+ const proof = await this._buildProofForRequest(originalRequest, void 0);
2232
+ headers.delete("Authorization");
2233
+ if (proof) headers.set("DPoP", proof);
2234
+ else headers.delete("DPoP");
2235
+ } else {
2236
+ const accessToken = this._session?.token?.accessToken;
2237
+ if (accessToken) {
2238
+ const proof = await this._buildProofForRequest(originalRequest, accessToken);
2239
+ if (proof) {
2240
+ headers.set("Authorization", `DPoP ${accessToken}`);
2241
+ headers.set("DPoP", proof);
2242
+ } else {
2243
+ headers.set("Authorization", `Bearer ${accessToken}`);
2244
+ }
2064
2245
  }
2065
2246
  }
2066
2247
  const cachedBody = this._requestBodyCache.get(originalRequest);
@@ -2131,6 +2312,65 @@ var PollarClient = class {
2131
2312
  } catch (err) {
2132
2313
  console.error("[PollarClient] Failed to persist refreshed session", err);
2133
2314
  }
2315
+ this._scheduleNextRefresh();
2316
+ }
2317
+ }
2318
+ // ─── Silent refresh scheduler ────────────────────────────────────────────────
2319
+ /**
2320
+ * Arm a single setTimeout to fire shortly before the current access token
2321
+ * expires. Idempotent — clearing any previous timer first. Safe to call
2322
+ * from any session-write site (initial login, restore-from-storage, after
2323
+ * a successful rotation). No-op if there's no session in memory.
2324
+ *
2325
+ * Browser/RN background-tab throttling makes long-running setTimeouts
2326
+ * unreliable on their own; the `visibilitychange` listener compensates by
2327
+ * re-invoking `_maybeProactiveRefresh` whenever the app comes back to the
2328
+ * foreground, catching any timer that fired late or never fired at all.
2329
+ */
2330
+ _scheduleNextRefresh() {
2331
+ this._clearRefreshTimer();
2332
+ const expiresAt = this._session?.token?.expiresAt;
2333
+ if (typeof expiresAt !== "number") return;
2334
+ const dueInMs = Math.max(0, (expiresAt - Math.floor(Date.now() / 1e3) - REFRESH_SKEW_SECONDS) * 1e3);
2335
+ this._refreshTimer = setTimeout(() => {
2336
+ void this._maybeProactiveRefresh();
2337
+ }, dueInMs);
2338
+ }
2339
+ /**
2340
+ * Decide whether to actually run a refresh right now. Called both from the
2341
+ * scheduler timer and from the visibility-change listener.
2342
+ *
2343
+ * Skip if:
2344
+ * - no session / no RT (nothing to refresh)
2345
+ * - app is hidden — wait for the visibility listener to re-trigger us
2346
+ * - `maxIdleMs` configured and no client request since that window — let
2347
+ * the next reactive 401-refresh handle it whenever the user comes back
2348
+ * - the AT still has more than `REFRESH_SKEW_SECONDS` of life — reschedule
2349
+ *
2350
+ * Otherwise call `refresh()`, which uses the existing in-flight singleton
2351
+ * so we never collide with a reactive 401-triggered refresh. On failure,
2352
+ * `_doRefresh` already calls `_clearSession`, so auth-state listeners see
2353
+ * `step:'idle'` — no extra event dispatch needed here.
2354
+ */
2355
+ async _maybeProactiveRefresh() {
2356
+ if (!this._session?.token?.refreshToken) return;
2357
+ if (!this._visibilityProvider.isVisible()) return;
2358
+ if (this._maxIdleMs !== void 0 && Date.now() - this._lastRequestAt > this._maxIdleMs) return;
2359
+ const expiresAt = this._session.token.expiresAt;
2360
+ if (Math.floor(Date.now() / 1e3) < expiresAt - REFRESH_SKEW_SECONDS) {
2361
+ this._scheduleNextRefresh();
2362
+ return;
2363
+ }
2364
+ try {
2365
+ await this.refresh();
2366
+ } catch (err) {
2367
+ console.warn("[PollarClient] Proactive refresh failed; session cleared", err);
2368
+ }
2369
+ }
2370
+ _clearRefreshTimer() {
2371
+ if (this._refreshTimer !== null) {
2372
+ clearTimeout(this._refreshTimer);
2373
+ this._refreshTimer = null;
2134
2374
  }
2135
2375
  }
2136
2376
  // ─── Auth state ──────────────────────────────────────────────────────────────
@@ -2142,13 +2382,45 @@ var PollarClient = class {
2142
2382
  cb(this._authState);
2143
2383
  return () => this._authStateListeners.delete(cb);
2144
2384
  }
2385
+ /**
2386
+ * Subscribe to persistent-storage degradation (Safari private mode,
2387
+ * sandboxed iframes, quota errors, etc.). The SDK keeps running off
2388
+ * in-memory storage after degrade, but sessions won't survive reload — a
2389
+ * host UI typically wants to show "your session won't be saved" so the
2390
+ * user isn't blindsided after a refresh.
2391
+ *
2392
+ * Fires at most once per client lifetime (the underlying adapter dedupes).
2393
+ * Late subscribers receive the latched state synchronously on subscribe.
2394
+ *
2395
+ * Only fires when the SDK constructs the default storage adapter. If you
2396
+ * pass a custom `config.storage`, wire your own notification path through
2397
+ * that adapter's API — the SDK has no hook into it.
2398
+ */
2399
+ onStorageDegrade(cb) {
2400
+ this._storageDegradeListeners.add(cb);
2401
+ if (this._storageDegraded) {
2402
+ cb(this._storageDegraded.reason, this._storageDegraded.error);
2403
+ }
2404
+ return () => this._storageDegradeListeners.delete(cb);
2405
+ }
2406
+ _dispatchStorageDegrade(reason, error) {
2407
+ if (this._storageDegraded) return;
2408
+ this._storageDegraded = { reason, error };
2409
+ for (const cb of this._storageDegradeListeners) {
2410
+ try {
2411
+ cb(reason, error);
2412
+ } catch (err) {
2413
+ console.error("[PollarClient] onStorageDegrade listener threw", err);
2414
+ }
2415
+ }
2416
+ }
2145
2417
  /** PII (email, names, avatar, providers). Held in memory only — never persisted. */
2146
2418
  getUserProfile() {
2147
2419
  return this._profile;
2148
2420
  }
2149
2421
  // ─── Login (unified entry point) ─────────────────────────────────────────
2150
2422
  login(options) {
2151
- if (!isBrowser) {
2423
+ if (!isClientRuntime) {
2152
2424
  warnServerSide("login");
2153
2425
  return;
2154
2426
  }
@@ -2159,7 +2431,9 @@ var PollarClient = class {
2159
2431
  loginOAuth(options.provider, {
2160
2432
  ...deps,
2161
2433
  basePath: this.basePath,
2162
- apiKey: this.apiKey
2434
+ apiKey: this.apiKey,
2435
+ openAuthUrl: this._openAuthUrl,
2436
+ redirectUri: this._oauthRedirectUri
2163
2437
  }).catch((err) => this._handleFlowError(err));
2164
2438
  } else if (options.provider === "email") {
2165
2439
  const { email } = options;
@@ -2175,7 +2449,7 @@ var PollarClient = class {
2175
2449
  }
2176
2450
  // ─── Email OTP flow (3 steps) ─────────────────────────────────────────────
2177
2451
  beginEmailLogin() {
2178
- if (!isBrowser) {
2452
+ if (!isClientRuntime) {
2179
2453
  warnServerSide("beginEmailLogin");
2180
2454
  return;
2181
2455
  }
@@ -2183,7 +2457,7 @@ var PollarClient = class {
2183
2457
  initEmailSession(this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
2184
2458
  }
2185
2459
  sendEmailCode(email) {
2186
- if (!isBrowser) {
2460
+ if (!isClientRuntime) {
2187
2461
  warnServerSide("sendEmailCode");
2188
2462
  return;
2189
2463
  }
@@ -2195,7 +2469,7 @@ var PollarClient = class {
2195
2469
  sendEmailCode(email, clientSessionId, this._flowDeps(signal)).catch((err) => this._handleFlowError(err));
2196
2470
  }
2197
2471
  verifyEmailCode(code) {
2198
- if (!isBrowser) {
2472
+ if (!isClientRuntime) {
2199
2473
  warnServerSide("verifyEmailCode");
2200
2474
  return;
2201
2475
  }
@@ -2213,7 +2487,7 @@ var PollarClient = class {
2213
2487
  }
2214
2488
  // ─── Wallet flow (single call) ────────────────────────────────────────────
2215
2489
  loginWallet(type) {
2216
- if (!isBrowser) {
2490
+ if (!isClientRuntime) {
2217
2491
  warnServerSide("loginWallet");
2218
2492
  return;
2219
2493
  }
@@ -2239,7 +2513,7 @@ var PollarClient = class {
2239
2513
  * across all devices.
2240
2514
  */
2241
2515
  async logout(options = {}) {
2242
- if (!isBrowser) {
2516
+ if (!isClientRuntime) {
2243
2517
  warnServerSide("logout");
2244
2518
  return;
2245
2519
  }
@@ -2269,7 +2543,7 @@ var PollarClient = class {
2269
2543
  * `current` flag identifies which entry corresponds to this client.
2270
2544
  */
2271
2545
  async listSessions() {
2272
- if (!isBrowser) {
2546
+ if (!isClientRuntime) {
2273
2547
  warnServerSide("listSessions");
2274
2548
  return [];
2275
2549
  }
@@ -2288,7 +2562,7 @@ var PollarClient = class {
2288
2562
  * does NOT clear local state — call `logout()` for that case.
2289
2563
  */
2290
2564
  async revokeSession(familyId) {
2291
- if (!isBrowser) {
2565
+ if (!isClientRuntime) {
2292
2566
  warnServerSide("revokeSession");
2293
2567
  return;
2294
2568
  }
@@ -2378,10 +2652,16 @@ var PollarClient = class {
2378
2652
  }
2379
2653
  }
2380
2654
  // ─── Transactions ─────────────────────────────────────────────────────────
2655
+ /**
2656
+ * Builds an unsigned XDR. Drives `_setTransactionState` for modal-style UIs
2657
+ * AND returns a {@link BuildOutcome} so headless callers can `await` and
2658
+ * inspect the result without subscribing to state changes.
2659
+ */
2381
2660
  async buildTx(operation, params, options) {
2382
2661
  if (!this._session?.wallet?.publicKey) {
2383
- this._setTransactionState({ step: "error", details: "No wallet connected" });
2384
- return;
2662
+ const details = "No wallet connected";
2663
+ this._setTransactionState({ step: "error", phase: "building", details });
2664
+ return { status: "error", details };
2385
2665
  }
2386
2666
  const body = {
2387
2667
  network: this.getNetwork(),
@@ -2395,40 +2675,194 @@ var PollarClient = class {
2395
2675
  const { data, error } = await this._api.POST("/tx/build", { body });
2396
2676
  if (!error && data?.success && data.content) {
2397
2677
  this._setTransactionState({ step: "built", buildData: data.content });
2398
- } else {
2399
- const details = error?.details;
2400
- this._setTransactionState({ step: "error", ...details && { details } });
2678
+ return { status: "built", buildData: data.content };
2401
2679
  }
2680
+ const details = error?.details;
2681
+ this._setTransactionState({ step: "error", phase: "building", ...details && { details } });
2682
+ return { status: "error", ...details && { details } };
2402
2683
  } catch (err) {
2403
2684
  console.error("[PollarClient] buildTx failed", err);
2404
- this._setTransactionState({ step: "error" });
2685
+ this._setTransactionState({ step: "error", phase: "building" });
2686
+ return { status: "error" };
2405
2687
  }
2406
2688
  }
2407
2689
  getWalletType() {
2408
2690
  return this._walletAdapter?.type ?? null;
2409
2691
  }
2410
- async signAndSubmitTx(unsignedXdr) {
2411
- const state = this._transactionState;
2412
- const buildData = state?.step === "built" ? state.buildData : state?.step === "error" ? state.buildData : void 0;
2413
- const stateExtra = buildData ? { buildData } : { external: true };
2414
- this._setTransactionState({ step: "signing", ...stateExtra });
2415
- const accountToSign = this._session?.wallet?.publicKey;
2692
+ /**
2693
+ * Signs the given unsigned XDR and returns the signed XDR.
2694
+ *
2695
+ * - External wallets: signs locally via the wallet adapter.
2696
+ * - Custodial wallets: posts to `/tx/sign`. The backend signs (through
2697
+ * wallet-service or the app's customer-managed adapter) and returns the
2698
+ * signed XDR plus an `idempotencyKey` the caller should echo back to
2699
+ * `submitTx`.
2700
+ *
2701
+ * Drives `_setTransactionState`: emits `signing` while in flight and
2702
+ * `signed` on success (or `error[phase: 'signing']` on failure). `buildData`
2703
+ * is threaded through if the consumer previously called `buildTx`.
2704
+ */
2705
+ async signTx(unsignedXdr) {
2706
+ const buildData = this._currentBuildData();
2707
+ this._setTransactionState({ step: "signing", ...buildData && { buildData } });
2416
2708
  if (this._walletAdapter) {
2709
+ const accountToSign = this._session?.wallet?.publicKey;
2710
+ const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
2417
2711
  try {
2418
- const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
2419
2712
  const { signedTxXdr } = await this._walletAdapter.signTransaction(unsignedXdr, signOpts);
2420
- const stellarClient = new StellarClient(this.getNetwork());
2421
- const result = await stellarClient.submitTransaction(signedTxXdr);
2422
- if (result.success) {
2423
- this._setTransactionState({ step: "success", ...stateExtra, hash: result.hash });
2424
- } else {
2425
- this._setTransactionState({ step: "error", ...stateExtra, details: result.errorCode });
2713
+ this._setTransactionState({
2714
+ step: "signed",
2715
+ signedXdr: signedTxXdr,
2716
+ ...buildData && { buildData }
2717
+ });
2718
+ return { status: "signed", signedXdr: signedTxXdr };
2719
+ } catch (err) {
2720
+ const details = err instanceof Error ? err.message : void 0;
2721
+ this._setTransactionState({
2722
+ step: "error",
2723
+ phase: "signing",
2724
+ ...buildData && { buildData },
2725
+ ...details && { details }
2726
+ });
2727
+ return { status: "error", ...details && { details } };
2728
+ }
2729
+ }
2730
+ const publicKey = this._session?.wallet?.publicKey ?? "";
2731
+ try {
2732
+ const { data, error } = await this._api.POST("/tx/sign", {
2733
+ body: { network: this.getNetwork(), publicKey, unsignedXdr }
2734
+ });
2735
+ if (!error && data?.success && data.content?.signedXdr) {
2736
+ const { signedXdr, idempotencyKey } = data.content;
2737
+ this._setTransactionState({
2738
+ step: "signed",
2739
+ signedXdr,
2740
+ submissionToken: idempotencyKey,
2741
+ ...buildData && { buildData }
2742
+ });
2743
+ return { status: "signed", signedXdr, submissionToken: idempotencyKey };
2744
+ }
2745
+ const details = error?.details;
2746
+ this._setTransactionState({
2747
+ step: "error",
2748
+ phase: "signing",
2749
+ ...buildData && { buildData },
2750
+ ...details && { details }
2751
+ });
2752
+ return { status: "error", ...details && { details } };
2753
+ } catch (err) {
2754
+ const details = err instanceof Error ? err.message : void 0;
2755
+ this._setTransactionState({
2756
+ step: "error",
2757
+ phase: "signing",
2758
+ ...buildData && { buildData },
2759
+ ...details && { details }
2760
+ });
2761
+ return { status: "error", ...details && { details } };
2762
+ }
2763
+ }
2764
+ /**
2765
+ * Submits a signed XDR via `/tx/submit` regardless of wallet type
2766
+ * (custodial or external). Routing through sdk-api gives us:
2767
+ * - End-to-end tx_records persistence with full phase lifecycle so the
2768
+ * developer dashboard can show every tx (both custodial and external
2769
+ * wallet flows) at `/apps/:id/monitor/transactions`.
2770
+ * - Idempotency tracking via `submissionToken` (returned by `signTx`).
2771
+ * - A single response shape (SUCCESS / PENDING / FAILED) shared by both
2772
+ * flows — previously external wallets could only return SUCCESS or
2773
+ * error since the direct-to-Horizon path was synchronous.
2774
+ *
2775
+ * The extra hop adds ~50–150 ms vs. the legacy direct-Horizon path; the
2776
+ * persistence + observability win is worth it.
2777
+ *
2778
+ * Drives `_setTransactionState`: emits `submitting` while in flight,
2779
+ * `submitted` on Horizon ack (pending), `success` on ledger confirmation,
2780
+ * or `error[phase: 'submitting']` on failure.
2781
+ */
2782
+ async submitTx(signedXdr, opts) {
2783
+ const buildData = this._currentBuildData();
2784
+ const outcomeExtra = buildData ? { buildData } : {};
2785
+ this._setTransactionState({ step: "submitting", signedXdr, ...buildData && { buildData } });
2786
+ const publicKey = this._session?.wallet?.publicKey ?? "";
2787
+ try {
2788
+ const { data, error } = await this._api.POST("/tx/submit", {
2789
+ body: {
2790
+ network: this.getNetwork(),
2791
+ publicKey,
2792
+ signedXdr,
2793
+ ...opts?.submissionToken && { idempotencyKey: opts.submissionToken }
2426
2794
  }
2427
- } catch {
2428
- this._setTransactionState({ step: "error", ...stateExtra });
2795
+ });
2796
+ if (!error && data?.success && data.content) {
2797
+ const { hash, status: backendStatus, resultCode } = data.content;
2798
+ if (backendStatus === "SUCCESS") {
2799
+ this._setTransactionState({ step: "success", hash, ...buildData && { buildData } });
2800
+ return { status: "success", hash, ...outcomeExtra };
2801
+ }
2802
+ if (backendStatus === "PENDING") {
2803
+ this._setTransactionState({ step: "submitted", hash, ...buildData && { buildData } });
2804
+ return { status: "pending", hash, ...outcomeExtra };
2805
+ }
2806
+ this._setTransactionState({
2807
+ step: "error",
2808
+ phase: "submitting",
2809
+ ...buildData && { buildData },
2810
+ ...resultCode && { details: resultCode }
2811
+ });
2812
+ return {
2813
+ status: "error",
2814
+ hash,
2815
+ ...outcomeExtra,
2816
+ ...resultCode && { details: resultCode, resultCode }
2817
+ };
2429
2818
  }
2430
- return;
2819
+ const details = error?.details;
2820
+ this._setTransactionState({
2821
+ step: "error",
2822
+ phase: "submitting",
2823
+ ...buildData && { buildData },
2824
+ ...details && { details }
2825
+ });
2826
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2827
+ } catch (err) {
2828
+ const details = err instanceof Error ? err.message : void 0;
2829
+ this._setTransactionState({
2830
+ step: "error",
2831
+ phase: "submitting",
2832
+ ...buildData && { buildData },
2833
+ ...details && { details }
2834
+ });
2835
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2431
2836
  }
2837
+ }
2838
+ /**
2839
+ * Signs and submits in one logical step. Returns a {@link SubmitOutcome}.
2840
+ *
2841
+ * - **External wallets**: composes `signTx` + `submitTx` client-side. State
2842
+ * machine sees the full granular sequence `signing → signed → submitting
2843
+ * → success` because the underlying methods each emit.
2844
+ * - **Custodial wallets**: atomic `/tx/sign-and-send` round-trip. State
2845
+ * machine emits the compound `signing-submitting` step (the SDK can't
2846
+ * observe when one phase ends and the next begins inside that single
2847
+ * backend call) and then transitions to `submitted` (Horizon ack only) or
2848
+ * `success` (ledger-confirmed), or `error[phase: 'signing-submitting']`.
2849
+ */
2850
+ async signAndSubmitTx(unsignedXdr) {
2851
+ if (this._walletAdapter) {
2852
+ const signed = await this.signTx(unsignedXdr);
2853
+ if (signed.status === "error") {
2854
+ const buildData2 = this._currentBuildData();
2855
+ return {
2856
+ status: "error",
2857
+ ...buildData2 && { buildData: buildData2 },
2858
+ ...signed.details && { details: signed.details }
2859
+ };
2860
+ }
2861
+ return this.submitTx(signed.signedXdr);
2862
+ }
2863
+ const buildData = this._currentBuildData();
2864
+ const outcomeExtra = buildData ? { buildData } : {};
2865
+ this._setTransactionState({ step: "signing-submitting", ...buildData && { buildData } });
2432
2866
  const body = {
2433
2867
  network: this.getNetwork(),
2434
2868
  publicKey: this._session?.wallet?.publicKey ?? "",
@@ -2437,15 +2871,129 @@ var PollarClient = class {
2437
2871
  try {
2438
2872
  const { data, error } = await this._api.POST("/tx/sign-and-send", { body });
2439
2873
  if (!error && data?.success && data.content?.hash) {
2440
- this._setTransactionState({ step: "success", ...stateExtra, hash: data.content.hash });
2441
- } else {
2442
- const details = error?.details;
2443
- this._setTransactionState({ step: "error", ...stateExtra, ...details && { details } });
2874
+ const {
2875
+ hash,
2876
+ status: backendStatus,
2877
+ resultCode
2878
+ } = data.content;
2879
+ if (backendStatus === "SUCCESS") {
2880
+ this._setTransactionState({ step: "success", hash, ...buildData && { buildData } });
2881
+ return { status: "success", hash, ...outcomeExtra };
2882
+ }
2883
+ if (backendStatus === "PENDING") {
2884
+ this._setTransactionState({ step: "submitted", hash, ...buildData && { buildData } });
2885
+ return { status: "pending", hash, ...outcomeExtra };
2886
+ }
2887
+ this._setTransactionState({
2888
+ step: "error",
2889
+ phase: "signing-submitting",
2890
+ ...buildData && { buildData },
2891
+ ...resultCode && { details: resultCode }
2892
+ });
2893
+ return {
2894
+ status: "error",
2895
+ hash,
2896
+ ...outcomeExtra,
2897
+ ...resultCode && { details: resultCode, resultCode }
2898
+ };
2444
2899
  }
2445
- } catch {
2446
- this._setTransactionState({ step: "error", ...stateExtra });
2900
+ const details = error?.details;
2901
+ this._setTransactionState({
2902
+ step: "error",
2903
+ phase: "signing-submitting",
2904
+ ...buildData && { buildData },
2905
+ ...details && { details }
2906
+ });
2907
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2908
+ } catch (err) {
2909
+ const details = err instanceof Error ? err.message : void 0;
2910
+ this._setTransactionState({
2911
+ step: "error",
2912
+ phase: "signing-submitting",
2913
+ ...buildData && { buildData },
2914
+ ...details && { details }
2915
+ });
2916
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2447
2917
  }
2448
2918
  }
2919
+ /**
2920
+ * One-shot: build → sign → submit, returning the final {@link SubmitOutcome}.
2921
+ *
2922
+ * - **External wallets**: composes `buildTx` + `signAndSubmitTx` client-side.
2923
+ * State machine sees the full granular sequence (`building → built →
2924
+ * signing → signed → submitting → success`) because each composed call
2925
+ * emits its own transitions.
2926
+ * - **Custodial wallets**: single round-trip to `/tx/build-sign-submit`. The
2927
+ * signed XDR never leaves the backend. State machine emits the compound
2928
+ * `building-signing-submitting` step (the SDK can't observe individual
2929
+ * phase boundaries inside one atomic call) and then transitions to
2930
+ * `submitted` / `success` / `error[phase: 'building-signing-submitting']`.
2931
+ *
2932
+ * If you need granular UI feedback for custodial flows (separate
2933
+ * "Building…", "Signing…", "Submitting…" indicators), call `buildTx`,
2934
+ * `signTx`, and `submitTx` separately instead.
2935
+ */
2936
+ async buildAndSignAndSubmitTx(operation, params, options) {
2937
+ if (this._walletAdapter) {
2938
+ const built = await this.buildTx(operation, params, options);
2939
+ if (built.status === "error") {
2940
+ return { status: "error", ...built.details && { details: built.details } };
2941
+ }
2942
+ return this.signAndSubmitTx(built.buildData.unsignedXdr);
2943
+ }
2944
+ if (!this._session?.wallet?.publicKey) {
2945
+ this._setTransactionState({ step: "error", phase: "building-signing-submitting", details: "No wallet connected" });
2946
+ return { status: "error", details: "No wallet connected" };
2947
+ }
2948
+ this._setTransactionState({ step: "building-signing-submitting" });
2949
+ try {
2950
+ const { data, error } = await this._api.POST("/tx/build-sign-submit", {
2951
+ body: {
2952
+ network: this.getNetwork(),
2953
+ publicKey: this._session.wallet.publicKey,
2954
+ operation,
2955
+ params,
2956
+ options: options ?? {}
2957
+ }
2958
+ });
2959
+ if (!error && data?.success && data.content) {
2960
+ const { hash, status: backendStatus, resultCode } = data.content;
2961
+ if (backendStatus === "SUCCESS") {
2962
+ this._setTransactionState({ step: "success", hash });
2963
+ return { status: "success", hash };
2964
+ }
2965
+ if (backendStatus === "PENDING") {
2966
+ this._setTransactionState({ step: "submitted", hash });
2967
+ return { status: "pending", hash };
2968
+ }
2969
+ this._setTransactionState({
2970
+ step: "error",
2971
+ phase: "building-signing-submitting",
2972
+ ...resultCode && { details: resultCode }
2973
+ });
2974
+ return { status: "error", hash, ...resultCode && { details: resultCode, resultCode } };
2975
+ }
2976
+ const details = error?.details;
2977
+ this._setTransactionState({
2978
+ step: "error",
2979
+ phase: "building-signing-submitting",
2980
+ ...details && { details }
2981
+ });
2982
+ return { status: "error", ...details && { details } };
2983
+ } catch (err) {
2984
+ const details = err instanceof Error ? err.message : void 0;
2985
+ this._setTransactionState({
2986
+ step: "error",
2987
+ phase: "building-signing-submitting",
2988
+ ...details && { details }
2989
+ });
2990
+ return { status: "error", ...details && { details } };
2991
+ }
2992
+ }
2993
+ /** Alias for {@link buildAndSignAndSubmitTx} — shorter "just do the thing" name. */
2994
+ async runTx(operation, params, options) {
2995
+ return this.buildAndSignAndSubmitTx(operation, params, options);
2996
+ }
2449
2997
  // ─── App config ───────────────────────────────────────────────────────────
2450
2998
  async getAppConfig() {
2451
2999
  try {
@@ -2512,6 +3060,11 @@ var PollarClient = class {
2512
3060
  _flowDeps(signal) {
2513
3061
  return {
2514
3062
  api: this._api,
3063
+ basePath: this.basePath,
3064
+ // SSE status streaming works on web; React Native's `fetch` has no
3065
+ // readable `response.body`, so those clients poll the non-streaming
3066
+ // status endpoint instead. `isBrowser` is false in RN and SSR alike.
3067
+ useStreaming: isBrowser,
2515
3068
  signal,
2516
3069
  setAuthState: this._setAuthState.bind(this),
2517
3070
  storeSession: this._storeSession.bind(this),
@@ -2533,7 +3086,22 @@ var PollarClient = class {
2533
3086
  */
2534
3087
  async _resolveWalletAdapter(id) {
2535
3088
  if (this._walletAdapterResolver) {
2536
- return Promise.resolve(this._walletAdapterResolver(id));
3089
+ const timeoutMs = this._walletResolverTimeoutMs;
3090
+ let timeoutHandle;
3091
+ const timeoutPromise = new Promise((_, reject) => {
3092
+ timeoutHandle = setTimeout(() => {
3093
+ reject(
3094
+ Object.assign(new Error(`[PollarClient] Wallet adapter resolver for "${id}" timed out after ${timeoutMs}ms`), {
3095
+ code: AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT
3096
+ })
3097
+ );
3098
+ }, timeoutMs);
3099
+ });
3100
+ try {
3101
+ return await Promise.race([Promise.resolve(this._walletAdapterResolver(id)), timeoutPromise]);
3102
+ } finally {
3103
+ if (timeoutHandle !== void 0) clearTimeout(timeoutHandle);
3104
+ }
2537
3105
  }
2538
3106
  if (id === "freighter" /* FREIGHTER */) return new FreighterAdapter();
2539
3107
  if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter();
@@ -2547,6 +3115,16 @@ var PollarClient = class {
2547
3115
  this._setAuthState({ step: "idle" });
2548
3116
  return;
2549
3117
  }
3118
+ if (error instanceof Error && error.code === AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT) {
3119
+ console.error("[PollarClient]", error.message);
3120
+ this._setAuthState({
3121
+ step: "error",
3122
+ previousStep: this._authState.step,
3123
+ message: error.message,
3124
+ errorCode: AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT
3125
+ });
3126
+ return;
3127
+ }
2550
3128
  console.error("[PollarClient] Unexpected error in auth flow", error);
2551
3129
  this._setAuthState({
2552
3130
  step: "error",
@@ -2568,6 +3146,7 @@ var PollarClient = class {
2568
3146
  }
2569
3147
  console.info("[PollarClient] Session restored from storage");
2570
3148
  this._setAuthState({ step: "authenticated", session: this._session });
3149
+ this._scheduleNextRefresh();
2571
3150
  } else {
2572
3151
  console.info("[PollarClient] No session in storage");
2573
3152
  }
@@ -2594,9 +3173,11 @@ var PollarClient = class {
2594
3173
  }
2595
3174
  await writeStorage(this._storage, this.apiKeyHash, persisted);
2596
3175
  this._setAuthState({ step: "authenticated", session: persisted });
3176
+ this._scheduleNextRefresh();
2597
3177
  }
2598
3178
  async _clearSession() {
2599
3179
  console.info("[PollarClient] Session cleared");
3180
+ this._clearRefreshTimer();
2600
3181
  this._session = null;
2601
3182
  this._profile = null;
2602
3183
  this._walletAdapter = null;
@@ -2629,6 +3210,46 @@ var PollarClient = class {
2629
3210
  console.info(`[PollarClient] transaction:${next.step}`);
2630
3211
  for (const cb of this._transactionStateListeners) cb(next);
2631
3212
  }
3213
+ /**
3214
+ * Threads `buildData` through state transitions. When the user has already
3215
+ * called `buildTx`, every subsequent state (signing, signed, submitting,
3216
+ * submitted, success, error) should carry the build summary so modal UIs
3217
+ * can keep showing "Send 5 USDC to G..." through the whole flow.
3218
+ */
3219
+ _currentBuildData() {
3220
+ const s = this._transactionState;
3221
+ if (!s) return void 0;
3222
+ if ("buildData" in s && s.buildData) return s.buildData;
3223
+ return void 0;
3224
+ }
3225
+ };
3226
+
3227
+ // src/stellar/StellarClient.ts
3228
+ var HORIZON_URLS = {
3229
+ mainnet: "https://horizon.stellar.org",
3230
+ testnet: "https://horizon-testnet.stellar.org"
3231
+ };
3232
+ var StellarClient = class {
3233
+ constructor(config) {
3234
+ this.horizonUrl = typeof config === "string" ? HORIZON_URLS[config] : config.horizonUrl;
3235
+ }
3236
+ async submitTransaction(signedXdr) {
3237
+ try {
3238
+ const response = await fetch(`${this.horizonUrl}/transactions`, {
3239
+ method: "POST",
3240
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
3241
+ body: new URLSearchParams({ tx: signedXdr })
3242
+ });
3243
+ if (!response.ok) {
3244
+ const body = await response.json().catch(() => ({}));
3245
+ return { success: false, errorCode: body.extras?.result_codes?.transaction ?? "HORIZON_ERROR" };
3246
+ }
3247
+ const data = await response.json();
3248
+ return { success: true, hash: data.hash };
3249
+ } catch {
3250
+ return { success: false, errorCode: "NETWORK_ERROR" };
3251
+ }
3252
+ }
2632
3253
  };
2633
3254
 
2634
3255
  // src/index.rn.ts