@pollar/core 0.7.1 → 0.8.0

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 } = {}) {
@@ -1166,34 +1168,6 @@ function generateJti() {
1166
1168
  );
1167
1169
  }
1168
1170
 
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
-
1197
1171
  // src/storage/web.ts
1198
1172
  var LOG_PREFIX = "[PollarClient:storage]";
1199
1173
  function createMemoryAdapter() {
@@ -1281,6 +1255,57 @@ function defaultStorage(options = {}) {
1281
1255
  return createLocalStorageAdapter(options);
1282
1256
  }
1283
1257
 
1258
+ // src/visibility/noop.ts
1259
+ function createNoopVisibilityProvider() {
1260
+ return {
1261
+ isVisible: () => true,
1262
+ onChange: () => () => {
1263
+ }
1264
+ };
1265
+ }
1266
+
1267
+ // src/visibility/web.ts
1268
+ function createWebVisibilityProvider() {
1269
+ const isVisibleNow = () => typeof document === "undefined" || document.visibilityState === "visible";
1270
+ return {
1271
+ isVisible: isVisibleNow,
1272
+ onChange: (cb) => {
1273
+ if (typeof window === "undefined" || typeof document === "undefined") {
1274
+ return () => {
1275
+ };
1276
+ }
1277
+ let last = isVisibleNow();
1278
+ const handler = () => {
1279
+ const next = isVisibleNow();
1280
+ if (next !== last) {
1281
+ last = next;
1282
+ cb(next);
1283
+ }
1284
+ };
1285
+ document.addEventListener("visibilitychange", handler);
1286
+ window.addEventListener("pageshow", handler);
1287
+ window.addEventListener("pagehide", handler);
1288
+ window.addEventListener("focus", handler);
1289
+ window.addEventListener("blur", handler);
1290
+ return () => {
1291
+ document.removeEventListener("visibilitychange", handler);
1292
+ window.removeEventListener("pageshow", handler);
1293
+ window.removeEventListener("pagehide", handler);
1294
+ window.removeEventListener("focus", handler);
1295
+ window.removeEventListener("blur", handler);
1296
+ };
1297
+ }
1298
+ };
1299
+ }
1300
+
1301
+ // src/visibility/autodetect.ts
1302
+ function defaultVisibilityProvider() {
1303
+ if (typeof document !== "undefined" && typeof window !== "undefined") {
1304
+ return createWebVisibilityProvider();
1305
+ }
1306
+ return createNoopVisibilityProvider();
1307
+ }
1308
+
1284
1309
  // src/types.ts
1285
1310
  var AUTH_ERROR_CODES = {
1286
1311
  SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
@@ -1291,6 +1316,7 @@ var AUTH_ERROR_CODES = {
1291
1316
  AUTH_FAILED: "AUTH_FAILED",
1292
1317
  WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
1293
1318
  WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
1319
+ WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
1294
1320
  UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
1295
1321
  };
1296
1322
  var PollarFlowError = class extends Error {
@@ -1681,7 +1707,7 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
1681
1707
  setAuthState({ step: "authenticating" });
1682
1708
  await streamUntilFound(api, clientSessionId, (data2) => data2?.status === "READY", 200, signal);
1683
1709
  const dpopJwk = await deps.getPublicJwk();
1684
- const { data, error } = await api.POST("/auth/login", {
1710
+ const { data } = await api.POST("/auth/login", {
1685
1711
  body: {
1686
1712
  clientSessionId,
1687
1713
  dpopJwk,
@@ -1846,7 +1872,7 @@ async function loginWallet(type, deps) {
1846
1872
  let connectedWallet;
1847
1873
  try {
1848
1874
  setAuthState({ step: "connecting_wallet", walletType: type });
1849
- const adapter = await deps.resolveWalletAdapter(type);
1875
+ const adapter = await withSignal(deps.resolveWalletAdapter(type), signal);
1850
1876
  const available = await withSignal(adapter.isAvailable(), signal);
1851
1877
  if (!available) {
1852
1878
  setAuthState({ step: "wallet_not_installed", walletType: type });
@@ -1883,6 +1909,7 @@ async function loginWallet(type, deps) {
1883
1909
 
1884
1910
  // src/client/client.ts
1885
1911
  var isBrowser = typeof window !== "undefined" && typeof localStorage !== "undefined";
1912
+ var REFRESH_SKEW_SECONDS = 60;
1886
1913
  function warnServerSide(method) {
1887
1914
  console.warn(
1888
1915
  `[PollarClient] ${method}() called server-side \u2014 browser APIs unavailable. Use PollarClient only in Client Components.`
@@ -1910,6 +1937,11 @@ var PollarClient = class {
1910
1937
  /** Singleton in-flight refresh — concurrent 401s coalesce into one /auth/refresh call. */
1911
1938
  this._refreshPromise = null;
1912
1939
  this._storageEventHandler = null;
1940
+ /** Updated by the request middleware. Read by the silent-refresh scheduler
1941
+ * to skip proactive refreshes after `maxIdleMs` of no HTTP activity. */
1942
+ this._lastRequestAt = Date.now();
1943
+ this._refreshTimer = null;
1944
+ this._visibilityUnsubscribe = null;
1913
1945
  this._transactionState = null;
1914
1946
  this._transactionStateListeners = /* @__PURE__ */ new Set();
1915
1947
  this._txHistoryState = { step: "idle" };
@@ -1920,15 +1952,32 @@ var PollarClient = class {
1920
1952
  this._authStateListeners = /* @__PURE__ */ new Set();
1921
1953
  this._networkState = { step: "idle" };
1922
1954
  this._networkStateListeners = /* @__PURE__ */ new Set();
1955
+ /**
1956
+ * Latched once the storage adapter degrades. We dedupe (the adapter only
1957
+ * fires once anyway) and use it to replay state to late-subscribers — same
1958
+ * pattern as `onAuthStateChange` replaying `_authState` on subscribe.
1959
+ * Only populated when the SDK constructed the default storage adapter; if
1960
+ * the consumer passes `config.storage`, they own degradation notifications.
1961
+ */
1962
+ this._storageDegraded = null;
1963
+ this._storageDegradeListeners = /* @__PURE__ */ new Set();
1923
1964
  this._walletAdapter = null;
1924
1965
  this._loginController = null;
1925
1966
  this.apiKey = config.apiKey;
1926
1967
  this.id = crypto.randomUUID();
1927
1968
  this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
1928
- this._storage = config.storage ?? defaultStorage(config.onStorageDegrade ? { onDegrade: config.onStorageDegrade } : void 0);
1969
+ this._storage = config.storage ?? defaultStorage({
1970
+ onDegrade: (reason, error) => {
1971
+ config.onStorageDegrade?.(reason, error);
1972
+ this._dispatchStorageDegrade(reason, error);
1973
+ }
1974
+ });
1929
1975
  this._keyManager = config.keyManager ?? defaultKeyManager(this._storage, config.apiKey);
1930
1976
  this._walletAdapterResolver = config.walletAdapter ?? null;
1977
+ this._walletResolverTimeoutMs = config.walletResolverTimeoutMs ?? 5e3;
1931
1978
  this._deviceLabel = config.deviceLabel;
1979
+ this._visibilityProvider = config.visibilityProvider ?? defaultVisibilityProvider();
1980
+ this._maxIdleMs = config.maxIdleMs;
1932
1981
  this._api = createApiClient(this.basePath);
1933
1982
  this._wireMiddlewares();
1934
1983
  this._networkState = { step: "connected", network: config.stellarNetwork ?? "testnet" };
@@ -1974,6 +2023,9 @@ var PollarClient = class {
1974
2023
  console.warn("[PollarClient] KeyManager init failed; DPoP unavailable for this session", err);
1975
2024
  }
1976
2025
  await this._restoreSession();
2026
+ this._visibilityUnsubscribe = this._visibilityProvider.onChange((visible) => {
2027
+ if (visible) void this._maybeProactiveRefresh();
2028
+ });
1977
2029
  }
1978
2030
  /** Detach the cross-tab storage listener and abort any in-flight login. */
1979
2031
  destroy() {
@@ -1983,6 +2035,11 @@ var PollarClient = class {
1983
2035
  }
1984
2036
  this._loginController?.abort();
1985
2037
  this._loginController = null;
2038
+ this._clearRefreshTimer();
2039
+ if (this._visibilityUnsubscribe) {
2040
+ this._visibilityUnsubscribe();
2041
+ this._visibilityUnsubscribe = null;
2042
+ }
1986
2043
  }
1987
2044
  // ─── Middlewares (DPoP + auto-refresh) ────────────────────────────────────
1988
2045
  _wireMiddlewares() {
@@ -1990,6 +2047,7 @@ var PollarClient = class {
1990
2047
  this._api.use({
1991
2048
  onRequest: async ({ request }) => {
1992
2049
  request.headers.set("x-pollar-api-key", self.apiKey);
2050
+ self._lastRequestAt = Date.now();
1993
2051
  await self._initialized;
1994
2052
  if (request.body !== null) {
1995
2053
  try {
@@ -2020,15 +2078,22 @@ var PollarClient = class {
2020
2078
  const newNonce = response.headers.get("DPoP-Nonce");
2021
2079
  if (newNonce) self._dpopNonce = newNonce;
2022
2080
  if (response.status !== 401) return response;
2023
- if (request.url.includes("/auth/refresh")) return response;
2024
2081
  const wwwAuth = response.headers.get("WWW-Authenticate") ?? "";
2025
2082
  const isNonceChallenge = wwwAuth.includes("use_dpop_nonce");
2083
+ if (request.url.includes("/auth/refresh")) {
2084
+ if (isNonceChallenge) return self._retryRequest(request);
2085
+ return response;
2086
+ }
2026
2087
  if (!isNonceChallenge) {
2027
2088
  try {
2028
2089
  await self.refresh();
2029
2090
  } catch {
2030
2091
  return response;
2031
2092
  }
2093
+ const method = request.method.toUpperCase();
2094
+ if (method !== "GET" && method !== "HEAD") {
2095
+ return response;
2096
+ }
2032
2097
  }
2033
2098
  return self._retryRequest(request);
2034
2099
  }
@@ -2053,14 +2118,22 @@ var PollarClient = class {
2053
2118
  }
2054
2119
  async _retryRequest(originalRequest) {
2055
2120
  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}`);
2121
+ const isRefresh = originalRequest.url.includes("/auth/refresh");
2122
+ if (isRefresh) {
2123
+ const proof = await this._buildProofForRequest(originalRequest, void 0);
2124
+ headers.delete("Authorization");
2125
+ if (proof) headers.set("DPoP", proof);
2126
+ else headers.delete("DPoP");
2127
+ } else {
2128
+ const accessToken = this._session?.token?.accessToken;
2129
+ if (accessToken) {
2130
+ const proof = await this._buildProofForRequest(originalRequest, accessToken);
2131
+ if (proof) {
2132
+ headers.set("Authorization", `DPoP ${accessToken}`);
2133
+ headers.set("DPoP", proof);
2134
+ } else {
2135
+ headers.set("Authorization", `Bearer ${accessToken}`);
2136
+ }
2064
2137
  }
2065
2138
  }
2066
2139
  const cachedBody = this._requestBodyCache.get(originalRequest);
@@ -2131,6 +2204,65 @@ var PollarClient = class {
2131
2204
  } catch (err) {
2132
2205
  console.error("[PollarClient] Failed to persist refreshed session", err);
2133
2206
  }
2207
+ this._scheduleNextRefresh();
2208
+ }
2209
+ }
2210
+ // ─── Silent refresh scheduler ────────────────────────────────────────────────
2211
+ /**
2212
+ * Arm a single setTimeout to fire shortly before the current access token
2213
+ * expires. Idempotent — clearing any previous timer first. Safe to call
2214
+ * from any session-write site (initial login, restore-from-storage, after
2215
+ * a successful rotation). No-op if there's no session in memory.
2216
+ *
2217
+ * Browser/RN background-tab throttling makes long-running setTimeouts
2218
+ * unreliable on their own; the `visibilitychange` listener compensates by
2219
+ * re-invoking `_maybeProactiveRefresh` whenever the app comes back to the
2220
+ * foreground, catching any timer that fired late or never fired at all.
2221
+ */
2222
+ _scheduleNextRefresh() {
2223
+ this._clearRefreshTimer();
2224
+ const expiresAt = this._session?.token?.expiresAt;
2225
+ if (typeof expiresAt !== "number") return;
2226
+ const dueInMs = Math.max(0, (expiresAt - Math.floor(Date.now() / 1e3) - REFRESH_SKEW_SECONDS) * 1e3);
2227
+ this._refreshTimer = setTimeout(() => {
2228
+ void this._maybeProactiveRefresh();
2229
+ }, dueInMs);
2230
+ }
2231
+ /**
2232
+ * Decide whether to actually run a refresh right now. Called both from the
2233
+ * scheduler timer and from the visibility-change listener.
2234
+ *
2235
+ * Skip if:
2236
+ * - no session / no RT (nothing to refresh)
2237
+ * - app is hidden — wait for the visibility listener to re-trigger us
2238
+ * - `maxIdleMs` configured and no client request since that window — let
2239
+ * the next reactive 401-refresh handle it whenever the user comes back
2240
+ * - the AT still has more than `REFRESH_SKEW_SECONDS` of life — reschedule
2241
+ *
2242
+ * Otherwise call `refresh()`, which uses the existing in-flight singleton
2243
+ * so we never collide with a reactive 401-triggered refresh. On failure,
2244
+ * `_doRefresh` already calls `_clearSession`, so auth-state listeners see
2245
+ * `step:'idle'` — no extra event dispatch needed here.
2246
+ */
2247
+ async _maybeProactiveRefresh() {
2248
+ if (!this._session?.token?.refreshToken) return;
2249
+ if (!this._visibilityProvider.isVisible()) return;
2250
+ if (this._maxIdleMs !== void 0 && Date.now() - this._lastRequestAt > this._maxIdleMs) return;
2251
+ const expiresAt = this._session.token.expiresAt;
2252
+ if (Math.floor(Date.now() / 1e3) < expiresAt - REFRESH_SKEW_SECONDS) {
2253
+ this._scheduleNextRefresh();
2254
+ return;
2255
+ }
2256
+ try {
2257
+ await this.refresh();
2258
+ } catch (err) {
2259
+ console.warn("[PollarClient] Proactive refresh failed; session cleared", err);
2260
+ }
2261
+ }
2262
+ _clearRefreshTimer() {
2263
+ if (this._refreshTimer !== null) {
2264
+ clearTimeout(this._refreshTimer);
2265
+ this._refreshTimer = null;
2134
2266
  }
2135
2267
  }
2136
2268
  // ─── Auth state ──────────────────────────────────────────────────────────────
@@ -2142,6 +2274,38 @@ var PollarClient = class {
2142
2274
  cb(this._authState);
2143
2275
  return () => this._authStateListeners.delete(cb);
2144
2276
  }
2277
+ /**
2278
+ * Subscribe to persistent-storage degradation (Safari private mode,
2279
+ * sandboxed iframes, quota errors, etc.). The SDK keeps running off
2280
+ * in-memory storage after degrade, but sessions won't survive reload — a
2281
+ * host UI typically wants to show "your session won't be saved" so the
2282
+ * user isn't blindsided after a refresh.
2283
+ *
2284
+ * Fires at most once per client lifetime (the underlying adapter dedupes).
2285
+ * Late subscribers receive the latched state synchronously on subscribe.
2286
+ *
2287
+ * Only fires when the SDK constructs the default storage adapter. If you
2288
+ * pass a custom `config.storage`, wire your own notification path through
2289
+ * that adapter's API — the SDK has no hook into it.
2290
+ */
2291
+ onStorageDegrade(cb) {
2292
+ this._storageDegradeListeners.add(cb);
2293
+ if (this._storageDegraded) {
2294
+ cb(this._storageDegraded.reason, this._storageDegraded.error);
2295
+ }
2296
+ return () => this._storageDegradeListeners.delete(cb);
2297
+ }
2298
+ _dispatchStorageDegrade(reason, error) {
2299
+ if (this._storageDegraded) return;
2300
+ this._storageDegraded = { reason, error };
2301
+ for (const cb of this._storageDegradeListeners) {
2302
+ try {
2303
+ cb(reason, error);
2304
+ } catch (err) {
2305
+ console.error("[PollarClient] onStorageDegrade listener threw", err);
2306
+ }
2307
+ }
2308
+ }
2145
2309
  /** PII (email, names, avatar, providers). Held in memory only — never persisted. */
2146
2310
  getUserProfile() {
2147
2311
  return this._profile;
@@ -2378,10 +2542,16 @@ var PollarClient = class {
2378
2542
  }
2379
2543
  }
2380
2544
  // ─── Transactions ─────────────────────────────────────────────────────────
2545
+ /**
2546
+ * Builds an unsigned XDR. Drives `_setTransactionState` for modal-style UIs
2547
+ * AND returns a {@link BuildOutcome} so headless callers can `await` and
2548
+ * inspect the result without subscribing to state changes.
2549
+ */
2381
2550
  async buildTx(operation, params, options) {
2382
2551
  if (!this._session?.wallet?.publicKey) {
2383
- this._setTransactionState({ step: "error", details: "No wallet connected" });
2384
- return;
2552
+ const details = "No wallet connected";
2553
+ this._setTransactionState({ step: "error", phase: "building", details });
2554
+ return { status: "error", details };
2385
2555
  }
2386
2556
  const body = {
2387
2557
  network: this.getNetwork(),
@@ -2395,40 +2565,194 @@ var PollarClient = class {
2395
2565
  const { data, error } = await this._api.POST("/tx/build", { body });
2396
2566
  if (!error && data?.success && data.content) {
2397
2567
  this._setTransactionState({ step: "built", buildData: data.content });
2398
- } else {
2399
- const details = error?.details;
2400
- this._setTransactionState({ step: "error", ...details && { details } });
2568
+ return { status: "built", buildData: data.content };
2401
2569
  }
2570
+ const details = error?.details;
2571
+ this._setTransactionState({ step: "error", phase: "building", ...details && { details } });
2572
+ return { status: "error", ...details && { details } };
2402
2573
  } catch (err) {
2403
2574
  console.error("[PollarClient] buildTx failed", err);
2404
- this._setTransactionState({ step: "error" });
2575
+ this._setTransactionState({ step: "error", phase: "building" });
2576
+ return { status: "error" };
2405
2577
  }
2406
2578
  }
2407
2579
  getWalletType() {
2408
2580
  return this._walletAdapter?.type ?? null;
2409
2581
  }
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;
2582
+ /**
2583
+ * Signs the given unsigned XDR and returns the signed XDR.
2584
+ *
2585
+ * - External wallets: signs locally via the wallet adapter.
2586
+ * - Custodial wallets: posts to `/tx/sign`. The backend signs (through
2587
+ * wallet-service or the app's customer-managed adapter) and returns the
2588
+ * signed XDR plus an `idempotencyKey` the caller should echo back to
2589
+ * `submitTx`.
2590
+ *
2591
+ * Drives `_setTransactionState`: emits `signing` while in flight and
2592
+ * `signed` on success (or `error[phase: 'signing']` on failure). `buildData`
2593
+ * is threaded through if the consumer previously called `buildTx`.
2594
+ */
2595
+ async signTx(unsignedXdr) {
2596
+ const buildData = this._currentBuildData();
2597
+ this._setTransactionState({ step: "signing", ...buildData && { buildData } });
2416
2598
  if (this._walletAdapter) {
2599
+ const accountToSign = this._session?.wallet?.publicKey;
2600
+ const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
2417
2601
  try {
2418
- const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
2419
2602
  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 });
2603
+ this._setTransactionState({
2604
+ step: "signed",
2605
+ signedXdr: signedTxXdr,
2606
+ ...buildData && { buildData }
2607
+ });
2608
+ return { status: "signed", signedXdr: signedTxXdr };
2609
+ } catch (err) {
2610
+ const details = err instanceof Error ? err.message : void 0;
2611
+ this._setTransactionState({
2612
+ step: "error",
2613
+ phase: "signing",
2614
+ ...buildData && { buildData },
2615
+ ...details && { details }
2616
+ });
2617
+ return { status: "error", ...details && { details } };
2618
+ }
2619
+ }
2620
+ const publicKey = this._session?.wallet?.publicKey ?? "";
2621
+ try {
2622
+ const { data, error } = await this._api.POST("/tx/sign", {
2623
+ body: { network: this.getNetwork(), publicKey, unsignedXdr }
2624
+ });
2625
+ if (!error && data?.success && data.content?.signedXdr) {
2626
+ const { signedXdr, idempotencyKey } = data.content;
2627
+ this._setTransactionState({
2628
+ step: "signed",
2629
+ signedXdr,
2630
+ submissionToken: idempotencyKey,
2631
+ ...buildData && { buildData }
2632
+ });
2633
+ return { status: "signed", signedXdr, submissionToken: idempotencyKey };
2634
+ }
2635
+ const details = error?.details;
2636
+ this._setTransactionState({
2637
+ step: "error",
2638
+ phase: "signing",
2639
+ ...buildData && { buildData },
2640
+ ...details && { details }
2641
+ });
2642
+ return { status: "error", ...details && { details } };
2643
+ } catch (err) {
2644
+ const details = err instanceof Error ? err.message : void 0;
2645
+ this._setTransactionState({
2646
+ step: "error",
2647
+ phase: "signing",
2648
+ ...buildData && { buildData },
2649
+ ...details && { details }
2650
+ });
2651
+ return { status: "error", ...details && { details } };
2652
+ }
2653
+ }
2654
+ /**
2655
+ * Submits a signed XDR via `/tx/submit` regardless of wallet type
2656
+ * (custodial or external). Routing through sdk-api gives us:
2657
+ * - End-to-end tx_records persistence with full phase lifecycle so the
2658
+ * developer dashboard can show every tx (both custodial and external
2659
+ * wallet flows) at `/apps/:id/monitor/transactions`.
2660
+ * - Idempotency tracking via `submissionToken` (returned by `signTx`).
2661
+ * - A single response shape (SUCCESS / PENDING / FAILED) shared by both
2662
+ * flows — previously external wallets could only return SUCCESS or
2663
+ * error since the direct-to-Horizon path was synchronous.
2664
+ *
2665
+ * The extra hop adds ~50–150 ms vs. the legacy direct-Horizon path; the
2666
+ * persistence + observability win is worth it.
2667
+ *
2668
+ * Drives `_setTransactionState`: emits `submitting` while in flight,
2669
+ * `submitted` on Horizon ack (pending), `success` on ledger confirmation,
2670
+ * or `error[phase: 'submitting']` on failure.
2671
+ */
2672
+ async submitTx(signedXdr, opts) {
2673
+ const buildData = this._currentBuildData();
2674
+ const outcomeExtra = buildData ? { buildData } : {};
2675
+ this._setTransactionState({ step: "submitting", signedXdr, ...buildData && { buildData } });
2676
+ const publicKey = this._session?.wallet?.publicKey ?? "";
2677
+ try {
2678
+ const { data, error } = await this._api.POST("/tx/submit", {
2679
+ body: {
2680
+ network: this.getNetwork(),
2681
+ publicKey,
2682
+ signedXdr,
2683
+ ...opts?.submissionToken && { idempotencyKey: opts.submissionToken }
2426
2684
  }
2427
- } catch {
2428
- this._setTransactionState({ step: "error", ...stateExtra });
2685
+ });
2686
+ if (!error && data?.success && data.content) {
2687
+ const { hash, status: backendStatus, resultCode } = data.content;
2688
+ if (backendStatus === "SUCCESS") {
2689
+ this._setTransactionState({ step: "success", hash, ...buildData && { buildData } });
2690
+ return { status: "success", hash, ...outcomeExtra };
2691
+ }
2692
+ if (backendStatus === "PENDING") {
2693
+ this._setTransactionState({ step: "submitted", hash, ...buildData && { buildData } });
2694
+ return { status: "pending", hash, ...outcomeExtra };
2695
+ }
2696
+ this._setTransactionState({
2697
+ step: "error",
2698
+ phase: "submitting",
2699
+ ...buildData && { buildData },
2700
+ ...resultCode && { details: resultCode }
2701
+ });
2702
+ return {
2703
+ status: "error",
2704
+ hash,
2705
+ ...outcomeExtra,
2706
+ ...resultCode && { details: resultCode, resultCode }
2707
+ };
2429
2708
  }
2430
- return;
2709
+ const details = error?.details;
2710
+ this._setTransactionState({
2711
+ step: "error",
2712
+ phase: "submitting",
2713
+ ...buildData && { buildData },
2714
+ ...details && { details }
2715
+ });
2716
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2717
+ } catch (err) {
2718
+ const details = err instanceof Error ? err.message : void 0;
2719
+ this._setTransactionState({
2720
+ step: "error",
2721
+ phase: "submitting",
2722
+ ...buildData && { buildData },
2723
+ ...details && { details }
2724
+ });
2725
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2726
+ }
2727
+ }
2728
+ /**
2729
+ * Signs and submits in one logical step. Returns a {@link SubmitOutcome}.
2730
+ *
2731
+ * - **External wallets**: composes `signTx` + `submitTx` client-side. State
2732
+ * machine sees the full granular sequence `signing → signed → submitting
2733
+ * → success` because the underlying methods each emit.
2734
+ * - **Custodial wallets**: atomic `/tx/sign-and-send` round-trip. State
2735
+ * machine emits the compound `signing-submitting` step (the SDK can't
2736
+ * observe when one phase ends and the next begins inside that single
2737
+ * backend call) and then transitions to `submitted` (Horizon ack only) or
2738
+ * `success` (ledger-confirmed), or `error[phase: 'signing-submitting']`.
2739
+ */
2740
+ async signAndSubmitTx(unsignedXdr) {
2741
+ if (this._walletAdapter) {
2742
+ const signed = await this.signTx(unsignedXdr);
2743
+ if (signed.status === "error") {
2744
+ const buildData2 = this._currentBuildData();
2745
+ return {
2746
+ status: "error",
2747
+ ...buildData2 && { buildData: buildData2 },
2748
+ ...signed.details && { details: signed.details }
2749
+ };
2750
+ }
2751
+ return this.submitTx(signed.signedXdr);
2431
2752
  }
2753
+ const buildData = this._currentBuildData();
2754
+ const outcomeExtra = buildData ? { buildData } : {};
2755
+ this._setTransactionState({ step: "signing-submitting", ...buildData && { buildData } });
2432
2756
  const body = {
2433
2757
  network: this.getNetwork(),
2434
2758
  publicKey: this._session?.wallet?.publicKey ?? "",
@@ -2437,15 +2761,129 @@ var PollarClient = class {
2437
2761
  try {
2438
2762
  const { data, error } = await this._api.POST("/tx/sign-and-send", { body });
2439
2763
  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 } });
2764
+ const {
2765
+ hash,
2766
+ status: backendStatus,
2767
+ resultCode
2768
+ } = data.content;
2769
+ if (backendStatus === "SUCCESS") {
2770
+ this._setTransactionState({ step: "success", hash, ...buildData && { buildData } });
2771
+ return { status: "success", hash, ...outcomeExtra };
2772
+ }
2773
+ if (backendStatus === "PENDING") {
2774
+ this._setTransactionState({ step: "submitted", hash, ...buildData && { buildData } });
2775
+ return { status: "pending", hash, ...outcomeExtra };
2776
+ }
2777
+ this._setTransactionState({
2778
+ step: "error",
2779
+ phase: "signing-submitting",
2780
+ ...buildData && { buildData },
2781
+ ...resultCode && { details: resultCode }
2782
+ });
2783
+ return {
2784
+ status: "error",
2785
+ hash,
2786
+ ...outcomeExtra,
2787
+ ...resultCode && { details: resultCode, resultCode }
2788
+ };
2444
2789
  }
2445
- } catch {
2446
- this._setTransactionState({ step: "error", ...stateExtra });
2790
+ const details = error?.details;
2791
+ this._setTransactionState({
2792
+ step: "error",
2793
+ phase: "signing-submitting",
2794
+ ...buildData && { buildData },
2795
+ ...details && { details }
2796
+ });
2797
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2798
+ } catch (err) {
2799
+ const details = err instanceof Error ? err.message : void 0;
2800
+ this._setTransactionState({
2801
+ step: "error",
2802
+ phase: "signing-submitting",
2803
+ ...buildData && { buildData },
2804
+ ...details && { details }
2805
+ });
2806
+ return { status: "error", ...outcomeExtra, ...details && { details } };
2807
+ }
2808
+ }
2809
+ /**
2810
+ * One-shot: build → sign → submit, returning the final {@link SubmitOutcome}.
2811
+ *
2812
+ * - **External wallets**: composes `buildTx` + `signAndSubmitTx` client-side.
2813
+ * State machine sees the full granular sequence (`building → built →
2814
+ * signing → signed → submitting → success`) because each composed call
2815
+ * emits its own transitions.
2816
+ * - **Custodial wallets**: single round-trip to `/tx/build-sign-submit`. The
2817
+ * signed XDR never leaves the backend. State machine emits the compound
2818
+ * `building-signing-submitting` step (the SDK can't observe individual
2819
+ * phase boundaries inside one atomic call) and then transitions to
2820
+ * `submitted` / `success` / `error[phase: 'building-signing-submitting']`.
2821
+ *
2822
+ * If you need granular UI feedback for custodial flows (separate
2823
+ * "Building…", "Signing…", "Submitting…" indicators), call `buildTx`,
2824
+ * `signTx`, and `submitTx` separately instead.
2825
+ */
2826
+ async buildAndSignAndSubmitTx(operation, params, options) {
2827
+ if (this._walletAdapter) {
2828
+ const built = await this.buildTx(operation, params, options);
2829
+ if (built.status === "error") {
2830
+ return { status: "error", ...built.details && { details: built.details } };
2831
+ }
2832
+ return this.signAndSubmitTx(built.buildData.unsignedXdr);
2833
+ }
2834
+ if (!this._session?.wallet?.publicKey) {
2835
+ this._setTransactionState({ step: "error", phase: "building-signing-submitting", details: "No wallet connected" });
2836
+ return { status: "error", details: "No wallet connected" };
2837
+ }
2838
+ this._setTransactionState({ step: "building-signing-submitting" });
2839
+ try {
2840
+ const { data, error } = await this._api.POST("/tx/build-sign-submit", {
2841
+ body: {
2842
+ network: this.getNetwork(),
2843
+ publicKey: this._session.wallet.publicKey,
2844
+ operation,
2845
+ params,
2846
+ options: options ?? {}
2847
+ }
2848
+ });
2849
+ if (!error && data?.success && data.content) {
2850
+ const { hash, status: backendStatus, resultCode } = data.content;
2851
+ if (backendStatus === "SUCCESS") {
2852
+ this._setTransactionState({ step: "success", hash });
2853
+ return { status: "success", hash };
2854
+ }
2855
+ if (backendStatus === "PENDING") {
2856
+ this._setTransactionState({ step: "submitted", hash });
2857
+ return { status: "pending", hash };
2858
+ }
2859
+ this._setTransactionState({
2860
+ step: "error",
2861
+ phase: "building-signing-submitting",
2862
+ ...resultCode && { details: resultCode }
2863
+ });
2864
+ return { status: "error", hash, ...resultCode && { details: resultCode, resultCode } };
2865
+ }
2866
+ const details = error?.details;
2867
+ this._setTransactionState({
2868
+ step: "error",
2869
+ phase: "building-signing-submitting",
2870
+ ...details && { details }
2871
+ });
2872
+ return { status: "error", ...details && { details } };
2873
+ } catch (err) {
2874
+ const details = err instanceof Error ? err.message : void 0;
2875
+ this._setTransactionState({
2876
+ step: "error",
2877
+ phase: "building-signing-submitting",
2878
+ ...details && { details }
2879
+ });
2880
+ return { status: "error", ...details && { details } };
2447
2881
  }
2448
2882
  }
2883
+ /** Alias for {@link buildAndSignAndSubmitTx} — shorter "just do the thing" name. */
2884
+ async runTx(operation, params, options) {
2885
+ return this.buildAndSignAndSubmitTx(operation, params, options);
2886
+ }
2449
2887
  // ─── App config ───────────────────────────────────────────────────────────
2450
2888
  async getAppConfig() {
2451
2889
  try {
@@ -2533,7 +2971,22 @@ var PollarClient = class {
2533
2971
  */
2534
2972
  async _resolveWalletAdapter(id) {
2535
2973
  if (this._walletAdapterResolver) {
2536
- return Promise.resolve(this._walletAdapterResolver(id));
2974
+ const timeoutMs = this._walletResolverTimeoutMs;
2975
+ let timeoutHandle;
2976
+ const timeoutPromise = new Promise((_, reject) => {
2977
+ timeoutHandle = setTimeout(() => {
2978
+ reject(
2979
+ Object.assign(new Error(`[PollarClient] Wallet adapter resolver for "${id}" timed out after ${timeoutMs}ms`), {
2980
+ code: AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT
2981
+ })
2982
+ );
2983
+ }, timeoutMs);
2984
+ });
2985
+ try {
2986
+ return await Promise.race([Promise.resolve(this._walletAdapterResolver(id)), timeoutPromise]);
2987
+ } finally {
2988
+ if (timeoutHandle !== void 0) clearTimeout(timeoutHandle);
2989
+ }
2537
2990
  }
2538
2991
  if (id === "freighter" /* FREIGHTER */) return new FreighterAdapter();
2539
2992
  if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter();
@@ -2547,6 +3000,16 @@ var PollarClient = class {
2547
3000
  this._setAuthState({ step: "idle" });
2548
3001
  return;
2549
3002
  }
3003
+ if (error instanceof Error && error.code === AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT) {
3004
+ console.error("[PollarClient]", error.message);
3005
+ this._setAuthState({
3006
+ step: "error",
3007
+ previousStep: this._authState.step,
3008
+ message: error.message,
3009
+ errorCode: AUTH_ERROR_CODES.WALLET_RESOLVER_TIMEOUT
3010
+ });
3011
+ return;
3012
+ }
2550
3013
  console.error("[PollarClient] Unexpected error in auth flow", error);
2551
3014
  this._setAuthState({
2552
3015
  step: "error",
@@ -2568,6 +3031,7 @@ var PollarClient = class {
2568
3031
  }
2569
3032
  console.info("[PollarClient] Session restored from storage");
2570
3033
  this._setAuthState({ step: "authenticated", session: this._session });
3034
+ this._scheduleNextRefresh();
2571
3035
  } else {
2572
3036
  console.info("[PollarClient] No session in storage");
2573
3037
  }
@@ -2594,9 +3058,11 @@ var PollarClient = class {
2594
3058
  }
2595
3059
  await writeStorage(this._storage, this.apiKeyHash, persisted);
2596
3060
  this._setAuthState({ step: "authenticated", session: persisted });
3061
+ this._scheduleNextRefresh();
2597
3062
  }
2598
3063
  async _clearSession() {
2599
3064
  console.info("[PollarClient] Session cleared");
3065
+ this._clearRefreshTimer();
2600
3066
  this._session = null;
2601
3067
  this._profile = null;
2602
3068
  this._walletAdapter = null;
@@ -2629,6 +3095,46 @@ var PollarClient = class {
2629
3095
  console.info(`[PollarClient] transaction:${next.step}`);
2630
3096
  for (const cb of this._transactionStateListeners) cb(next);
2631
3097
  }
3098
+ /**
3099
+ * Threads `buildData` through state transitions. When the user has already
3100
+ * called `buildTx`, every subsequent state (signing, signed, submitting,
3101
+ * submitted, success, error) should carry the build summary so modal UIs
3102
+ * can keep showing "Send 5 USDC to G..." through the whole flow.
3103
+ */
3104
+ _currentBuildData() {
3105
+ const s = this._transactionState;
3106
+ if (!s) return void 0;
3107
+ if ("buildData" in s && s.buildData) return s.buildData;
3108
+ return void 0;
3109
+ }
3110
+ };
3111
+
3112
+ // src/stellar/StellarClient.ts
3113
+ var HORIZON_URLS = {
3114
+ mainnet: "https://horizon.stellar.org",
3115
+ testnet: "https://horizon-testnet.stellar.org"
3116
+ };
3117
+ var StellarClient = class {
3118
+ constructor(config) {
3119
+ this.horizonUrl = typeof config === "string" ? HORIZON_URLS[config] : config.horizonUrl;
3120
+ }
3121
+ async submitTransaction(signedXdr) {
3122
+ try {
3123
+ const response = await fetch(`${this.horizonUrl}/transactions`, {
3124
+ method: "POST",
3125
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
3126
+ body: new URLSearchParams({ tx: signedXdr })
3127
+ });
3128
+ if (!response.ok) {
3129
+ const body = await response.json().catch(() => ({}));
3130
+ return { success: false, errorCode: body.extras?.result_codes?.transaction ?? "HORIZON_ERROR" };
3131
+ }
3132
+ const data = await response.json();
3133
+ return { success: true, hash: data.hash };
3134
+ } catch {
3135
+ return { success: false, errorCode: "NETWORK_ERROR" };
3136
+ }
3137
+ }
2632
3138
  };
2633
3139
 
2634
3140
  // src/index.rn.ts