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