@virex-tech/paywallo-sdk 2.5.1 → 2.5.3

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.js CHANGED
@@ -348,13 +348,13 @@ function getCachedDeviceInfo() {
348
348
  return null;
349
349
  }
350
350
  }
351
- var import_react_native2, Device, Application, Localization, _debug2, cachedDeviceInfo, nativeDeviceInfo;
351
+ var import_react_native2, Application, Device, Localization, _debug2, cachedDeviceInfo, nativeDeviceInfo;
352
352
  var init_NativeDeviceInfo = __esm({
353
353
  "src/utils/NativeDeviceInfo.ts"() {
354
354
  "use strict";
355
355
  import_react_native2 = require("react-native");
356
- Device = __toESM(require("expo-device"));
357
356
  Application = __toESM(require("expo-application"));
357
+ Device = __toESM(require("expo-device"));
358
358
  Localization = __toESM(require("expo-localization"));
359
359
  init_ClientError();
360
360
  _debug2 = false;
@@ -968,7 +968,9 @@ var init_AdvertisingIdManager = __esm({
968
968
  return this.cached;
969
969
  }
970
970
  async collect(requestATT = false) {
971
- if (this.cached) return this.cached;
971
+ if (this.cached && !(requestATT && this.cached.attStatus === "undetermined")) {
972
+ return this.cached;
973
+ }
972
974
  const fallback = { idfv: null, idfa: null, gaid: null, attStatus: "unavailable" };
973
975
  try {
974
976
  const tracking = await import("expo-tracking-transparency");
@@ -1391,6 +1393,7 @@ var init_AttributionTracker = __esm({
1391
1393
  constructor(debug = false) {
1392
1394
  this.cache = null;
1393
1395
  this.captureInProgress = false;
1396
+ this.listeners = /* @__PURE__ */ new Set();
1394
1397
  this.storage = new SecureStorage(debug);
1395
1398
  }
1396
1399
  /**
@@ -1430,10 +1433,31 @@ var init_AttributionTracker = __esm({
1430
1433
  };
1431
1434
  await this.storage.set(ATTRIBUTION_STORAGE_KEY, JSON.stringify(capture));
1432
1435
  this.cache = capture;
1436
+ this.notifyListeners(capture);
1433
1437
  } finally {
1434
1438
  this.captureInProgress = false;
1435
1439
  }
1436
1440
  }
1441
+ /**
1442
+ * Subscribes to the first real attribution write (first-write-wins — no-op
1443
+ * captures never fire). Useful for late enrichment consumers (e.g. re-push
1444
+ * of Superwall attributes when a warm deep link / deferred match lands after
1445
+ * init). Returns an unsubscribe function.
1446
+ */
1447
+ onCapture(listener) {
1448
+ this.listeners.add(listener);
1449
+ return () => {
1450
+ this.listeners.delete(listener);
1451
+ };
1452
+ }
1453
+ notifyListeners(capture) {
1454
+ for (const listener of this.listeners) {
1455
+ try {
1456
+ listener(capture);
1457
+ } catch {
1458
+ }
1459
+ }
1460
+ }
1437
1461
  /**
1438
1462
  * Returns the persisted attribution capture, or `null` if none exists.
1439
1463
  * Synchronous — reads from in-memory cache (populated via `loadFromStorage()`).
@@ -1527,6 +1551,68 @@ var init_InstallIdempotency = __esm({
1527
1551
  }
1528
1552
  });
1529
1553
 
1554
+ // src/domains/identity/deferredAppLink.ts
1555
+ function parseQuery(s) {
1556
+ const result = {};
1557
+ for (const pair of s.split("&")) {
1558
+ const eqIndex = pair.indexOf("=");
1559
+ if (eqIndex === -1) continue;
1560
+ try {
1561
+ const key = decodeURIComponent(pair.slice(0, eqIndex).replace(/\+/g, " "));
1562
+ const value = decodeURIComponent(pair.slice(eqIndex + 1).replace(/\+/g, " "));
1563
+ if (key) result[key] = value;
1564
+ } catch {
1565
+ }
1566
+ }
1567
+ return result;
1568
+ }
1569
+ function parseDeferredAppLink(url) {
1570
+ if (!url || url.trim() === "") return null;
1571
+ const raw = url.slice(0, 2048);
1572
+ const cleanRaw = raw.split("#")[0];
1573
+ const qIndex = cleanRaw.indexOf("?");
1574
+ if (qIndex === -1) return null;
1575
+ const topParams = parseQuery(cleanRaw.slice(qIndex + 1));
1576
+ let fbclid = nz(topParams["fbclid"]);
1577
+ let utmSource = nz(topParams["utm_source"]);
1578
+ let utmMedium = nz(topParams["utm_medium"]);
1579
+ let utmCampaign = nz(topParams["utm_campaign"]);
1580
+ let ttclid = nz(topParams["ttclid"]);
1581
+ let trackingId = nz(topParams["tracking_id"]);
1582
+ let targetUrl = null;
1583
+ if (topParams["target_url"]) {
1584
+ try {
1585
+ targetUrl = decodeURIComponent(topParams["target_url"]);
1586
+ } catch {
1587
+ targetUrl = topParams["target_url"];
1588
+ }
1589
+ }
1590
+ if (!fbclid && targetUrl) {
1591
+ const cleanTargetUrl = targetUrl.split("#")[0];
1592
+ const innerQ = cleanTargetUrl.indexOf("?");
1593
+ if (innerQ !== -1) {
1594
+ const inner = parseQuery(cleanTargetUrl.slice(innerQ + 1));
1595
+ fbclid = fbclid ?? nz(inner["fbclid"]);
1596
+ utmSource = utmSource ?? nz(inner["utm_source"]);
1597
+ utmMedium = utmMedium ?? nz(inner["utm_medium"]);
1598
+ utmCampaign = utmCampaign ?? nz(inner["utm_campaign"]);
1599
+ ttclid = ttclid ?? nz(inner["ttclid"]);
1600
+ trackingId = trackingId ?? nz(inner["tracking_id"]);
1601
+ }
1602
+ }
1603
+ if (!fbclid && !ttclid && !trackingId && !utmSource && !utmMedium && !utmCampaign) {
1604
+ return null;
1605
+ }
1606
+ return { fbclid, utmSource, utmMedium, utmCampaign, ttclid, trackingId, targetUrl, raw };
1607
+ }
1608
+ var nz;
1609
+ var init_deferredAppLink = __esm({
1610
+ "src/domains/identity/deferredAppLink.ts"() {
1611
+ "use strict";
1612
+ nz = (v) => typeof v === "string" && v.trim().length > 0 ? v : null;
1613
+ }
1614
+ });
1615
+
1530
1616
  // src/domains/identity/MetaBridge.ts
1531
1617
  var import_react_native5, TIMEOUT_MS, NativeBridge, MetaBridge, metaBridge;
1532
1618
  var init_MetaBridge = __esm({
@@ -1625,68 +1711,6 @@ var init_MetaBridge = __esm({
1625
1711
  }
1626
1712
  });
1627
1713
 
1628
- // src/domains/identity/deferredAppLink.ts
1629
- function parseQuery(s) {
1630
- const result = {};
1631
- for (const pair of s.split("&")) {
1632
- const eqIndex = pair.indexOf("=");
1633
- if (eqIndex === -1) continue;
1634
- try {
1635
- const key = decodeURIComponent(pair.slice(0, eqIndex).replace(/\+/g, " "));
1636
- const value = decodeURIComponent(pair.slice(eqIndex + 1).replace(/\+/g, " "));
1637
- if (key) result[key] = value;
1638
- } catch {
1639
- }
1640
- }
1641
- return result;
1642
- }
1643
- function parseDeferredAppLink(url) {
1644
- if (!url || url.trim() === "") return null;
1645
- const raw = url.slice(0, 2048);
1646
- const cleanRaw = raw.split("#")[0];
1647
- const qIndex = cleanRaw.indexOf("?");
1648
- if (qIndex === -1) return null;
1649
- const topParams = parseQuery(cleanRaw.slice(qIndex + 1));
1650
- let fbclid = nz(topParams["fbclid"]);
1651
- let utmSource = nz(topParams["utm_source"]);
1652
- let utmMedium = nz(topParams["utm_medium"]);
1653
- let utmCampaign = nz(topParams["utm_campaign"]);
1654
- let ttclid = nz(topParams["ttclid"]);
1655
- let trackingId = nz(topParams["tracking_id"]);
1656
- let targetUrl = null;
1657
- if (topParams["target_url"]) {
1658
- try {
1659
- targetUrl = decodeURIComponent(topParams["target_url"]);
1660
- } catch {
1661
- targetUrl = topParams["target_url"];
1662
- }
1663
- }
1664
- if (!fbclid && targetUrl) {
1665
- const cleanTargetUrl = targetUrl.split("#")[0];
1666
- const innerQ = cleanTargetUrl.indexOf("?");
1667
- if (innerQ !== -1) {
1668
- const inner = parseQuery(cleanTargetUrl.slice(innerQ + 1));
1669
- fbclid = fbclid ?? nz(inner["fbclid"]);
1670
- utmSource = utmSource ?? nz(inner["utm_source"]);
1671
- utmMedium = utmMedium ?? nz(inner["utm_medium"]);
1672
- utmCampaign = utmCampaign ?? nz(inner["utm_campaign"]);
1673
- ttclid = ttclid ?? nz(inner["ttclid"]);
1674
- trackingId = trackingId ?? nz(inner["tracking_id"]);
1675
- }
1676
- }
1677
- if (!fbclid && !ttclid && !trackingId && !utmSource && !utmMedium && !utmCampaign) {
1678
- return null;
1679
- }
1680
- return { fbclid, utmSource, utmMedium, utmCampaign, ttclid, trackingId, targetUrl, raw };
1681
- }
1682
- var nz;
1683
- var init_deferredAppLink = __esm({
1684
- "src/domains/identity/deferredAppLink.ts"() {
1685
- "use strict";
1686
- nz = (v) => typeof v === "string" && v.trim().length > 0 ? v : null;
1687
- }
1688
- });
1689
-
1690
1714
  // src/domains/identity/InstallReferrerManager.ts
1691
1715
  var import_react_native6, REFERRER_TIMEOUT_MS, InstallReferrerManager, installReferrerManager;
1692
1716
  var init_InstallReferrerManager = __esm({
@@ -1694,8 +1718,8 @@ var init_InstallReferrerManager = __esm({
1694
1718
  "use strict";
1695
1719
  import_react_native6 = require("react-native");
1696
1720
  init_NativeDeviceInfo();
1697
- init_MetaBridge();
1698
1721
  init_deferredAppLink();
1722
+ init_MetaBridge();
1699
1723
  REFERRER_TIMEOUT_MS = 5e3;
1700
1724
  InstallReferrerManager = class {
1701
1725
  async getReferrer() {
@@ -1795,7 +1819,17 @@ var init_InstallReferrerManager = __esm({
1795
1819
  });
1796
1820
 
1797
1821
  // src/domains/identity/InstallTracker.ts
1798
- var import_react_native7, InstallTracker;
1822
+ function extractDeferredAttribution(raw) {
1823
+ if (raw === null || typeof raw !== "object") return null;
1824
+ const source = raw;
1825
+ const out = {};
1826
+ for (const field of DEFERRED_ATTRIBUTION_FIELDS) {
1827
+ const value = source[field];
1828
+ if (typeof value === "string" && value !== "") out[field] = value;
1829
+ }
1830
+ return Object.keys(out).length > 0 ? out : null;
1831
+ }
1832
+ var import_react_native7, InstallTracker, DEFERRED_ATTRIBUTION_FIELDS;
1799
1833
  var init_InstallTracker = __esm({
1800
1834
  "src/domains/identity/InstallTracker.ts"() {
1801
1835
  "use strict";
@@ -1961,7 +1995,7 @@ var init_InstallTracker = __esm({
1961
1995
  const baseUrl = apiClient.getBaseUrl();
1962
1996
  const appKey = apiClient.getAppKey();
1963
1997
  const referrer = await installReferrerManager.getReferrer();
1964
- await fetch(`${baseUrl}/sdk/attribution/deferred-match/${appKey}`, {
1998
+ const response = await fetch(`${baseUrl}/sdk/attribution/deferred-match/${appKey}`, {
1965
1999
  method: "POST",
1966
2000
  headers: { "Content-Type": "application/json", "X-App-Key": appKey },
1967
2001
  body: JSON.stringify({
@@ -1983,12 +2017,42 @@ var init_InstallTracker = __esm({
1983
2017
  });
1984
2018
  await storage.set(INSTALL_KEYS.DEFERRED_MATCH_DONE, "1").catch(() => {
1985
2019
  });
2020
+ await this.applyDeferredMatchResponse(response);
1986
2021
  } catch {
1987
2022
  } finally {
1988
2023
  clearTimeout(timer);
1989
2024
  }
1990
2025
  }
2026
+ async applyDeferredMatchResponse(response) {
2027
+ try {
2028
+ const body = await response.json();
2029
+ if (body === null || typeof body !== "object") return;
2030
+ const outer = body;
2031
+ const source = typeof outer.data === "object" && outer.data !== null ? outer.data : body;
2032
+ if (source.matched !== true) return;
2033
+ const attribution = extractDeferredAttribution(source.attribution);
2034
+ if (!attribution) return;
2035
+ await attributionTracker.capture({
2036
+ ...attribution,
2037
+ installReferrerSource: "deferred_match"
2038
+ });
2039
+ } catch {
2040
+ }
2041
+ }
1991
2042
  };
2043
+ DEFERRED_ATTRIBUTION_FIELDS = [
2044
+ "fbclid",
2045
+ "gclid",
2046
+ "ttclid",
2047
+ "utmSource",
2048
+ "utmMedium",
2049
+ "utmCampaign",
2050
+ "utmContent",
2051
+ "utmTerm",
2052
+ "tiktokCampaignId",
2053
+ "tiktokAdgroupId",
2054
+ "tiktokAdId"
2055
+ ];
1992
2056
  }
1993
2057
  });
1994
2058
 
@@ -2563,8 +2627,8 @@ var init_NotificationHandlerSetup = __esm({
2563
2627
  // src/core/version.ts
2564
2628
  function resolveVersion() {
2565
2629
  try {
2566
- if ("2.5.1") {
2567
- return "2.5.1";
2630
+ if ("2.5.3") {
2631
+ return "2.5.3";
2568
2632
  }
2569
2633
  } catch {
2570
2634
  }
@@ -3183,99 +3247,236 @@ var init_notifications = __esm({
3183
3247
  }
3184
3248
  });
3185
3249
 
3186
- // src/domains/paywall/paywallCloseReason.ts
3187
- function toCanonicalReason(reason) {
3188
- if (reason in CLOSE_REASON_MAP) return CLOSE_REASON_MAP[reason];
3189
- return reason;
3190
- }
3191
- var CLOSE_REASON_MAP;
3192
- var init_paywallCloseReason = __esm({
3193
- "src/domains/paywall/paywallCloseReason.ts"() {
3250
+ // src/domains/onboarding/OnboardingError.ts
3251
+ var OnboardingError, ONBOARDING_ERROR_CODES;
3252
+ var init_OnboardingError = __esm({
3253
+ "src/domains/onboarding/OnboardingError.ts"() {
3194
3254
  "use strict";
3195
- CLOSE_REASON_MAP = {
3196
- dismissed: "dismiss",
3197
- purchased: "purchase",
3198
- backgrounded: "dismiss",
3199
- timeout: "timeout"
3255
+ init_PaywalloError();
3256
+ OnboardingError = class extends PaywalloError {
3257
+ constructor(code, message) {
3258
+ super("onboarding", code, message);
3259
+ this.name = "OnboardingError";
3260
+ }
3261
+ };
3262
+ ONBOARDING_ERROR_CODES = {
3263
+ NOT_INITIALIZED: "ONBOARDING_NOT_INITIALIZED",
3264
+ INVALID_STEP_NAME: "ONBOARDING_INVALID_STEP_NAME"
3200
3265
  };
3201
3266
  }
3202
3267
  });
3203
3268
 
3204
- // src/domains/paywall/emitPaywallClosed.ts
3205
- async function emitPaywallClosed(opts) {
3206
- try {
3207
- const api = PaywalloClient.getApiClient();
3208
- if (!api) return;
3209
- const sessionId = PaywalloClient.getSessionId();
3210
- const closedAt = /* @__PURE__ */ new Date();
3211
- const durationS = Math.max(0, Math.round((closedAt.getTime() - opts.presentedAt) / 1e3));
3212
- await api.trackEvent("paywall", PaywalloClient.getDistinctId(), {
3213
- type: "closed",
3214
- paywall_id: opts.paywallId,
3215
- placement: opts.placement,
3216
- closed_at: closedAt.toISOString(),
3217
- duration_s: durationS,
3218
- close_reason: toCanonicalReason(opts.reason),
3219
- ...opts.variantId && { variant_id: opts.variantId },
3220
- ...opts.variantKey && { variant_key: opts.variantKey },
3221
- ...opts.campaignId && { campaign_id: opts.campaignId },
3222
- ...typeof opts.scrollDepth === "number" && { scroll_depth: opts.scrollDepth },
3223
- ...sessionId && { sessionId }
3224
- });
3225
- } catch (err) {
3226
- if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] emitPaywallClosed failed", err);
3227
- }
3228
- }
3229
- var init_emitPaywallClosed = __esm({
3230
- "src/domains/paywall/emitPaywallClosed.ts"() {
3231
- "use strict";
3232
- init_PaywalloClient();
3233
- init_paywallCloseReason();
3234
- }
3235
- });
3236
-
3237
- // src/domains/paywall/extractFirstPageBackground.ts
3238
- function extractFirstPageBackground(craftData) {
3239
- if (!craftData) return DEFAULT_BACKGROUND;
3240
- try {
3241
- const data = JSON.parse(craftData);
3242
- const nodes = { ...data.nodes, ...data.sharedNodes };
3243
- const rootId = data.rootId;
3244
- if (!rootId || !nodes[rootId]) return DEFAULT_BACKGROUND;
3245
- const firstPageId = nodes[rootId].children?.[0];
3246
- if (!firstPageId) return DEFAULT_BACKGROUND;
3247
- const bg = nodes[firstPageId]?.props?.backgroundColor;
3248
- return typeof bg === "string" && bg.length > 0 && bg !== "transparent" ? bg : DEFAULT_BACKGROUND;
3249
- } catch {
3250
- return DEFAULT_BACKGROUND;
3251
- }
3252
- }
3253
- function isColorDark(color) {
3254
- const hex = color.replace("#", "").trim();
3255
- const normalized = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex;
3256
- if (normalized.length !== 6) return false;
3257
- const r = parseInt(normalized.slice(0, 2), 16);
3258
- const g = parseInt(normalized.slice(2, 4), 16);
3259
- const b = parseInt(normalized.slice(4, 6), 16);
3260
- if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return false;
3261
- const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
3262
- return luminance < 0.5;
3263
- }
3264
- var DEFAULT_BACKGROUND;
3265
- var init_extractFirstPageBackground = __esm({
3266
- "src/domains/paywall/extractFirstPageBackground.ts"() {
3269
+ // src/domains/onboarding/OnboardingManager.ts
3270
+ var OnboardingManager, onboardingManager;
3271
+ var init_OnboardingManager = __esm({
3272
+ "src/domains/onboarding/OnboardingManager.ts"() {
3267
3273
  "use strict";
3268
- DEFAULT_BACKGROUND = "#FFFFFF";
3269
- }
3270
- });
3271
-
3272
- // src/domains/paywall/parseWebViewMessage.ts
3273
- function deriveMessageId(message) {
3274
- if (message.id) return message.id;
3275
- const payloadKey = message.payload?.productId ?? message.payload?.url ?? (message.payload?.timestamp != null ? String(message.payload.timestamp) : "");
3276
- return `${message.type}:${payloadKey}`;
3277
- }
3278
- function parseWebViewMessage(raw) {
3274
+ init_OnboardingError();
3275
+ OnboardingManager = class {
3276
+ constructor() {
3277
+ this.pipeline = null;
3278
+ this.debug = false;
3279
+ this.lastStep = null;
3280
+ this.finished = false;
3281
+ }
3282
+ /**
3283
+ * Injects runtime dependencies. Called by PaywalloClient during init.
3284
+ */
3285
+ injectDeps(config) {
3286
+ this.pipeline = config.pipeline;
3287
+ this.debug = config.debug ?? false;
3288
+ }
3289
+ /**
3290
+ * Tracks an onboarding step view.
3291
+ * Saves the step name internally for implicit drop detection on the backend.
3292
+ *
3293
+ * @param stepName - Unique name for this onboarding step.
3294
+ * @param order - Optional explicit display order (accepts decimals, e.g. 2.1 for A/B variants).
3295
+ */
3296
+ async step(stepName, order) {
3297
+ this.assertConfig();
3298
+ this.validateStepName(stepName, "step");
3299
+ this.lastStep = stepName;
3300
+ const stepPayload = { step_name: stepName };
3301
+ if (order !== void 0 && Number.isFinite(order) && order >= 0) {
3302
+ stepPayload.order = order;
3303
+ }
3304
+ const payload = {
3305
+ family: "onboarding",
3306
+ type: "step",
3307
+ ...stepPayload
3308
+ };
3309
+ return this.emit("onboarding", payload, "step");
3310
+ }
3311
+ /**
3312
+ * Tracks successful completion of the onboarding flow.
3313
+ * Marks the flow as finished.
3314
+ */
3315
+ async complete() {
3316
+ this.assertConfig();
3317
+ this.finished = true;
3318
+ return this.emit("onboarding", { family: "onboarding", type: "complete" }, "complete");
3319
+ }
3320
+ /**
3321
+ * Tracks an explicit drop (abandonment) at the given step.
3322
+ * Marks the flow as finished.
3323
+ *
3324
+ * @param stepName - The step name where the user dropped off.
3325
+ */
3326
+ async drop(stepName) {
3327
+ this.assertConfig();
3328
+ this.validateStepName(stepName, "drop");
3329
+ this.finished = true;
3330
+ const payload = {
3331
+ family: "onboarding",
3332
+ type: "drop",
3333
+ ...{ step_name: stepName }
3334
+ };
3335
+ return this.emit("onboarding", payload, "drop");
3336
+ }
3337
+ /** Returns the last step name seen, or null if no step was tracked yet. */
3338
+ getLastStep() {
3339
+ return this.lastStep;
3340
+ }
3341
+ /** Returns whether the flow has been marked as finished (complete or drop). */
3342
+ isFinished() {
3343
+ return this.finished;
3344
+ }
3345
+ async emit(eventName, properties, logLabel) {
3346
+ const pipeline = this.pipeline;
3347
+ const distinctId = pipeline.getDistinctId();
3348
+ if (!distinctId) {
3349
+ if (this.debug) console.log(`[Paywallo:Onboarding] skipped (no distinctId): ${logLabel}`);
3350
+ return;
3351
+ }
3352
+ await pipeline.trackEvent(eventName, distinctId, properties);
3353
+ if (this.debug) {
3354
+ console.log(`[Paywallo:Onboarding] event emitted: ${eventName}.${logLabel}`, properties);
3355
+ }
3356
+ }
3357
+ assertConfig() {
3358
+ if (!this.pipeline) {
3359
+ throw new OnboardingError(
3360
+ ONBOARDING_ERROR_CODES.NOT_INITIALIZED,
3361
+ "OnboardingManager not initialized. Call Paywallo.init() first."
3362
+ );
3363
+ }
3364
+ }
3365
+ validateStepName(stepName, method) {
3366
+ if (typeof stepName !== "string" || stepName.trim().length === 0) {
3367
+ throw new OnboardingError(
3368
+ ONBOARDING_ERROR_CODES.INVALID_STEP_NAME,
3369
+ `OnboardingManager.${method}(): stepName must be a non-empty string.`
3370
+ );
3371
+ }
3372
+ }
3373
+ };
3374
+ onboardingManager = new OnboardingManager();
3375
+ }
3376
+ });
3377
+
3378
+ // src/domains/onboarding/index.ts
3379
+ var init_onboarding = __esm({
3380
+ "src/domains/onboarding/index.ts"() {
3381
+ "use strict";
3382
+ init_OnboardingError();
3383
+ init_OnboardingManager();
3384
+ }
3385
+ });
3386
+
3387
+ // src/domains/paywall/paywallCloseReason.ts
3388
+ function toCanonicalReason(reason) {
3389
+ if (reason in CLOSE_REASON_MAP) return CLOSE_REASON_MAP[reason];
3390
+ return reason;
3391
+ }
3392
+ var CLOSE_REASON_MAP;
3393
+ var init_paywallCloseReason = __esm({
3394
+ "src/domains/paywall/paywallCloseReason.ts"() {
3395
+ "use strict";
3396
+ CLOSE_REASON_MAP = {
3397
+ dismissed: "dismiss",
3398
+ purchased: "purchase",
3399
+ backgrounded: "dismiss",
3400
+ timeout: "timeout"
3401
+ };
3402
+ }
3403
+ });
3404
+
3405
+ // src/domains/paywall/emitPaywallClosed.ts
3406
+ async function emitPaywallClosed(opts) {
3407
+ try {
3408
+ const api = PaywalloClient.getApiClient();
3409
+ if (!api) return;
3410
+ const sessionId = PaywalloClient.getSessionId();
3411
+ const closedAt = /* @__PURE__ */ new Date();
3412
+ const durationS = Math.max(0, Math.round((closedAt.getTime() - opts.presentedAt) / 1e3));
3413
+ await api.trackEvent("paywall", PaywalloClient.getDistinctId(), {
3414
+ type: "closed",
3415
+ paywall_id: opts.paywallId,
3416
+ placement: opts.placement,
3417
+ closed_at: closedAt.toISOString(),
3418
+ duration_s: durationS,
3419
+ close_reason: toCanonicalReason(opts.reason),
3420
+ ...opts.variantId && { variant_id: opts.variantId },
3421
+ ...opts.variantKey && { variant_key: opts.variantKey },
3422
+ ...opts.campaignId && { campaign_id: opts.campaignId },
3423
+ ...typeof opts.scrollDepth === "number" && { scroll_depth: opts.scrollDepth },
3424
+ ...sessionId && { sessionId }
3425
+ });
3426
+ } catch (err) {
3427
+ if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] emitPaywallClosed failed", err);
3428
+ }
3429
+ }
3430
+ var init_emitPaywallClosed = __esm({
3431
+ "src/domains/paywall/emitPaywallClosed.ts"() {
3432
+ "use strict";
3433
+ init_PaywalloClient();
3434
+ init_paywallCloseReason();
3435
+ }
3436
+ });
3437
+
3438
+ // src/domains/paywall/extractFirstPageBackground.ts
3439
+ function extractFirstPageBackground(craftData) {
3440
+ if (!craftData) return DEFAULT_BACKGROUND;
3441
+ try {
3442
+ const data = JSON.parse(craftData);
3443
+ const nodes = { ...data.nodes, ...data.sharedNodes };
3444
+ const rootId = data.rootId;
3445
+ if (!rootId || !nodes[rootId]) return DEFAULT_BACKGROUND;
3446
+ const firstPageId = nodes[rootId].children?.[0];
3447
+ if (!firstPageId) return DEFAULT_BACKGROUND;
3448
+ const bg = nodes[firstPageId]?.props?.backgroundColor;
3449
+ return typeof bg === "string" && bg.length > 0 && bg !== "transparent" ? bg : DEFAULT_BACKGROUND;
3450
+ } catch {
3451
+ return DEFAULT_BACKGROUND;
3452
+ }
3453
+ }
3454
+ function isColorDark(color) {
3455
+ const hex = color.replace("#", "").trim();
3456
+ const normalized = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex;
3457
+ if (normalized.length !== 6) return false;
3458
+ const r = parseInt(normalized.slice(0, 2), 16);
3459
+ const g = parseInt(normalized.slice(2, 4), 16);
3460
+ const b = parseInt(normalized.slice(4, 6), 16);
3461
+ if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return false;
3462
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
3463
+ return luminance < 0.5;
3464
+ }
3465
+ var DEFAULT_BACKGROUND;
3466
+ var init_extractFirstPageBackground = __esm({
3467
+ "src/domains/paywall/extractFirstPageBackground.ts"() {
3468
+ "use strict";
3469
+ DEFAULT_BACKGROUND = "#FFFFFF";
3470
+ }
3471
+ });
3472
+
3473
+ // src/domains/paywall/parseWebViewMessage.ts
3474
+ function deriveMessageId(message) {
3475
+ if (message.id) return message.id;
3476
+ const payloadKey = message.payload?.productId ?? message.payload?.url ?? (message.payload?.timestamp != null ? String(message.payload.timestamp) : "");
3477
+ return `${message.type}:${payloadKey}`;
3478
+ }
3479
+ function parseWebViewMessage(raw) {
3279
3480
  try {
3280
3481
  const parsed = JSON.parse(raw);
3281
3482
  if (typeof parsed?.type !== "string" || !ALLOWED_MESSAGE_TYPES.has(parsed.type)) {
@@ -3910,54 +4111,379 @@ var init_PaywallWebView = __esm({
3910
4111
  }
3911
4112
  });
3912
4113
 
3913
- // src/domains/iap/IAPTransactionEmitter.ts
3914
- var IAPTransactionEmitter;
3915
- var init_IAPTransactionEmitter = __esm({
3916
- "src/domains/iap/IAPTransactionEmitter.ts"() {
4114
+ // src/domains/iap/PurchaseError.ts
4115
+ function createPurchaseError(code, message) {
4116
+ return new PurchaseError(
4117
+ PURCHASE_ERROR_CODES[code],
4118
+ message ?? DEFAULT_MESSAGES[code] ?? "Unknown purchase error",
4119
+ code === "USER_CANCELLED"
4120
+ );
4121
+ }
4122
+ var PurchaseError, PURCHASE_ERROR_CODES, DEFAULT_MESSAGES;
4123
+ var init_PurchaseError = __esm({
4124
+ "src/domains/iap/PurchaseError.ts"() {
3917
4125
  "use strict";
3918
- init_PaywalloClient();
3919
- IAPTransactionEmitter = class {
3920
- constructor(getProductFromCache, getApiClientFn, getDebug) {
3921
- this.getProductFromCache = getProductFromCache;
3922
- this.getApiClientFn = getApiClientFn;
3923
- this.getDebug = getDebug;
4126
+ init_PaywalloError();
4127
+ PurchaseError = class extends PaywalloError {
4128
+ constructor(code, message, userCancelled = false, httpStatus) {
4129
+ super("purchase", code, message);
4130
+ this.name = "PurchaseError";
4131
+ this.userCancelled = userCancelled;
4132
+ this.httpStatus = httpStatus;
3924
4133
  }
3925
- /**
3926
- * Emits `checkout_started` quando o usuário inicia a compra (toca em comprar),
3927
- * antes do StoreKit/Billing abrir — conta mesmo que a sheet nativa seja cancelada.
3928
- * Vira InitiateCheckout no Meta/TikTok. Fire-and-forget — uma falha de tracking
3929
- * nunca pode travar a compra.
3930
- */
3931
- async emitCheckoutStarted(productId) {
3932
- try {
3933
- const apiClient = this.getApiClientFn();
3934
- if (!apiClient) return;
3935
- const distinctId = PaywalloClient.getDistinctId();
3936
- if (!distinctId) return;
3937
- const product = this.getProductFromCache(productId);
3938
- const properties = {
3939
- product_id: productId,
3940
- amount: product?.priceValue ?? 0,
3941
- currency: product?.currency ?? "USD"
3942
- };
3943
- await apiClient.trackEvent("checkout_started", distinctId, properties, void 0, {
3944
- priority: "critical"
3945
- });
3946
- if (this.getDebug()) console.log("[Paywallo PURCHASE] emitted checkout_started", productId);
4134
+ };
4135
+ PURCHASE_ERROR_CODES = {
4136
+ NOT_INITIALIZED: "PURCHASE_NOT_INITIALIZED",
4137
+ PRODUCT_NOT_FOUND: "PRODUCT_NOT_FOUND",
4138
+ PURCHASE_FAILED: "PURCHASE_FAILED",
4139
+ RESTORE_FAILED: "RESTORE_FAILED",
4140
+ VALIDATION_FAILED: "VALIDATION_FAILED",
4141
+ USER_CANCELLED: "USER_CANCELLED",
4142
+ NETWORK_ERROR: "PURCHASE_NETWORK_ERROR",
4143
+ STORE_ERROR: "PURCHASE_STORE_ERROR",
4144
+ PENDING_PURCHASE: "PURCHASE_PENDING",
4145
+ DEFERRED_PURCHASE: "PURCHASE_DEFERRED",
4146
+ STORE_NOT_AVAILABLE: "PURCHASE_STORE_NOT_AVAILABLE"
4147
+ };
4148
+ DEFAULT_MESSAGES = {
4149
+ NOT_INITIALIZED: "Purchase controller not initialized. Call init() first.",
4150
+ PRODUCT_NOT_FOUND: "Product not found in store.",
4151
+ PURCHASE_FAILED: "Purchase failed. Please try again.",
4152
+ RESTORE_FAILED: "Failed to restore purchases. Please try again.",
4153
+ VALIDATION_FAILED: "Failed to validate purchase with server.",
4154
+ USER_CANCELLED: "Purchase was cancelled.",
4155
+ NETWORK_ERROR: "Network error. Please check your connection.",
4156
+ STORE_ERROR: "Store error. Please try again later.",
4157
+ PENDING_PURCHASE: "Purchase is pending approval.",
4158
+ DEFERRED_PURCHASE: "Purchase is deferred (e.g., parental approval required).",
4159
+ STORE_NOT_AVAILABLE: "In-app purchases are not available on this device."
4160
+ };
4161
+ }
4162
+ });
4163
+
4164
+ // src/domains/iap/NativeStoreKit.ts
4165
+ var NativeStoreKit_exports = {};
4166
+ __export(NativeStoreKit_exports, {
4167
+ NativeStoreKit: () => NativeStoreKit,
4168
+ nativeStoreKit: () => nativeStoreKit
4169
+ });
4170
+ function isAvailable3() {
4171
+ return PaywalloStoreKitNative != null;
4172
+ }
4173
+ function mapNativeProduct(native) {
4174
+ const product = {
4175
+ productId: native.productId,
4176
+ title: native.title,
4177
+ description: native.description,
4178
+ price: native.price,
4179
+ priceValue: native.priceValue,
4180
+ currency: native.currency ?? "USD",
4181
+ localizedPrice: native.localizedPrice,
4182
+ type: mapProductType(native.type),
4183
+ subscriptionPeriod: native.subscriptionPeriod,
4184
+ introductoryPrice: native.introductoryPrice,
4185
+ introductoryPriceValue: native.introductoryPriceValue,
4186
+ freeTrialPeriod: native.freeTrialPeriod
4187
+ };
4188
+ if (native.subscriptionPeriod) {
4189
+ const prices = calculatePerPeriodPrices(native.priceValue, native.subscriptionPeriod);
4190
+ product.pricePerMonth = prices.perMonth;
4191
+ product.pricePerWeek = prices.perWeek;
4192
+ product.pricePerDay = prices.perDay;
4193
+ }
4194
+ return product;
4195
+ }
4196
+ function mapProductType(type) {
4197
+ switch (type) {
4198
+ case "subscription":
4199
+ case "autoRenewable":
4200
+ return "subscription";
4201
+ case "consumable":
4202
+ return "consumable";
4203
+ default:
4204
+ return "non_consumable";
4205
+ }
4206
+ }
4207
+ function calculatePerPeriodPrices(priceValue, period) {
4208
+ const totalDays = periodToDays(period);
4209
+ if (totalDays <= 0) return {};
4210
+ const perDay = priceValue / totalDays;
4211
+ const perWeek = perDay * 7;
4212
+ const perMonth = perDay * 30;
4213
+ return {
4214
+ perDay: perDay.toFixed(2),
4215
+ perWeek: perWeek.toFixed(2),
4216
+ perMonth: perMonth.toFixed(2)
4217
+ };
4218
+ }
4219
+ function periodToDays(period) {
4220
+ const match = period.match(/^P(\d+)([DWMY])$/);
4221
+ if (!match) return 0;
4222
+ const value = parseInt(match[1], 10);
4223
+ switch (match[2]) {
4224
+ case "D":
4225
+ return value;
4226
+ case "W":
4227
+ return value * 7;
4228
+ case "M":
4229
+ return value * 30;
4230
+ case "Y":
4231
+ return value * 365;
4232
+ default:
4233
+ return 0;
4234
+ }
4235
+ }
4236
+ function mapNativePurchase(native) {
4237
+ return {
4238
+ productId: native.productId,
4239
+ transactionId: native.transactionId,
4240
+ transactionDate: native.transactionDate,
4241
+ receipt: native.receipt,
4242
+ platform: import_react_native16.Platform.OS
4243
+ };
4244
+ }
4245
+ function normalizeTransactionUpdate(raw) {
4246
+ if (!raw || typeof raw !== "object") return null;
4247
+ const r = raw;
4248
+ const type = r.type;
4249
+ if (type !== "renewed" && type !== "refunded" && type !== "canceled") return null;
4250
+ const transactionId = typeof r.transactionId === "string" ? r.transactionId : null;
4251
+ const productId = typeof r.productId === "string" ? r.productId : null;
4252
+ if (!transactionId) return null;
4253
+ const update = {
4254
+ type,
4255
+ transactionId,
4256
+ productId: productId ?? ""
4257
+ };
4258
+ if (typeof r.amount === "number") update.amount = r.amount;
4259
+ if (typeof r.currency === "string") update.currency = r.currency;
4260
+ return update;
4261
+ }
4262
+ var import_react_native16, TRANSACTION_UPDATE_EVENT, PaywalloStoreKitNative, nativeStoreKit, NativeStoreKit;
4263
+ var init_NativeStoreKit = __esm({
4264
+ "src/domains/iap/NativeStoreKit.ts"() {
4265
+ "use strict";
4266
+ import_react_native16 = require("react-native");
4267
+ init_PaywalloClient();
4268
+ init_uuid();
4269
+ init_IdentityManager();
4270
+ init_PurchaseError();
4271
+ TRANSACTION_UPDATE_EVENT = "PaywalloTransactionUpdate";
4272
+ PaywalloStoreKitNative = import_react_native16.NativeModules.PaywalloStoreKit ?? null;
4273
+ nativeStoreKit = {
4274
+ isAvailable: isAvailable3,
4275
+ async getProducts(productIds) {
4276
+ if (!PaywalloStoreKitNative) {
4277
+ return [];
4278
+ }
4279
+ const nativeProducts = await PaywalloStoreKitNative.getProducts(productIds);
4280
+ return nativeProducts.map(mapNativeProduct);
4281
+ },
4282
+ async purchase(productId) {
4283
+ if (!PaywalloStoreKitNative) {
4284
+ throw new PurchaseError(
4285
+ PURCHASE_ERROR_CODES.STORE_NOT_AVAILABLE,
4286
+ "StoreKit native module not available"
4287
+ );
4288
+ }
4289
+ const distinctId = identityManager.getDistinctId();
4290
+ const rawUuid = distinctId?.startsWith("$paywallo_anon:") ? distinctId.slice("$paywallo_anon:".length) : distinctId;
4291
+ const appAccountToken = isValidUUIDv4(rawUuid) ? rawUuid : null;
4292
+ if (distinctId && appAccountToken === null) {
4293
+ if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] distinctId is not a valid UUID v4 \u2014 passing null to appAccountToken:", distinctId);
4294
+ }
4295
+ try {
4296
+ const result = await PaywalloStoreKitNative.purchase(productId, appAccountToken);
4297
+ if (!result || result.pending) {
4298
+ return null;
4299
+ }
4300
+ return mapNativePurchase(result);
4301
+ } catch (err) {
4302
+ const message = err instanceof Error ? err.message : String(err);
4303
+ if (message.toLowerCase().includes("cancel")) {
4304
+ throw new PurchaseError(PURCHASE_ERROR_CODES.USER_CANCELLED, message, true);
4305
+ }
4306
+ throw new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, message);
4307
+ }
4308
+ },
4309
+ async finishTransaction(transactionId) {
4310
+ if (!PaywalloStoreKitNative) return;
4311
+ try {
4312
+ await PaywalloStoreKitNative.finishTransaction(transactionId);
4313
+ } catch (err) {
4314
+ console.error("[Paywallo] finishTransaction native error", err);
4315
+ }
4316
+ },
4317
+ async getActiveTransactions() {
4318
+ if (!PaywalloStoreKitNative) return [];
4319
+ try {
4320
+ const results = await PaywalloStoreKitNative.getActiveTransactions();
4321
+ return results.map(mapNativePurchase);
4322
+ } catch (err) {
4323
+ console.error("[Paywallo] getActiveTransactions failed", err);
4324
+ return [];
4325
+ }
4326
+ },
4327
+ /**
4328
+ * Subscribes to native transaction update events (renewals, refunds,
4329
+ * cancellations) from StoreKit 2 (`Transaction.updates`) on iOS or
4330
+ * `PurchasesUpdatedListener` + refund diff on Android.
4331
+ *
4332
+ * Returns a subscription with a `remove()` method. The caller is
4333
+ * responsible for cleanup (typically on `Paywallo.reset()`).
4334
+ *
4335
+ * Returns `null` when the native module isn't available (non-mobile env,
4336
+ * native module not linked) — callers should treat null as a no-op.
4337
+ */
4338
+ subscribeToTransactionUpdates(listener) {
4339
+ if (!PaywalloStoreKitNative) return null;
4340
+ try {
4341
+ const EmitterCtor = import_react_native16.NativeEventEmitter;
4342
+ const emitter = new EmitterCtor(PaywalloStoreKitNative);
4343
+ const sub = emitter.addListener(TRANSACTION_UPDATE_EVENT, (payload) => {
4344
+ const update = normalizeTransactionUpdate(payload);
4345
+ if (update) listener(update);
4346
+ });
4347
+ return { remove: () => sub.remove() };
4348
+ } catch (err) {
4349
+ if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] subscribeToTransactionUpdates failed", err);
4350
+ return null;
4351
+ }
4352
+ }
4353
+ };
4354
+ NativeStoreKit = nativeStoreKit;
4355
+ }
4356
+ });
4357
+
4358
+ // src/domains/iap/IAPTransactionEmitter.ts
4359
+ var IAPTransactionEmitter;
4360
+ var init_IAPTransactionEmitter = __esm({
4361
+ "src/domains/iap/IAPTransactionEmitter.ts"() {
4362
+ "use strict";
4363
+ init_PaywalloClient();
4364
+ init_NativeStoreKit();
4365
+ IAPTransactionEmitter = class {
4366
+ constructor(getProductFromCache, getApiClientFn, getDebug) {
4367
+ this.getProductFromCache = getProductFromCache;
4368
+ this.getApiClientFn = getApiClientFn;
4369
+ this.getDebug = getDebug;
4370
+ this.transactionUpdateSubscription = null;
4371
+ }
4372
+ /**
4373
+ * Emits `transaction {type: "completed"}` after a server-validated purchase.
4374
+ * Fire-and-forget — a tracking failure must never poison the purchase flow.
4375
+ */
4376
+ async emitTransactionCompleted(purchase, productId, options) {
4377
+ try {
4378
+ const apiClient = this.getApiClientFn();
4379
+ if (!apiClient) return;
4380
+ const distinctId = PaywalloClient.getDistinctId();
4381
+ if (!distinctId) return;
4382
+ const product = this.getProductFromCache(productId);
4383
+ const properties = {
4384
+ type: "completed",
4385
+ tx_id: purchase.transactionId,
4386
+ product_id: purchase.productId
4387
+ };
4388
+ if (product?.priceValue !== void 0 && product.priceValue > 0) {
4389
+ properties.amount = product.priceValue;
4390
+ }
4391
+ if (product?.currency) {
4392
+ properties.currency = product.currency;
4393
+ }
4394
+ if (options?.paywallId) properties.paywall_id = options.paywallId;
4395
+ if (options?.variantId) properties.variant_id = options.variantId;
4396
+ await apiClient.trackEvent("transaction", distinctId, properties, void 0, { priority: "critical" });
4397
+ } catch (err) {
4398
+ if (this.getDebug()) console.log("[Paywallo PURCHASE] transaction event emission failed", err);
4399
+ }
4400
+ }
4401
+ /**
4402
+ * Emits `checkout_started` quando o usuário inicia a compra (toca em comprar),
4403
+ * antes do StoreKit/Billing abrir — conta mesmo que a sheet nativa seja cancelada.
4404
+ * Vira InitiateCheckout no Meta/TikTok. Fire-and-forget — uma falha de tracking
4405
+ * nunca pode travar a compra.
4406
+ */
4407
+ async emitCheckoutStarted(productId) {
4408
+ try {
4409
+ const apiClient = this.getApiClientFn();
4410
+ if (!apiClient) return;
4411
+ const distinctId = PaywalloClient.getDistinctId();
4412
+ if (!distinctId) return;
4413
+ const product = this.getProductFromCache(productId);
4414
+ const properties = {
4415
+ product_id: productId,
4416
+ amount: product?.priceValue ?? 0,
4417
+ currency: product?.currency ?? "USD"
4418
+ };
4419
+ await apiClient.trackEvent("checkout_started", distinctId, properties, void 0, {
4420
+ priority: "critical"
4421
+ });
4422
+ if (this.getDebug()) console.log("[Paywallo PURCHASE] emitted checkout_started", productId);
3947
4423
  } catch (err) {
3948
4424
  if (this.getDebug()) console.log("[Paywallo PURCHASE] checkout_started emission failed", err);
3949
4425
  }
3950
4426
  }
3951
4427
  /**
3952
- * No-op. Native transaction update listener removed — sale events are sourced
3953
- * exclusively from server-side webhooks (Apple/Google/Superwall/RevenueCat).
4428
+ * Forwards a native transaction update to the EventBatcher.
4429
+ * Public so tests can exercise the wiring without a fake NativeEventEmitter.
4430
+ */
4431
+ async emitTransactionUpdate(update) {
4432
+ try {
4433
+ const apiClient = this.getApiClientFn() ?? PaywalloClient.getApiClient();
4434
+ if (!apiClient) {
4435
+ if (this.getDebug()) console.log("[Paywallo IAP] transaction update dropped (no apiClient)", update.type);
4436
+ return;
4437
+ }
4438
+ const distinctId = PaywalloClient.getDistinctId();
4439
+ if (!distinctId) {
4440
+ if (this.getDebug()) console.log("[Paywallo IAP] transaction update dropped (no distinctId)", update.type);
4441
+ return;
4442
+ }
4443
+ const properties = {
4444
+ type: update.type,
4445
+ tx_id: update.transactionId,
4446
+ product_id: update.productId
4447
+ };
4448
+ if (update.type !== "canceled") {
4449
+ const cached2 = this.getProductFromCache(update.productId);
4450
+ const amount = update.amount ?? cached2?.priceValue;
4451
+ const currency = update.currency ?? cached2?.currency;
4452
+ if (typeof amount === "number" && amount > 0) properties.amount = amount;
4453
+ if (currency) properties.currency = currency;
4454
+ }
4455
+ await apiClient.trackEvent("transaction", distinctId, properties, void 0, { priority: "critical" });
4456
+ if (this.getDebug()) console.log(`[Paywallo IAP] emitted transaction {${update.type}}`, update.transactionId);
4457
+ } catch (err) {
4458
+ if (this.getDebug()) console.log("[Paywallo IAP] transaction update emission failed", err);
4459
+ }
4460
+ }
4461
+ /**
4462
+ * Starts listening to native Transaction.updates. Idempotent.
3954
4463
  */
3955
4464
  startListener() {
4465
+ if (this.transactionUpdateSubscription) return;
4466
+ this.transactionUpdateSubscription = nativeStoreKit.subscribeToTransactionUpdates(
4467
+ (update) => {
4468
+ void this.emitTransactionUpdate(update);
4469
+ }
4470
+ );
4471
+ if (this.getDebug() && this.transactionUpdateSubscription) {
4472
+ console.log("[Paywallo IAP] transaction update listener started");
4473
+ }
3956
4474
  }
3957
4475
  /**
3958
- * No-op. See startListener.
4476
+ * Removes the native listener. Safe to call multiple times.
3959
4477
  */
3960
4478
  stopListener() {
4479
+ if (!this.transactionUpdateSubscription) return;
4480
+ try {
4481
+ this.transactionUpdateSubscription.remove();
4482
+ } catch (err) {
4483
+ if (this.getDebug()) console.log("[Paywallo IAP] transaction listener removal failed", err);
4484
+ }
4485
+ this.transactionUpdateSubscription = null;
4486
+ if (this.getDebug()) console.log("[Paywallo IAP] transaction update listener stopped");
3961
4487
  }
3962
4488
  };
3963
4489
  }
@@ -4445,11 +4971,11 @@ var init_types2 = __esm({
4445
4971
  });
4446
4972
 
4447
4973
  // src/core/network/NetworkMonitor.ts
4448
- var import_react_native16, NetworkMonitorClass, networkMonitor;
4974
+ var import_react_native17, NetworkMonitorClass, networkMonitor;
4449
4975
  var init_NetworkMonitor = __esm({
4450
4976
  "src/core/network/NetworkMonitor.ts"() {
4451
4977
  "use strict";
4452
- import_react_native16 = require("react-native");
4978
+ import_react_native17 = require("react-native");
4453
4979
  init_types2();
4454
4980
  NetworkMonitorClass = class {
4455
4981
  constructor() {
@@ -4478,7 +5004,7 @@ var init_NetworkMonitor = __esm({
4478
5004
  }, this.config.checkInterval);
4479
5005
  }
4480
5006
  setupAppStateListener() {
4481
- this.appStateSubscription = import_react_native16.AppState.addEventListener("change", (state) => {
5007
+ this.appStateSubscription = import_react_native17.AppState.addEventListener("change", (state) => {
4482
5008
  if (state === "active") {
4483
5009
  if (!this.checkIntervalId) {
4484
5010
  this.checkIntervalId = setInterval(() => {
@@ -4846,62 +5372,12 @@ var init_queue = __esm({
4846
5372
  }
4847
5373
  });
4848
5374
 
4849
- // src/domains/iap/PurchaseError.ts
4850
- function createPurchaseError(code, message) {
4851
- return new PurchaseError(
4852
- PURCHASE_ERROR_CODES[code],
4853
- message ?? DEFAULT_MESSAGES[code] ?? "Unknown purchase error",
4854
- code === "USER_CANCELLED"
4855
- );
4856
- }
4857
- var PurchaseError, PURCHASE_ERROR_CODES, DEFAULT_MESSAGES;
4858
- var init_PurchaseError = __esm({
4859
- "src/domains/iap/PurchaseError.ts"() {
4860
- "use strict";
4861
- init_PaywalloError();
4862
- PurchaseError = class extends PaywalloError {
4863
- constructor(code, message, userCancelled = false, httpStatus) {
4864
- super("purchase", code, message);
4865
- this.name = "PurchaseError";
4866
- this.userCancelled = userCancelled;
4867
- this.httpStatus = httpStatus;
4868
- }
4869
- };
4870
- PURCHASE_ERROR_CODES = {
4871
- NOT_INITIALIZED: "PURCHASE_NOT_INITIALIZED",
4872
- PRODUCT_NOT_FOUND: "PRODUCT_NOT_FOUND",
4873
- PURCHASE_FAILED: "PURCHASE_FAILED",
4874
- RESTORE_FAILED: "RESTORE_FAILED",
4875
- VALIDATION_FAILED: "VALIDATION_FAILED",
4876
- USER_CANCELLED: "USER_CANCELLED",
4877
- NETWORK_ERROR: "PURCHASE_NETWORK_ERROR",
4878
- STORE_ERROR: "PURCHASE_STORE_ERROR",
4879
- PENDING_PURCHASE: "PURCHASE_PENDING",
4880
- DEFERRED_PURCHASE: "PURCHASE_DEFERRED",
4881
- STORE_NOT_AVAILABLE: "PURCHASE_STORE_NOT_AVAILABLE"
4882
- };
4883
- DEFAULT_MESSAGES = {
4884
- NOT_INITIALIZED: "Purchase controller not initialized. Call init() first.",
4885
- PRODUCT_NOT_FOUND: "Product not found in store.",
4886
- PURCHASE_FAILED: "Purchase failed. Please try again.",
4887
- RESTORE_FAILED: "Failed to restore purchases. Please try again.",
4888
- VALIDATION_FAILED: "Failed to validate purchase with server.",
4889
- USER_CANCELLED: "Purchase was cancelled.",
4890
- NETWORK_ERROR: "Network error. Please check your connection.",
4891
- STORE_ERROR: "Store error. Please try again later.",
4892
- PENDING_PURCHASE: "Purchase is pending approval.",
4893
- DEFERRED_PURCHASE: "Purchase is deferred (e.g., parental approval required).",
4894
- STORE_NOT_AVAILABLE: "In-app purchases are not available on this device."
4895
- };
4896
- }
4897
- });
4898
-
4899
5375
  // src/domains/iap/IAPValidator.ts
4900
- var import_react_native17, IAPValidator;
5376
+ var import_react_native18, IAPValidator;
4901
5377
  var init_IAPValidator = __esm({
4902
5378
  "src/domains/iap/IAPValidator.ts"() {
4903
5379
  "use strict";
4904
- import_react_native17 = require("react-native");
5380
+ import_react_native18 = require("react-native");
4905
5381
  init_PaywalloClient();
4906
5382
  init_queue();
4907
5383
  init_PurchaseError();
@@ -4927,7 +5403,7 @@ var init_IAPValidator = __esm({
4927
5403
  async validateWithServer(purchase, productId, options) {
4928
5404
  const apiClient = this.getApiClientFn();
4929
5405
  const product = this.getProductFromCache(productId);
4930
- const platform = import_react_native17.Platform.OS;
5406
+ const platform = import_react_native18.Platform.OS;
4931
5407
  const distinctId = PaywalloClient.getDistinctId();
4932
5408
  return this.validateWithRetry(
4933
5409
  () => apiClient.validatePurchase(
@@ -4948,243 +5424,49 @@ var init_IAPValidator = __esm({
4948
5424
  * Bucketizes a validation failure into `server4xx` (permanent) vs `network` (transient).
4949
5425
  */
4950
5426
  classifyValidationError(error) {
4951
- if (error instanceof PurchaseError && typeof error.httpStatus === "number") {
4952
- const status = error.httpStatus;
4953
- if (status >= 400 && status < 500) return "server4xx";
4954
- }
4955
- return "network";
4956
- }
4957
- /**
4958
- * Pushes the validation payload into the offline queue for durable retry.
4959
- * The StoreKit transaction is intentionally left un-finished.
4960
- */
4961
- async enqueueValidationRetry(purchase, productId, options) {
4962
- try {
4963
- const apiClient = this.getApiClientFn();
4964
- const product = this.getProductFromCache(productId);
4965
- const platform = import_react_native17.Platform.OS;
4966
- const distinctId = PaywalloClient.getDistinctId();
4967
- const appKey = apiClient.getAppKey();
4968
- const body = {
4969
- platform,
4970
- receipt_data: purchase.receipt,
4971
- product_id: purchase.productId,
4972
- transaction_id: purchase.transactionId,
4973
- price_local: product?.priceValue ?? 0,
4974
- currency: product?.currency ?? "USD",
4975
- country: this.getDeviceCountry(),
4976
- distinct_id: distinctId
4977
- };
4978
- if (options?.paywallPlacement !== void 0) body.paywall_placement = options.paywallPlacement;
4979
- if (options?.variantKey !== void 0) body.variant_key = options.variantKey;
4980
- await offlineQueue.enqueue(
4981
- "POST",
4982
- "/sdk/purchases/validate",
4983
- body,
4984
- { "X-App-Key": appKey },
4985
- `validate:${purchase.transactionId}`,
4986
- "critical"
4987
- );
4988
- if (this.getDebug()) console.log("[Paywallo PURCHASE] validation retry enqueued", purchase.transactionId);
4989
- } catch (err) {
4990
- console.error("[Paywallo PURCHASE] failed to enqueue validation retry", err);
4991
- }
4992
- }
4993
- };
4994
- }
4995
- });
4996
-
4997
- // src/domains/iap/NativeStoreKit.ts
4998
- var NativeStoreKit_exports = {};
4999
- __export(NativeStoreKit_exports, {
5000
- NativeStoreKit: () => NativeStoreKit,
5001
- nativeStoreKit: () => nativeStoreKit
5002
- });
5003
- function isAvailable3() {
5004
- return PaywalloStoreKitNative != null;
5005
- }
5006
- function mapNativeProduct(native) {
5007
- const product = {
5008
- productId: native.productId,
5009
- title: native.title,
5010
- description: native.description,
5011
- price: native.price,
5012
- priceValue: native.priceValue,
5013
- currency: native.currency ?? "USD",
5014
- localizedPrice: native.localizedPrice,
5015
- type: mapProductType(native.type),
5016
- subscriptionPeriod: native.subscriptionPeriod,
5017
- introductoryPrice: native.introductoryPrice,
5018
- introductoryPriceValue: native.introductoryPriceValue,
5019
- freeTrialPeriod: native.freeTrialPeriod
5020
- };
5021
- if (native.subscriptionPeriod) {
5022
- const prices = calculatePerPeriodPrices(native.priceValue, native.subscriptionPeriod);
5023
- product.pricePerMonth = prices.perMonth;
5024
- product.pricePerWeek = prices.perWeek;
5025
- product.pricePerDay = prices.perDay;
5026
- }
5027
- return product;
5028
- }
5029
- function mapProductType(type) {
5030
- switch (type) {
5031
- case "subscription":
5032
- case "autoRenewable":
5033
- return "subscription";
5034
- case "consumable":
5035
- return "consumable";
5036
- default:
5037
- return "non_consumable";
5038
- }
5039
- }
5040
- function calculatePerPeriodPrices(priceValue, period) {
5041
- const totalDays = periodToDays(period);
5042
- if (totalDays <= 0) return {};
5043
- const perDay = priceValue / totalDays;
5044
- const perWeek = perDay * 7;
5045
- const perMonth = perDay * 30;
5046
- return {
5047
- perDay: perDay.toFixed(2),
5048
- perWeek: perWeek.toFixed(2),
5049
- perMonth: perMonth.toFixed(2)
5050
- };
5051
- }
5052
- function periodToDays(period) {
5053
- const match = period.match(/^P(\d+)([DWMY])$/);
5054
- if (!match) return 0;
5055
- const value = parseInt(match[1], 10);
5056
- switch (match[2]) {
5057
- case "D":
5058
- return value;
5059
- case "W":
5060
- return value * 7;
5061
- case "M":
5062
- return value * 30;
5063
- case "Y":
5064
- return value * 365;
5065
- default:
5066
- return 0;
5067
- }
5068
- }
5069
- function mapNativePurchase(native) {
5070
- return {
5071
- productId: native.productId,
5072
- transactionId: native.transactionId,
5073
- transactionDate: native.transactionDate,
5074
- receipt: native.receipt,
5075
- platform: import_react_native18.Platform.OS
5076
- };
5077
- }
5078
- function normalizeTransactionUpdate(raw) {
5079
- if (!raw || typeof raw !== "object") return null;
5080
- const r = raw;
5081
- const type = r.type;
5082
- if (type !== "renewed" && type !== "refunded" && type !== "canceled") return null;
5083
- const transactionId = typeof r.transactionId === "string" ? r.transactionId : null;
5084
- const productId = typeof r.productId === "string" ? r.productId : null;
5085
- if (!transactionId) return null;
5086
- const update = {
5087
- type,
5088
- transactionId,
5089
- productId: productId ?? ""
5090
- };
5091
- if (typeof r.amount === "number") update.amount = r.amount;
5092
- if (typeof r.currency === "string") update.currency = r.currency;
5093
- return update;
5094
- }
5095
- var import_react_native18, TRANSACTION_UPDATE_EVENT, PaywalloStoreKitNative, nativeStoreKit, NativeStoreKit;
5096
- var init_NativeStoreKit = __esm({
5097
- "src/domains/iap/NativeStoreKit.ts"() {
5098
- "use strict";
5099
- import_react_native18 = require("react-native");
5100
- init_PaywalloClient();
5101
- init_uuid();
5102
- init_IdentityManager();
5103
- init_PurchaseError();
5104
- TRANSACTION_UPDATE_EVENT = "PaywalloTransactionUpdate";
5105
- PaywalloStoreKitNative = import_react_native18.NativeModules.PaywalloStoreKit ?? null;
5106
- nativeStoreKit = {
5107
- isAvailable: isAvailable3,
5108
- async getProducts(productIds) {
5109
- if (!PaywalloStoreKitNative) {
5110
- return [];
5111
- }
5112
- const nativeProducts = await PaywalloStoreKitNative.getProducts(productIds);
5113
- return nativeProducts.map(mapNativeProduct);
5114
- },
5115
- async purchase(productId) {
5116
- if (!PaywalloStoreKitNative) {
5117
- throw new PurchaseError(
5118
- PURCHASE_ERROR_CODES.STORE_NOT_AVAILABLE,
5119
- "StoreKit native module not available"
5120
- );
5121
- }
5122
- const distinctId = identityManager.getDistinctId();
5123
- const rawUuid = distinctId?.startsWith("$paywallo_anon:") ? distinctId.slice("$paywallo_anon:".length) : distinctId;
5124
- const appAccountToken = isValidUUIDv4(rawUuid) ? rawUuid : null;
5125
- if (distinctId && appAccountToken === null) {
5126
- if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] distinctId is not a valid UUID v4 \u2014 passing null to appAccountToken:", distinctId);
5127
- }
5128
- try {
5129
- const result = await PaywalloStoreKitNative.purchase(productId, appAccountToken);
5130
- if (!result || result.pending) {
5131
- return null;
5132
- }
5133
- return mapNativePurchase(result);
5134
- } catch (err) {
5135
- const message = err instanceof Error ? err.message : String(err);
5136
- if (message.toLowerCase().includes("cancel")) {
5137
- throw new PurchaseError(PURCHASE_ERROR_CODES.USER_CANCELLED, message, true);
5138
- }
5139
- throw new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, message);
5140
- }
5141
- },
5142
- async finishTransaction(transactionId) {
5143
- if (!PaywalloStoreKitNative) return;
5144
- try {
5145
- await PaywalloStoreKitNative.finishTransaction(transactionId);
5146
- } catch (err) {
5147
- console.error("[Paywallo] finishTransaction native error", err);
5148
- }
5149
- },
5150
- async getActiveTransactions() {
5151
- if (!PaywalloStoreKitNative) return [];
5152
- try {
5153
- const results = await PaywalloStoreKitNative.getActiveTransactions();
5154
- return results.map(mapNativePurchase);
5155
- } catch (err) {
5156
- console.error("[Paywallo] getActiveTransactions failed", err);
5157
- return [];
5427
+ if (error instanceof PurchaseError && typeof error.httpStatus === "number") {
5428
+ const status = error.httpStatus;
5429
+ if (status >= 400 && status < 500) return "server4xx";
5158
5430
  }
5159
- },
5431
+ return "network";
5432
+ }
5160
5433
  /**
5161
- * Subscribes to native transaction update events (renewals, refunds,
5162
- * cancellations) from StoreKit 2 (`Transaction.updates`) on iOS or
5163
- * `PurchasesUpdatedListener` + refund diff on Android.
5164
- *
5165
- * Returns a subscription with a `remove()` method. The caller is
5166
- * responsible for cleanup (typically on `Paywallo.reset()`).
5167
- *
5168
- * Returns `null` when the native module isn't available (non-mobile env,
5169
- * native module not linked) — callers should treat null as a no-op.
5434
+ * Pushes the validation payload into the offline queue for durable retry.
5435
+ * The StoreKit transaction is intentionally left un-finished.
5170
5436
  */
5171
- subscribeToTransactionUpdates(listener) {
5172
- if (!PaywalloStoreKitNative) return null;
5437
+ async enqueueValidationRetry(purchase, productId, options) {
5173
5438
  try {
5174
- const EmitterCtor = import_react_native18.NativeEventEmitter;
5175
- const emitter = new EmitterCtor(PaywalloStoreKitNative);
5176
- const sub = emitter.addListener(TRANSACTION_UPDATE_EVENT, (payload) => {
5177
- const update = normalizeTransactionUpdate(payload);
5178
- if (update) listener(update);
5179
- });
5180
- return { remove: () => sub.remove() };
5439
+ const apiClient = this.getApiClientFn();
5440
+ const product = this.getProductFromCache(productId);
5441
+ const platform = import_react_native18.Platform.OS;
5442
+ const distinctId = PaywalloClient.getDistinctId();
5443
+ const appKey = apiClient.getAppKey();
5444
+ const body = {
5445
+ platform,
5446
+ receipt_data: purchase.receipt,
5447
+ product_id: purchase.productId,
5448
+ transaction_id: purchase.transactionId,
5449
+ price_local: product?.priceValue ?? 0,
5450
+ currency: product?.currency ?? "USD",
5451
+ country: this.getDeviceCountry(),
5452
+ distinct_id: distinctId
5453
+ };
5454
+ if (options?.paywallPlacement !== void 0) body.paywall_placement = options.paywallPlacement;
5455
+ if (options?.variantKey !== void 0) body.variant_key = options.variantKey;
5456
+ await offlineQueue.enqueue(
5457
+ "POST",
5458
+ "/sdk/purchases/validate",
5459
+ body,
5460
+ { "X-App-Key": appKey },
5461
+ `validate:${purchase.transactionId}`,
5462
+ "critical"
5463
+ );
5464
+ if (this.getDebug()) console.log("[Paywallo PURCHASE] validation retry enqueued", purchase.transactionId);
5181
5465
  } catch (err) {
5182
- if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] subscribeToTransactionUpdates failed", err);
5183
- return null;
5466
+ console.error("[Paywallo PURCHASE] failed to enqueue validation retry", err);
5184
5467
  }
5185
5468
  }
5186
5469
  };
5187
- NativeStoreKit = nativeStoreKit;
5188
5470
  }
5189
5471
  });
5190
5472
 
@@ -5288,8 +5570,8 @@ var init_IAPService = __esm({
5288
5570
  stopTransactionListener() {
5289
5571
  this.emitter.stopListener();
5290
5572
  }
5291
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
5292
- async emitTransactionUpdate(_update) {
5573
+ async emitTransactionUpdate(update) {
5574
+ return this.emitter.emitTransactionUpdate(update);
5293
5575
  }
5294
5576
  async purchase(productId, options) {
5295
5577
  if (!nativeStoreKit.isAvailable()) {
@@ -5318,6 +5600,10 @@ var init_IAPService = __esm({
5318
5600
  if (this.debug) console.log(`[Paywallo PURCHASE] ${productId} server validated`);
5319
5601
  await this.finishTransaction(purchase.transactionId);
5320
5602
  if (this.debug) console.log(`[Paywallo PURCHASE] ${productId} finished`);
5603
+ await this.emitter.emitTransactionCompleted(purchase, productId, {
5604
+ paywallId: options?.paywallId,
5605
+ variantId: options?.variantId
5606
+ });
5321
5607
  }).catch(async (err) => {
5322
5608
  const kind = this.validator.classifyValidationError(err);
5323
5609
  if (kind === "server4xx") {
@@ -6299,254 +6585,117 @@ var init_PlanService = __esm({
6299
6585
  (p) => p.identifier === cached2.data.currentIdentifier
6300
6586
  );
6301
6587
  return current ?? null;
6302
- }
6303
- }
6304
- if (!this.cache.get()) {
6305
- try {
6306
- const stored = await secureStorage.get(ALL_PLANS_CACHE_KEY);
6307
- if (stored) {
6308
- const parsed = JSON.parse(stored);
6309
- this.cache.set(parsed);
6310
- }
6311
- } catch {
6312
- }
6313
- }
6314
- const memCached = this.cache.get();
6315
- if (memCached) {
6316
- void this.fetchAllPlans();
6317
- const current = memCached.data.plans.find(
6318
- (p) => p.identifier === memCached.data.currentIdentifier
6319
- );
6320
- return current ?? null;
6321
- }
6322
- return this.fetchCurrentPlan();
6323
- }
6324
- async getAllPlans(forceRefresh = false) {
6325
- if (!forceRefresh) {
6326
- const cached2 = this.cache.get();
6327
- if (cached2 && !cached2.isStale) return cached2.data;
6328
- }
6329
- if (!this.cache.get()) {
6330
- try {
6331
- const stored = await secureStorage.get(ALL_PLANS_CACHE_KEY);
6332
- if (stored) {
6333
- const parsed = JSON.parse(stored);
6334
- this.cache.set(parsed);
6335
- }
6336
- } catch {
6337
- }
6338
- }
6339
- const memCached = this.cache.get();
6340
- if (memCached) {
6341
- void this.fetchAllPlans();
6342
- return memCached.data;
6343
- }
6344
- return this.fetchAllPlans();
6345
- }
6346
- invalidateCache() {
6347
- this.cache.invalidate();
6348
- secureStorage.remove(PLAN_CACHE_KEY).catch(() => void 0);
6349
- secureStorage.remove(ALL_PLANS_CACHE_KEY).catch(() => void 0);
6350
- }
6351
- getApiClient() {
6352
- if (!this.config) {
6353
- throw new PlanError(PLAN_ERROR_CODES.NOT_INITIALIZED, "PlanService not initialized. Call Paywallo.init() first");
6354
- }
6355
- return this.config.apiClient;
6356
- }
6357
- async fetchCurrentPlan() {
6358
- try {
6359
- const res = await this.getApiClient().get("/sdk/plans");
6360
- if (!res.ok) {
6361
- this.log("fetchCurrentPlan failed", res.status);
6362
- return null;
6363
- }
6364
- const raw = res.data.data.plan;
6365
- return raw ? mapPlan(raw) : null;
6366
- } catch (err) {
6367
- this.log("fetchCurrentPlan error", err);
6368
- return null;
6369
- }
6370
- }
6371
- async fetchAllPlans() {
6372
- try {
6373
- const res = await this.getApiClient().get("/sdk/plans/all");
6374
- if (!res.ok) {
6375
- this.log("fetchAllPlans failed", res.status);
6376
- return { plans: [], currentIdentifier: null };
6377
- }
6378
- const result = {
6379
- plans: res.data.data.plans.map(mapPlan),
6380
- currentIdentifier: res.data.data.current_identifier
6381
- };
6382
- this.cache.set(result);
6383
- secureStorage.set(ALL_PLANS_CACHE_KEY, JSON.stringify(result)).catch(() => void 0);
6384
- return result;
6385
- } catch (err) {
6386
- this.log("fetchAllPlans error", err);
6387
- return { plans: [], currentIdentifier: null };
6388
- }
6389
- }
6390
- log(...args) {
6391
- if (this.config?.debug) console.log("[Paywallo Plans]", ...args);
6392
- }
6393
- };
6394
- planService = new PlanService();
6395
- }
6396
- });
6397
-
6398
- // src/domains/plan/index.ts
6399
- var init_plan = __esm({
6400
- "src/domains/plan/index.ts"() {
6401
- "use strict";
6402
- init_PlanCache();
6403
- init_PlanService();
6404
- }
6405
- });
6406
-
6407
- // src/domains/session/index.ts
6408
- var init_session = __esm({
6409
- "src/domains/session/index.ts"() {
6410
- "use strict";
6411
- init_SessionError();
6412
- init_SessionManager();
6413
- }
6414
- });
6415
-
6416
- // src/domains/onboarding/OnboardingError.ts
6417
- var OnboardingError, ONBOARDING_ERROR_CODES;
6418
- var init_OnboardingError = __esm({
6419
- "src/domains/onboarding/OnboardingError.ts"() {
6420
- "use strict";
6421
- init_PaywalloError();
6422
- OnboardingError = class extends PaywalloError {
6423
- constructor(code, message) {
6424
- super("onboarding", code, message);
6425
- this.name = "OnboardingError";
6426
- }
6427
- };
6428
- ONBOARDING_ERROR_CODES = {
6429
- NOT_INITIALIZED: "ONBOARDING_NOT_INITIALIZED",
6430
- INVALID_STEP_NAME: "ONBOARDING_INVALID_STEP_NAME"
6431
- };
6432
- }
6433
- });
6434
-
6435
- // src/domains/onboarding/OnboardingManager.ts
6436
- var OnboardingManager, onboardingManager;
6437
- var init_OnboardingManager = __esm({
6438
- "src/domains/onboarding/OnboardingManager.ts"() {
6439
- "use strict";
6440
- init_OnboardingError();
6441
- OnboardingManager = class {
6442
- constructor() {
6443
- this.pipeline = null;
6444
- this.debug = false;
6445
- this.lastStep = null;
6446
- this.finished = false;
6447
- }
6448
- /**
6449
- * Injects runtime dependencies. Called by PaywalloClient during init.
6450
- */
6451
- injectDeps(config) {
6452
- this.pipeline = config.pipeline;
6453
- this.debug = config.debug ?? false;
6454
- }
6455
- /**
6456
- * Tracks an onboarding step view.
6457
- * Saves the step name internally for implicit drop detection on the backend.
6458
- *
6459
- * @param stepName - Unique name for this onboarding step.
6460
- * @param order - Optional explicit display order (accepts decimals, e.g. 2.1 for A/B variants).
6461
- */
6462
- async step(stepName, order) {
6463
- this.assertConfig();
6464
- this.validateStepName(stepName, "step");
6465
- this.lastStep = stepName;
6466
- const stepPayload = { step_name: stepName };
6467
- if (order !== void 0 && Number.isFinite(order) && order >= 0) {
6468
- stepPayload.order = order;
6469
- }
6470
- const payload = {
6471
- family: "onboarding",
6472
- type: "step",
6473
- ...stepPayload
6474
- };
6475
- return this.emit("onboarding", payload, "step");
6476
- }
6477
- /**
6478
- * Tracks successful completion of the onboarding flow.
6479
- * Marks the flow as finished.
6480
- */
6481
- async complete() {
6482
- this.assertConfig();
6483
- this.finished = true;
6484
- return this.emit("onboarding", { family: "onboarding", type: "complete" }, "complete");
6485
- }
6486
- /**
6487
- * Tracks an explicit drop (abandonment) at the given step.
6488
- * Marks the flow as finished.
6489
- *
6490
- * @param stepName - The step name where the user dropped off.
6491
- */
6492
- async drop(stepName) {
6493
- this.assertConfig();
6494
- this.validateStepName(stepName, "drop");
6495
- this.finished = true;
6496
- const payload = {
6497
- family: "onboarding",
6498
- type: "drop",
6499
- ...{ step_name: stepName }
6500
- };
6501
- return this.emit("onboarding", payload, "drop");
6588
+ }
6589
+ }
6590
+ if (!this.cache.get()) {
6591
+ try {
6592
+ const stored = await secureStorage.get(ALL_PLANS_CACHE_KEY);
6593
+ if (stored) {
6594
+ const parsed = JSON.parse(stored);
6595
+ this.cache.set(parsed);
6596
+ }
6597
+ } catch {
6598
+ }
6599
+ }
6600
+ const memCached = this.cache.get();
6601
+ if (memCached) {
6602
+ void this.fetchAllPlans();
6603
+ const current = memCached.data.plans.find(
6604
+ (p) => p.identifier === memCached.data.currentIdentifier
6605
+ );
6606
+ return current ?? null;
6607
+ }
6608
+ return this.fetchCurrentPlan();
6502
6609
  }
6503
- /** Returns the last step name seen, or null if no step was tracked yet. */
6504
- getLastStep() {
6505
- return this.lastStep;
6610
+ async getAllPlans(forceRefresh = false) {
6611
+ if (!forceRefresh) {
6612
+ const cached2 = this.cache.get();
6613
+ if (cached2 && !cached2.isStale) return cached2.data;
6614
+ }
6615
+ if (!this.cache.get()) {
6616
+ try {
6617
+ const stored = await secureStorage.get(ALL_PLANS_CACHE_KEY);
6618
+ if (stored) {
6619
+ const parsed = JSON.parse(stored);
6620
+ this.cache.set(parsed);
6621
+ }
6622
+ } catch {
6623
+ }
6624
+ }
6625
+ const memCached = this.cache.get();
6626
+ if (memCached) {
6627
+ void this.fetchAllPlans();
6628
+ return memCached.data;
6629
+ }
6630
+ return this.fetchAllPlans();
6506
6631
  }
6507
- /** Returns whether the flow has been marked as finished (complete or drop). */
6508
- isFinished() {
6509
- return this.finished;
6632
+ invalidateCache() {
6633
+ this.cache.invalidate();
6634
+ secureStorage.remove(PLAN_CACHE_KEY).catch(() => void 0);
6635
+ secureStorage.remove(ALL_PLANS_CACHE_KEY).catch(() => void 0);
6510
6636
  }
6511
- async emit(eventName, properties, logLabel) {
6512
- const pipeline = this.pipeline;
6513
- const distinctId = pipeline.getDistinctId();
6514
- if (!distinctId) {
6515
- if (this.debug) console.log(`[Paywallo:Onboarding] skipped (no distinctId): ${logLabel}`);
6516
- return;
6517
- }
6518
- await pipeline.trackEvent(eventName, distinctId, properties);
6519
- if (this.debug) {
6520
- console.log(`[Paywallo:Onboarding] event emitted: ${eventName}.${logLabel}`, properties);
6637
+ getApiClient() {
6638
+ if (!this.config) {
6639
+ throw new PlanError(PLAN_ERROR_CODES.NOT_INITIALIZED, "PlanService not initialized. Call Paywallo.init() first");
6521
6640
  }
6641
+ return this.config.apiClient;
6522
6642
  }
6523
- assertConfig() {
6524
- if (!this.pipeline) {
6525
- throw new OnboardingError(
6526
- ONBOARDING_ERROR_CODES.NOT_INITIALIZED,
6527
- "OnboardingManager not initialized. Call Paywallo.init() first."
6528
- );
6643
+ async fetchCurrentPlan() {
6644
+ try {
6645
+ const res = await this.getApiClient().get("/sdk/plans");
6646
+ if (!res.ok) {
6647
+ this.log("fetchCurrentPlan failed", res.status);
6648
+ return null;
6649
+ }
6650
+ const raw = res.data.data.plan;
6651
+ return raw ? mapPlan(raw) : null;
6652
+ } catch (err) {
6653
+ this.log("fetchCurrentPlan error", err);
6654
+ return null;
6529
6655
  }
6530
6656
  }
6531
- validateStepName(stepName, method) {
6532
- if (typeof stepName !== "string" || stepName.trim().length === 0) {
6533
- throw new OnboardingError(
6534
- ONBOARDING_ERROR_CODES.INVALID_STEP_NAME,
6535
- `OnboardingManager.${method}(): stepName must be a non-empty string.`
6536
- );
6657
+ async fetchAllPlans() {
6658
+ try {
6659
+ const res = await this.getApiClient().get("/sdk/plans/all");
6660
+ if (!res.ok) {
6661
+ this.log("fetchAllPlans failed", res.status);
6662
+ return { plans: [], currentIdentifier: null };
6663
+ }
6664
+ const result = {
6665
+ plans: res.data.data.plans.map(mapPlan),
6666
+ currentIdentifier: res.data.data.current_identifier
6667
+ };
6668
+ this.cache.set(result);
6669
+ secureStorage.set(ALL_PLANS_CACHE_KEY, JSON.stringify(result)).catch(() => void 0);
6670
+ return result;
6671
+ } catch (err) {
6672
+ this.log("fetchAllPlans error", err);
6673
+ return { plans: [], currentIdentifier: null };
6537
6674
  }
6538
6675
  }
6676
+ log(...args) {
6677
+ if (this.config?.debug) console.log("[Paywallo Plans]", ...args);
6678
+ }
6539
6679
  };
6540
- onboardingManager = new OnboardingManager();
6680
+ planService = new PlanService();
6541
6681
  }
6542
6682
  });
6543
6683
 
6544
- // src/domains/onboarding/index.ts
6545
- var init_onboarding = __esm({
6546
- "src/domains/onboarding/index.ts"() {
6684
+ // src/domains/plan/index.ts
6685
+ var init_plan = __esm({
6686
+ "src/domains/plan/index.ts"() {
6547
6687
  "use strict";
6548
- init_OnboardingError();
6549
- init_OnboardingManager();
6688
+ init_PlanCache();
6689
+ init_PlanService();
6690
+ }
6691
+ });
6692
+
6693
+ // src/domains/session/index.ts
6694
+ var init_session = __esm({
6695
+ "src/domains/session/index.ts"() {
6696
+ "use strict";
6697
+ init_SessionError();
6698
+ init_SessionManager();
6550
6699
  }
6551
6700
  });
6552
6701
 
@@ -6622,7 +6771,21 @@ var init_schemas = __esm({
6622
6771
  // `closed`-specific
6623
6772
  closed_at: import_zod.z.string().optional(),
6624
6773
  duration_s: import_zod.z.number().nonnegative().optional(),
6625
- close_reason: import_zod.z.enum(["dismiss", "cta", "purchase", "error", "timeout"]).optional(),
6774
+ // Two emitter dialects coexist: the canonical emitters
6775
+ // (`emitPaywallClosed`/heartbeat) send "dismiss"/"cta"/"purchase"/"error"/
6776
+ // "timeout"; the SuperwallAutoBridge forwards the server-enum values
6777
+ // "purchased"/"dismissed"/"restored" straight through. The server accepts
6778
+ // both — the schema must too, or every bridge close fails debug validation.
6779
+ close_reason: import_zod.z.enum([
6780
+ "dismiss",
6781
+ "cta",
6782
+ "purchase",
6783
+ "error",
6784
+ "timeout",
6785
+ "purchased",
6786
+ "dismissed",
6787
+ "restored"
6788
+ ]).optional(),
6626
6789
  scroll_depth: import_zod.z.number().min(0).max(1).optional(),
6627
6790
  // `purchased`-specific (mínimo — transaction canônica leva detalhes)
6628
6791
  product_id: import_zod.z.string().optional()
@@ -6630,21 +6793,33 @@ var init_schemas = __esm({
6630
6793
  transactionEventSchema = import_zod.z.object({
6631
6794
  type: import_zod.z.enum([
6632
6795
  "completed",
6796
+ "trial_started",
6633
6797
  "failed",
6634
6798
  "refunded",
6635
6799
  "renewed",
6636
6800
  "canceled",
6637
6801
  "expired"
6638
6802
  ]),
6639
- tx_id: import_zod.z.string().min(1),
6803
+ // Server contract key is `transaction_id` (what IngestTranslator reads and
6804
+ // the SuperwallAutoBridge emits); `tx_id` is the legacy key still emitted
6805
+ // by IAPTransactionEmitter — the server accepts both. At least one is
6806
+ // required (enforced by the refine below).
6807
+ transaction_id: import_zod.z.string().min(1).optional(),
6808
+ tx_id: import_zod.z.string().min(1).optional(),
6640
6809
  amount: import_zod.z.number().optional(),
6810
+ // Full (post-trial) price carried on trial_started for analytics, since
6811
+ // `amount` is 0 for a free trial.
6812
+ full_price: import_zod.z.number().nonnegative().optional(),
6641
6813
  currency: import_zod.z.string().length(3).optional(),
6642
6814
  product_id: import_zod.z.string().optional(),
6643
6815
  subscription_id: import_zod.z.string().optional(),
6644
6816
  paywall_id: import_zod.z.string().optional(),
6645
6817
  variant_id: import_zod.z.string().optional(),
6646
6818
  reason: import_zod.z.string().optional()
6647
- }).strict();
6819
+ }).strict().refine((v) => v.transaction_id !== void 0 || v.tx_id !== void 0, {
6820
+ message: "transaction requires transaction_id (or legacy tx_id)",
6821
+ path: ["transaction_id"]
6822
+ });
6648
6823
  onboardingEventSchema = import_zod.z.object({
6649
6824
  type: import_zod.z.enum(["step", "complete", "drop"]),
6650
6825
  step_index: import_zod.z.number().int().nonnegative().optional(),
@@ -7474,8 +7649,13 @@ var init_ApiClientFlags = __esm({
7474
7649
  });
7475
7650
 
7476
7651
  // src/core/ApiClientQueue.ts
7652
+ function isServerError(res) {
7653
+ if (typeof res !== "object" || res === null) return false;
7654
+ const r = res;
7655
+ return r.ok === false && typeof r.status === "number" && r.status >= 500;
7656
+ }
7477
7657
  async function postWithQueue(deps, url, payload, label, priority) {
7478
- if (deps.isOfflineQueueEnabled() && !networkMonitor.isOnline()) {
7658
+ const enqueue = async () => {
7479
7659
  await offlineQueue.enqueue(
7480
7660
  "POST",
7481
7661
  url,
@@ -7484,22 +7664,22 @@ async function postWithQueue(deps, url, payload, label, priority) {
7484
7664
  void 0,
7485
7665
  priority
7486
7666
  );
7667
+ };
7668
+ if (deps.isOfflineQueueEnabled() && !networkMonitor.isOnline()) {
7669
+ await enqueue();
7487
7670
  if (deps.isDebug()) console.log("[Paywallo:ApiClient] Queued for offline:", label, priority ?? "normal");
7488
7671
  return;
7489
7672
  }
7490
7673
  try {
7491
- await deps.post(url, payload);
7674
+ const res = await deps.post(url, payload);
7675
+ if (isServerError(res) && deps.isOfflineQueueEnabled()) {
7676
+ if (deps.isDebug()) console.log("[Paywallo:ApiClient] Server error, queued for retry:", label, priority ?? "normal");
7677
+ await enqueue();
7678
+ }
7492
7679
  } catch (error) {
7493
7680
  if (deps.isDebug()) console.log("[Paywallo:ApiClient] Request failed:", label, error);
7494
7681
  if (deps.isOfflineQueueEnabled()) {
7495
- await offlineQueue.enqueue(
7496
- "POST",
7497
- url,
7498
- payload,
7499
- { "X-App-Key": deps.getAppKey() },
7500
- void 0,
7501
- priority
7502
- );
7682
+ await enqueue();
7503
7683
  return;
7504
7684
  }
7505
7685
  throw error;
@@ -8905,11 +9085,11 @@ var init_PaywalloClient = __esm({
8905
9085
  init_campaign();
8906
9086
  init_identity();
8907
9087
  init_notifications();
9088
+ init_onboarding();
8908
9089
  init_paywall();
8909
9090
  init_plan();
8910
9091
  init_session();
8911
9092
  init_ClientError();
8912
- init_onboarding();
8913
9093
  init_network();
8914
9094
  init_PaywalloAnalytics();
8915
9095
  init_PaywalloFlags();
@@ -9616,8 +9796,41 @@ function usePlanPurchase() {
9616
9796
  };
9617
9797
  }
9618
9798
 
9619
- // src/hooks/usePlans.ts
9799
+ // src/hooks/usePaywalloScreenTracking.ts
9620
9800
  var import_react18 = require("react");
9801
+ init_PaywalloClient();
9802
+ function isNavState(value) {
9803
+ return typeof value === "object" && value !== null && "routes" in value && "index" in value && Array.isArray(value.routes) && typeof value.index === "number";
9804
+ }
9805
+ function getActiveRouteName(state) {
9806
+ try {
9807
+ if (!isNavState(state)) return null;
9808
+ const route = state.routes[state.index];
9809
+ if (!route) return null;
9810
+ if (isNavState(route.state)) {
9811
+ return getActiveRouteName(route.state);
9812
+ }
9813
+ return typeof route.name === "string" ? route.name : null;
9814
+ } catch {
9815
+ return null;
9816
+ }
9817
+ }
9818
+ function usePaywalloScreenTracking() {
9819
+ const lastScreenRef = (0, import_react18.useRef)(null);
9820
+ return (0, import_react18.useCallback)((state) => {
9821
+ if (!PaywalloClient.isReady()) return;
9822
+ const screenName = getActiveRouteName(state);
9823
+ if (!screenName || screenName === lastScreenRef.current) return;
9824
+ lastScreenRef.current = screenName;
9825
+ void PaywalloClient.track("screen_view", {
9826
+ properties: { screen_name: screenName }
9827
+ }).catch(() => {
9828
+ });
9829
+ }, []);
9830
+ }
9831
+
9832
+ // src/hooks/usePlans.ts
9833
+ var import_react19 = require("react");
9621
9834
  async function getPlanService() {
9622
9835
  try {
9623
9836
  const mod = await Promise.resolve().then(() => (init_PlanService(), PlanService_exports));
@@ -9627,11 +9840,11 @@ async function getPlanService() {
9627
9840
  }
9628
9841
  }
9629
9842
  function usePlans() {
9630
- const [allPlans, setAllPlans] = (0, import_react18.useState)([]);
9631
- const [currentPlan, setCurrentPlan] = (0, import_react18.useState)(null);
9632
- const [isLoading, setIsLoading] = (0, import_react18.useState)(true);
9633
- const [error, setError] = (0, import_react18.useState)(null);
9634
- const fetchPlans = (0, import_react18.useCallback)(async (forceRefresh = false) => {
9843
+ const [allPlans, setAllPlans] = (0, import_react19.useState)([]);
9844
+ const [currentPlan, setCurrentPlan] = (0, import_react19.useState)(null);
9845
+ const [isLoading, setIsLoading] = (0, import_react19.useState)(true);
9846
+ const [error, setError] = (0, import_react19.useState)(null);
9847
+ const fetchPlans = (0, import_react19.useCallback)(async (forceRefresh = false) => {
9635
9848
  setIsLoading(true);
9636
9849
  setError(null);
9637
9850
  try {
@@ -9652,17 +9865,17 @@ function usePlans() {
9652
9865
  setIsLoading(false);
9653
9866
  }
9654
9867
  }, []);
9655
- const refresh = (0, import_react18.useCallback)(() => {
9868
+ const refresh = (0, import_react19.useCallback)(() => {
9656
9869
  return fetchPlans(true);
9657
9870
  }, [fetchPlans]);
9658
- (0, import_react18.useEffect)(() => {
9871
+ (0, import_react19.useEffect)(() => {
9659
9872
  void fetchPlans(false);
9660
9873
  }, [fetchPlans]);
9661
9874
  return { currentPlan, allPlans, isLoading, error, refresh };
9662
9875
  }
9663
9876
 
9664
9877
  // src/hooks/useProducts.ts
9665
- var import_react19 = require("react");
9878
+ var import_react20 = require("react");
9666
9879
 
9667
9880
  // src/utils/productMappers.ts
9668
9881
  function mapToIAPProduct(product) {
@@ -9701,11 +9914,11 @@ function mapToFormattedProduct(product) {
9701
9914
 
9702
9915
  // src/hooks/useProducts.ts
9703
9916
  function useProducts(initialProductIds) {
9704
- const [products, setProducts] = (0, import_react19.useState)([]);
9705
- const [formattedProducts, setFormattedProducts] = (0, import_react19.useState)([]);
9706
- const [isLoading, setIsLoading] = (0, import_react19.useState)(false);
9707
- const [error, setError] = (0, import_react19.useState)(null);
9708
- const loadProducts = (0, import_react19.useCallback)(async (productIds) => {
9917
+ const [products, setProducts] = (0, import_react20.useState)([]);
9918
+ const [formattedProducts, setFormattedProducts] = (0, import_react20.useState)([]);
9919
+ const [isLoading, setIsLoading] = (0, import_react20.useState)(false);
9920
+ const [error, setError] = (0, import_react20.useState)(null);
9921
+ const loadProducts = (0, import_react20.useCallback)(async (productIds) => {
9709
9922
  setIsLoading(true);
9710
9923
  setError(null);
9711
9924
  try {
@@ -9719,24 +9932,24 @@ function useProducts(initialProductIds) {
9719
9932
  setIsLoading(false);
9720
9933
  }
9721
9934
  }, []);
9722
- (0, import_react19.useEffect)(() => {
9935
+ (0, import_react20.useEffect)(() => {
9723
9936
  if (initialProductIds && initialProductIds.length > 0) {
9724
9937
  void loadProducts(initialProductIds);
9725
9938
  }
9726
9939
  }, []);
9727
- const getProduct = (0, import_react19.useCallback)(
9940
+ const getProduct = (0, import_react20.useCallback)(
9728
9941
  (productId) => {
9729
9942
  return products.find((p) => p.productId === productId);
9730
9943
  },
9731
9944
  [products]
9732
9945
  );
9733
- const getFormattedProduct = (0, import_react19.useCallback)(
9946
+ const getFormattedProduct = (0, import_react20.useCallback)(
9734
9947
  (productId) => {
9735
9948
  return formattedProducts.find((p) => p.productId === productId);
9736
9949
  },
9737
9950
  [formattedProducts]
9738
9951
  );
9739
- const getPriceInfo = (0, import_react19.useCallback)(
9952
+ const getPriceInfo = (0, import_react20.useCallback)(
9740
9953
  (productId) => {
9741
9954
  const p = formattedProducts.find((fp) => fp.productId === productId);
9742
9955
  if (!p) return void 0;
@@ -9765,14 +9978,14 @@ function useProducts(initialProductIds) {
9765
9978
  }
9766
9979
 
9767
9980
  // src/hooks/usePurchase.ts
9768
- var import_react20 = require("react");
9981
+ var import_react21 = require("react");
9769
9982
  function toIAPPurchase(p) {
9770
9983
  return { productId: p.productId, transactionId: p.transactionId, transactionDate: p.transactionDate, transactionReceipt: p.receipt };
9771
9984
  }
9772
9985
  function usePurchase() {
9773
- const [state, setState] = (0, import_react20.useState)("idle");
9774
- const [error, setError] = (0, import_react20.useState)(null);
9775
- const purchase = (0, import_react20.useCallback)(async (productId) => {
9986
+ const [state, setState] = (0, import_react21.useState)("idle");
9987
+ const [error, setError] = (0, import_react21.useState)(null);
9988
+ const purchase = (0, import_react21.useCallback)(async (productId) => {
9776
9989
  setState("purchasing");
9777
9990
  setError(null);
9778
9991
  try {
@@ -9800,7 +10013,7 @@ function usePurchase() {
9800
10013
  return { status: "failed", error: purchaseError };
9801
10014
  }
9802
10015
  }, []);
9803
- const restore = (0, import_react20.useCallback)(async () => {
10016
+ const restore = (0, import_react21.useCallback)(async () => {
9804
10017
  setState("restoring");
9805
10018
  setError(null);
9806
10019
  try {
@@ -9819,14 +10032,14 @@ function usePurchase() {
9819
10032
  }
9820
10033
 
9821
10034
  // src/hooks/useSubscription.ts
9822
- var import_react21 = require("react");
10035
+ var import_react22 = require("react");
9823
10036
  init_SubscriptionManager();
9824
10037
  var EMPTY_ENTITLEMENTS = [];
9825
10038
  function useSubscription() {
9826
- const [status, setStatus] = (0, import_react21.useState)(null);
9827
- const [isLoading, setIsLoading] = (0, import_react21.useState)(true);
9828
- const [error, setError] = (0, import_react21.useState)(null);
9829
- const refresh = (0, import_react21.useCallback)(async () => {
10039
+ const [status, setStatus] = (0, import_react22.useState)(null);
10040
+ const [isLoading, setIsLoading] = (0, import_react22.useState)(true);
10041
+ const [error, setError] = (0, import_react22.useState)(null);
10042
+ const refresh = (0, import_react22.useCallback)(async () => {
9830
10043
  setIsLoading(true);
9831
10044
  setError(null);
9832
10045
  try {
@@ -9839,7 +10052,7 @@ function useSubscription() {
9839
10052
  setIsLoading(false);
9840
10053
  }
9841
10054
  }, []);
9842
- const restore = (0, import_react21.useCallback)(async () => {
10055
+ const restore = (0, import_react22.useCallback)(async () => {
9843
10056
  setIsLoading(true);
9844
10057
  setError(null);
9845
10058
  try {
@@ -9852,7 +10065,7 @@ function useSubscription() {
9852
10065
  setIsLoading(false);
9853
10066
  }
9854
10067
  }, []);
9855
- (0, import_react21.useEffect)(() => {
10068
+ (0, import_react22.useEffect)(() => {
9856
10069
  void subscriptionManager.getSubscriptionStatus().then((s) => {
9857
10070
  setStatus(s);
9858
10071
  setIsLoading(false);
@@ -9879,39 +10092,6 @@ function useSubscription() {
9879
10092
  };
9880
10093
  }
9881
10094
 
9882
- // src/hooks/usePaywalloScreenTracking.ts
9883
- var import_react22 = require("react");
9884
- init_PaywalloClient();
9885
- function isNavState(value) {
9886
- return typeof value === "object" && value !== null && "routes" in value && "index" in value && Array.isArray(value.routes) && typeof value.index === "number";
9887
- }
9888
- function getActiveRouteName(state) {
9889
- try {
9890
- if (!isNavState(state)) return null;
9891
- const route = state.routes[state.index];
9892
- if (!route) return null;
9893
- if (isNavState(route.state)) {
9894
- return getActiveRouteName(route.state);
9895
- }
9896
- return typeof route.name === "string" ? route.name : null;
9897
- } catch {
9898
- return null;
9899
- }
9900
- }
9901
- function usePaywalloScreenTracking() {
9902
- const lastScreenRef = (0, import_react22.useRef)(null);
9903
- return (0, import_react22.useCallback)((state) => {
9904
- if (!PaywalloClient.isReady()) return;
9905
- const screenName = getActiveRouteName(state);
9906
- if (!screenName || screenName === lastScreenRef.current) return;
9907
- lastScreenRef.current = screenName;
9908
- void PaywalloClient.track("screen_view", {
9909
- properties: { screen_name: screenName }
9910
- }).catch(() => {
9911
- });
9912
- }, []);
9913
- }
9914
-
9915
10095
  // src/PaywalloProvider.tsx
9916
10096
  var import_react28 = require("react");
9917
10097
  var import_react_native29 = require("react-native");
@@ -9919,6 +10099,7 @@ init_PaywalloClient();
9919
10099
  init_NativeStorage();
9920
10100
  init_SecureStorage();
9921
10101
  init_campaign();
10102
+ init_identity();
9922
10103
  init_paywall();
9923
10104
  init_paywall();
9924
10105
  init_paywall();
@@ -9930,7 +10111,9 @@ init_paywall();
9930
10111
  init_identity();
9931
10112
  var CONFIG_POLL_INTERVAL_MS = 250;
9932
10113
  var CONFIG_POLL_MAX_ATTEMPTS = 40;
10114
+ var CONFIG_RETRY_DELAYS_MS = [3e4, 6e4, 12e4];
9933
10115
  var lastSignature = null;
10116
+ var retryTimer = null;
9934
10117
  function log(debug, msg, data) {
9935
10118
  if (!debug) return;
9936
10119
  if (data !== void 0) console.log(`[Paywallo:SuperwallAttr] ${msg}`, data);
@@ -9997,28 +10180,37 @@ async function waitForConfigured(mod, debug) {
9997
10180
  status = String(await mod.getConfigurationStatus());
9998
10181
  } catch (err) {
9999
10182
  log(debug, "getConfigurationStatus error", { err: String(err) });
10000
- return false;
10183
+ return "failed";
10001
10184
  }
10002
- if (status === "CONFIGURED") return true;
10185
+ if (status === "CONFIGURED") return "configured";
10003
10186
  if (status === "FAILED") {
10004
10187
  log(debug, "Superwall configuration FAILED \u2014 skipping attribute push");
10005
- return false;
10188
+ return "failed";
10006
10189
  }
10007
10190
  await delay(CONFIG_POLL_INTERVAL_MS);
10008
10191
  }
10009
10192
  log(debug, "Superwall not configured within timeout \u2014 skipping attribute push");
10010
- return false;
10193
+ return "timeout";
10011
10194
  }
10012
10195
  async function pushAttributionToSuperwall(debug = false) {
10196
+ clearRetryTimer();
10197
+ await attemptPush(debug, 0);
10198
+ }
10199
+ async function attemptPush(debug, retryIndex) {
10200
+ const native = await resolveSuperwallNativeModule(debug);
10201
+ if (!native) return;
10202
+ const waited = await waitForConfigured(native, debug);
10203
+ if (waited === "timeout") {
10204
+ scheduleRetry(debug, retryIndex);
10205
+ return;
10206
+ }
10207
+ if (waited !== "configured") return;
10013
10208
  const attrs = buildSuperwallAttributes(attributionTracker.get());
10014
10209
  const signature = JSON.stringify(attrs);
10015
10210
  if (signature === lastSignature) {
10016
10211
  log(debug, "attributes unchanged \u2014 skipping push");
10017
10212
  return;
10018
10213
  }
10019
- const native = await resolveSuperwallNativeModule(debug);
10020
- if (!native) return;
10021
- if (!await waitForConfigured(native, debug)) return;
10022
10214
  try {
10023
10215
  await native.setUserAttributes(attrs);
10024
10216
  lastSignature = signature;
@@ -10027,6 +10219,28 @@ async function pushAttributionToSuperwall(debug = false) {
10027
10219
  log(debug, "setUserAttributes error", { err: String(err) });
10028
10220
  }
10029
10221
  }
10222
+ function scheduleRetry(debug, retryIndex) {
10223
+ const delayMs = CONFIG_RETRY_DELAYS_MS[retryIndex];
10224
+ if (delayMs === void 0) {
10225
+ log(debug, "Superwall never configured \u2014 giving up attribute push");
10226
+ return;
10227
+ }
10228
+ clearRetryTimer();
10229
+ log(debug, "scheduling attribute push retry", { retryIndex, delayMs });
10230
+ retryTimer = setTimeout(() => {
10231
+ retryTimer = null;
10232
+ void attemptPush(debug, retryIndex + 1);
10233
+ }, delayMs);
10234
+ }
10235
+ function clearRetryTimer() {
10236
+ if (retryTimer !== null) {
10237
+ clearTimeout(retryTimer);
10238
+ retryTimer = null;
10239
+ }
10240
+ }
10241
+ function cancelSuperwallAttributeRetry() {
10242
+ clearRetryTimer();
10243
+ }
10030
10244
 
10031
10245
  // src/domains/paywall/SuperwallAutoBridge.ts
10032
10246
  init_PaywalloClient();
@@ -10059,6 +10273,7 @@ function buildDeterministicEventId(parts) {
10059
10273
 
10060
10274
  // src/domains/paywall/SuperwallAutoBridge.ts
10061
10275
  var bridgeStarted = false;
10276
+ var startInProgress = false;
10062
10277
  var unsubscribe = null;
10063
10278
  var debugEnabled = false;
10064
10279
  function log2(msg, data) {
@@ -10099,8 +10314,7 @@ function mapDismissToCloseReason(resultType) {
10099
10314
  case "purchased":
10100
10315
  return "purchased";
10101
10316
  case "restored":
10102
- return "purchased";
10103
- // restore counts as a purchase outcome for funnel
10317
+ return "restored";
10104
10318
  case "declined":
10105
10319
  return "dismissed";
10106
10320
  default:
@@ -10136,17 +10350,66 @@ function buildCallbacks() {
10136
10350
  const identifier = "paywallIdentifier" in safe && typeof safe.paywallIdentifier === "string" ? safe.paywallIdentifier : productId;
10137
10351
  log2("onPurchase", { productId, identifier });
10138
10352
  void trackPaywallEvent("purchased", identifier, ts, { product_id: productId });
10353
+ },
10354
+ // Global Superwall event listener — used specifically for `transactionComplete`
10355
+ // which is the only event that exposes the full product (currency/price) +
10356
+ // transaction (storeTransactionId) shape needed to create a server-side
10357
+ // `transactions` row. Other events are skipped (paywallOpen/Dismiss already
10358
+ // handled above with their typed callbacks).
10359
+ onSuperwallEvent: (info) => {
10360
+ const ev = info?.event;
10361
+ if (!ev || ev.event !== "transactionComplete") return;
10362
+ const ts = Date.now();
10363
+ const product = ev.product;
10364
+ const transaction = ev.transaction;
10365
+ if (!product) {
10366
+ log2("transactionComplete missing product \u2014 skipping transaction track");
10367
+ return;
10368
+ }
10369
+ const productId = product.productIdentifier ?? product.id ?? "unknown";
10370
+ const currency = product.currencyCode ?? "USD";
10371
+ const price = product.price ?? 0;
10372
+ const transactionId = transaction?.purchaseToken ?? transaction?.storeTransactionId ?? transaction?.originalTransactionIdentifier ?? `${productId}_${String(ts)}`;
10373
+ const introPrice = product.trialPeriodPrice ?? 0;
10374
+ const isTrial = product.hasFreeTrial === true && introPrice <= 0;
10375
+ const isPaidIntro = introPrice > 0;
10376
+ const paywallIdentifier = ev.paywallInfo?.identifier;
10377
+ log2("onSuperwallEvent: transactionComplete", { productId, currency, price, introPrice, transactionId, isTrial });
10378
+ void (async () => {
10379
+ try {
10380
+ await PaywalloClient.track("transaction", {
10381
+ priority: "critical",
10382
+ properties: {
10383
+ type: isTrial ? "trial_started" : "completed",
10384
+ // Server's IngestTranslator looks for `transaction_id`/`product_id`
10385
+ // /`amount`/`currency`/`paywall_id` keys. A free trial MUST carry
10386
+ // amount=0 (no revenue at start); a paid intro offer carries its
10387
+ // intro amount (real revenue); a normal purchase carries `price`.
10388
+ // `full_price` carries the recurring full price so the server can
10389
+ // value the eventual conversion (free trial) or keep the headline
10390
+ // price alongside the discounted intro amount (paid intro).
10391
+ transaction_id: transactionId,
10392
+ product_id: productId,
10393
+ amount: isTrial ? 0 : isPaidIntro ? introPrice : price,
10394
+ currency,
10395
+ ...isTrial || isPaidIntro ? { full_price: price } : {},
10396
+ ...paywallIdentifier ? { paywall_id: paywallIdentifier } : {}
10397
+ }
10398
+ });
10399
+ } catch (err) {
10400
+ log2("transactionComplete track error", { err: String(err) });
10401
+ }
10402
+ })();
10139
10403
  }
10140
- // onSuperwallEvent removed — transaction sale events are sourced exclusively
10141
- // from server-side webhooks (Apple/Google/Superwall/RevenueCat).
10142
10404
  };
10143
10405
  }
10144
10406
  function startSuperwallAutoBridge(debug = false) {
10145
10407
  debugEnabled = debug;
10146
- if (bridgeStarted) {
10147
- log2("already started, skipping");
10408
+ if (bridgeStarted || startInProgress) {
10409
+ log2("already started or starting, skipping");
10148
10410
  return;
10149
10411
  }
10412
+ startInProgress = true;
10150
10413
  void detectAndStart();
10151
10414
  }
10152
10415
  async function detectAndStart() {
@@ -10157,6 +10420,7 @@ async function detectAndStart() {
10157
10420
  const internal = await import("expo-superwall/build/src/internal/superwallEventBridge");
10158
10421
  if (typeof internal.subscribeToSuperwallEvents !== "function") {
10159
10422
  log2("expo-superwall subscribeToSuperwallEvents not found \u2014 bridge inactive");
10423
+ startInProgress = false;
10160
10424
  return;
10161
10425
  }
10162
10426
  subscribeFn = internal.subscribeToSuperwallEvents;
@@ -10165,6 +10429,11 @@ async function detectAndStart() {
10165
10429
  }
10166
10430
  } catch {
10167
10431
  log2("expo-superwall not installed \u2014 bridge inactive");
10432
+ startInProgress = false;
10433
+ return;
10434
+ }
10435
+ if (!startInProgress) {
10436
+ log2("start cancelled by stop \u2014 bridge inactive");
10168
10437
  return;
10169
10438
  }
10170
10439
  try {
@@ -10174,7 +10443,18 @@ async function detectAndStart() {
10174
10443
  log2("bridge started");
10175
10444
  } catch (err) {
10176
10445
  log2("subscribe failed", { err: String(err) });
10446
+ } finally {
10447
+ startInProgress = false;
10448
+ }
10449
+ }
10450
+ function stopSuperwallAutoBridge() {
10451
+ if (unsubscribe) {
10452
+ unsubscribe();
10453
+ unsubscribe = null;
10177
10454
  }
10455
+ bridgeStarted = false;
10456
+ startInProgress = false;
10457
+ log2("bridge stopped");
10178
10458
  }
10179
10459
 
10180
10460
  // src/PaywalloProvider.tsx
@@ -10741,6 +11021,9 @@ function PaywalloProvider({ children, config }) {
10741
11021
  const configRef = (0, import_react28.useRef)(config);
10742
11022
  (0, import_react28.useEffect)(() => {
10743
11023
  let mounted = true;
11024
+ const unsubscribeAttribution = attributionTracker.onCapture(() => {
11025
+ void pushAttributionToSuperwall(configRef.current.debug);
11026
+ });
10744
11027
  const bootstrap = async () => {
10745
11028
  if (!PaywalloClient.isReady()) {
10746
11029
  initLocalization({ detectDevice: true });
@@ -10751,11 +11034,10 @@ function PaywalloProvider({ children, config }) {
10751
11034
  setInitError(err instanceof Error ? err : new ClientError(CLIENT_ERROR_CODES.NOT_INITIALIZED, String(err)));
10752
11035
  return;
10753
11036
  }
10754
- if (!mounted) return;
10755
- startSuperwallAutoBridge(configRef.current.debug);
10756
- void pushAttributionToSuperwall(configRef.current.debug);
10757
11037
  }
10758
11038
  if (!mounted) return;
11039
+ startSuperwallAutoBridge(configRef.current.debug);
11040
+ void pushAttributionToSuperwall(configRef.current.debug);
10759
11041
  setDistinctId(PaywalloClient.getDistinctId());
10760
11042
  setIsInitialized(true);
10761
11043
  if (!autoPreloadTriggeredRef.current) {
@@ -10773,6 +11055,9 @@ function PaywalloProvider({ children, config }) {
10773
11055
  void bootstrap();
10774
11056
  return () => {
10775
11057
  mounted = false;
11058
+ unsubscribeAttribution();
11059
+ stopSuperwallAutoBridge();
11060
+ cancelSuperwallAttributeRetry();
10776
11061
  };
10777
11062
  }, [preloadCampaign]);
10778
11063
  const getPaywallConfig = (0, import_react28.useCallback)(async (p) => {