@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.mjs CHANGED
@@ -271,8 +271,8 @@ var init_ClientError = __esm({
271
271
 
272
272
  // src/utils/NativeDeviceInfo.ts
273
273
  import { Platform } from "react-native";
274
- import * as Device from "expo-device";
275
274
  import * as Application from "expo-application";
275
+ import * as Device from "expo-device";
276
276
  import * as Localization from "expo-localization";
277
277
  function assertExpoModules() {
278
278
  if (!Device || Device.osName === void 0) {
@@ -952,7 +952,9 @@ var init_AdvertisingIdManager = __esm({
952
952
  return this.cached;
953
953
  }
954
954
  async collect(requestATT = false) {
955
- if (this.cached) return this.cached;
955
+ if (this.cached && !(requestATT && this.cached.attStatus === "undetermined")) {
956
+ return this.cached;
957
+ }
956
958
  const fallback = { idfv: null, idfa: null, gaid: null, attStatus: "unavailable" };
957
959
  try {
958
960
  const tracking = await import("expo-tracking-transparency");
@@ -1375,6 +1377,7 @@ var init_AttributionTracker = __esm({
1375
1377
  constructor(debug = false) {
1376
1378
  this.cache = null;
1377
1379
  this.captureInProgress = false;
1380
+ this.listeners = /* @__PURE__ */ new Set();
1378
1381
  this.storage = new SecureStorage(debug);
1379
1382
  }
1380
1383
  /**
@@ -1414,10 +1417,31 @@ var init_AttributionTracker = __esm({
1414
1417
  };
1415
1418
  await this.storage.set(ATTRIBUTION_STORAGE_KEY, JSON.stringify(capture));
1416
1419
  this.cache = capture;
1420
+ this.notifyListeners(capture);
1417
1421
  } finally {
1418
1422
  this.captureInProgress = false;
1419
1423
  }
1420
1424
  }
1425
+ /**
1426
+ * Subscribes to the first real attribution write (first-write-wins — no-op
1427
+ * captures never fire). Useful for late enrichment consumers (e.g. re-push
1428
+ * of Superwall attributes when a warm deep link / deferred match lands after
1429
+ * init). Returns an unsubscribe function.
1430
+ */
1431
+ onCapture(listener) {
1432
+ this.listeners.add(listener);
1433
+ return () => {
1434
+ this.listeners.delete(listener);
1435
+ };
1436
+ }
1437
+ notifyListeners(capture) {
1438
+ for (const listener of this.listeners) {
1439
+ try {
1440
+ listener(capture);
1441
+ } catch {
1442
+ }
1443
+ }
1444
+ }
1421
1445
  /**
1422
1446
  * Returns the persisted attribution capture, or `null` if none exists.
1423
1447
  * Synchronous — reads from in-memory cache (populated via `loadFromStorage()`).
@@ -1511,6 +1535,68 @@ var init_InstallIdempotency = __esm({
1511
1535
  }
1512
1536
  });
1513
1537
 
1538
+ // src/domains/identity/deferredAppLink.ts
1539
+ function parseQuery(s) {
1540
+ const result = {};
1541
+ for (const pair of s.split("&")) {
1542
+ const eqIndex = pair.indexOf("=");
1543
+ if (eqIndex === -1) continue;
1544
+ try {
1545
+ const key = decodeURIComponent(pair.slice(0, eqIndex).replace(/\+/g, " "));
1546
+ const value = decodeURIComponent(pair.slice(eqIndex + 1).replace(/\+/g, " "));
1547
+ if (key) result[key] = value;
1548
+ } catch {
1549
+ }
1550
+ }
1551
+ return result;
1552
+ }
1553
+ function parseDeferredAppLink(url) {
1554
+ if (!url || url.trim() === "") return null;
1555
+ const raw = url.slice(0, 2048);
1556
+ const cleanRaw = raw.split("#")[0];
1557
+ const qIndex = cleanRaw.indexOf("?");
1558
+ if (qIndex === -1) return null;
1559
+ const topParams = parseQuery(cleanRaw.slice(qIndex + 1));
1560
+ let fbclid = nz(topParams["fbclid"]);
1561
+ let utmSource = nz(topParams["utm_source"]);
1562
+ let utmMedium = nz(topParams["utm_medium"]);
1563
+ let utmCampaign = nz(topParams["utm_campaign"]);
1564
+ let ttclid = nz(topParams["ttclid"]);
1565
+ let trackingId = nz(topParams["tracking_id"]);
1566
+ let targetUrl = null;
1567
+ if (topParams["target_url"]) {
1568
+ try {
1569
+ targetUrl = decodeURIComponent(topParams["target_url"]);
1570
+ } catch {
1571
+ targetUrl = topParams["target_url"];
1572
+ }
1573
+ }
1574
+ if (!fbclid && targetUrl) {
1575
+ const cleanTargetUrl = targetUrl.split("#")[0];
1576
+ const innerQ = cleanTargetUrl.indexOf("?");
1577
+ if (innerQ !== -1) {
1578
+ const inner = parseQuery(cleanTargetUrl.slice(innerQ + 1));
1579
+ fbclid = fbclid ?? nz(inner["fbclid"]);
1580
+ utmSource = utmSource ?? nz(inner["utm_source"]);
1581
+ utmMedium = utmMedium ?? nz(inner["utm_medium"]);
1582
+ utmCampaign = utmCampaign ?? nz(inner["utm_campaign"]);
1583
+ ttclid = ttclid ?? nz(inner["ttclid"]);
1584
+ trackingId = trackingId ?? nz(inner["tracking_id"]);
1585
+ }
1586
+ }
1587
+ if (!fbclid && !ttclid && !trackingId && !utmSource && !utmMedium && !utmCampaign) {
1588
+ return null;
1589
+ }
1590
+ return { fbclid, utmSource, utmMedium, utmCampaign, ttclid, trackingId, targetUrl, raw };
1591
+ }
1592
+ var nz;
1593
+ var init_deferredAppLink = __esm({
1594
+ "src/domains/identity/deferredAppLink.ts"() {
1595
+ "use strict";
1596
+ nz = (v) => typeof v === "string" && v.trim().length > 0 ? v : null;
1597
+ }
1598
+ });
1599
+
1514
1600
  // src/domains/identity/MetaBridge.ts
1515
1601
  import { NativeModules as NativeModules2 } from "react-native";
1516
1602
  var TIMEOUT_MS, NativeBridge, MetaBridge, metaBridge;
@@ -1609,68 +1695,6 @@ var init_MetaBridge = __esm({
1609
1695
  }
1610
1696
  });
1611
1697
 
1612
- // src/domains/identity/deferredAppLink.ts
1613
- function parseQuery(s) {
1614
- const result = {};
1615
- for (const pair of s.split("&")) {
1616
- const eqIndex = pair.indexOf("=");
1617
- if (eqIndex === -1) continue;
1618
- try {
1619
- const key = decodeURIComponent(pair.slice(0, eqIndex).replace(/\+/g, " "));
1620
- const value = decodeURIComponent(pair.slice(eqIndex + 1).replace(/\+/g, " "));
1621
- if (key) result[key] = value;
1622
- } catch {
1623
- }
1624
- }
1625
- return result;
1626
- }
1627
- function parseDeferredAppLink(url) {
1628
- if (!url || url.trim() === "") return null;
1629
- const raw = url.slice(0, 2048);
1630
- const cleanRaw = raw.split("#")[0];
1631
- const qIndex = cleanRaw.indexOf("?");
1632
- if (qIndex === -1) return null;
1633
- const topParams = parseQuery(cleanRaw.slice(qIndex + 1));
1634
- let fbclid = nz(topParams["fbclid"]);
1635
- let utmSource = nz(topParams["utm_source"]);
1636
- let utmMedium = nz(topParams["utm_medium"]);
1637
- let utmCampaign = nz(topParams["utm_campaign"]);
1638
- let ttclid = nz(topParams["ttclid"]);
1639
- let trackingId = nz(topParams["tracking_id"]);
1640
- let targetUrl = null;
1641
- if (topParams["target_url"]) {
1642
- try {
1643
- targetUrl = decodeURIComponent(topParams["target_url"]);
1644
- } catch {
1645
- targetUrl = topParams["target_url"];
1646
- }
1647
- }
1648
- if (!fbclid && targetUrl) {
1649
- const cleanTargetUrl = targetUrl.split("#")[0];
1650
- const innerQ = cleanTargetUrl.indexOf("?");
1651
- if (innerQ !== -1) {
1652
- const inner = parseQuery(cleanTargetUrl.slice(innerQ + 1));
1653
- fbclid = fbclid ?? nz(inner["fbclid"]);
1654
- utmSource = utmSource ?? nz(inner["utm_source"]);
1655
- utmMedium = utmMedium ?? nz(inner["utm_medium"]);
1656
- utmCampaign = utmCampaign ?? nz(inner["utm_campaign"]);
1657
- ttclid = ttclid ?? nz(inner["ttclid"]);
1658
- trackingId = trackingId ?? nz(inner["tracking_id"]);
1659
- }
1660
- }
1661
- if (!fbclid && !ttclid && !trackingId && !utmSource && !utmMedium && !utmCampaign) {
1662
- return null;
1663
- }
1664
- return { fbclid, utmSource, utmMedium, utmCampaign, ttclid, trackingId, targetUrl, raw };
1665
- }
1666
- var nz;
1667
- var init_deferredAppLink = __esm({
1668
- "src/domains/identity/deferredAppLink.ts"() {
1669
- "use strict";
1670
- nz = (v) => typeof v === "string" && v.trim().length > 0 ? v : null;
1671
- }
1672
- });
1673
-
1674
1698
  // src/domains/identity/InstallReferrerManager.ts
1675
1699
  import { Platform as Platform3 } from "react-native";
1676
1700
  var REFERRER_TIMEOUT_MS, InstallReferrerManager, installReferrerManager;
@@ -1678,8 +1702,8 @@ var init_InstallReferrerManager = __esm({
1678
1702
  "src/domains/identity/InstallReferrerManager.ts"() {
1679
1703
  "use strict";
1680
1704
  init_NativeDeviceInfo();
1681
- init_MetaBridge();
1682
1705
  init_deferredAppLink();
1706
+ init_MetaBridge();
1683
1707
  REFERRER_TIMEOUT_MS = 5e3;
1684
1708
  InstallReferrerManager = class {
1685
1709
  async getReferrer() {
@@ -1780,7 +1804,17 @@ var init_InstallReferrerManager = __esm({
1780
1804
 
1781
1805
  // src/domains/identity/InstallTracker.ts
1782
1806
  import { Dimensions, PixelRatio, Platform as Platform4 } from "react-native";
1783
- var InstallTracker;
1807
+ function extractDeferredAttribution(raw) {
1808
+ if (raw === null || typeof raw !== "object") return null;
1809
+ const source = raw;
1810
+ const out = {};
1811
+ for (const field of DEFERRED_ATTRIBUTION_FIELDS) {
1812
+ const value = source[field];
1813
+ if (typeof value === "string" && value !== "") out[field] = value;
1814
+ }
1815
+ return Object.keys(out).length > 0 ? out : null;
1816
+ }
1817
+ var InstallTracker, DEFERRED_ATTRIBUTION_FIELDS;
1784
1818
  var init_InstallTracker = __esm({
1785
1819
  "src/domains/identity/InstallTracker.ts"() {
1786
1820
  "use strict";
@@ -1945,7 +1979,7 @@ var init_InstallTracker = __esm({
1945
1979
  const baseUrl = apiClient.getBaseUrl();
1946
1980
  const appKey = apiClient.getAppKey();
1947
1981
  const referrer = await installReferrerManager.getReferrer();
1948
- await fetch(`${baseUrl}/sdk/attribution/deferred-match/${appKey}`, {
1982
+ const response = await fetch(`${baseUrl}/sdk/attribution/deferred-match/${appKey}`, {
1949
1983
  method: "POST",
1950
1984
  headers: { "Content-Type": "application/json", "X-App-Key": appKey },
1951
1985
  body: JSON.stringify({
@@ -1967,12 +2001,42 @@ var init_InstallTracker = __esm({
1967
2001
  });
1968
2002
  await storage.set(INSTALL_KEYS.DEFERRED_MATCH_DONE, "1").catch(() => {
1969
2003
  });
2004
+ await this.applyDeferredMatchResponse(response);
1970
2005
  } catch {
1971
2006
  } finally {
1972
2007
  clearTimeout(timer);
1973
2008
  }
1974
2009
  }
2010
+ async applyDeferredMatchResponse(response) {
2011
+ try {
2012
+ const body = await response.json();
2013
+ if (body === null || typeof body !== "object") return;
2014
+ const outer = body;
2015
+ const source = typeof outer.data === "object" && outer.data !== null ? outer.data : body;
2016
+ if (source.matched !== true) return;
2017
+ const attribution = extractDeferredAttribution(source.attribution);
2018
+ if (!attribution) return;
2019
+ await attributionTracker.capture({
2020
+ ...attribution,
2021
+ installReferrerSource: "deferred_match"
2022
+ });
2023
+ } catch {
2024
+ }
2025
+ }
1975
2026
  };
2027
+ DEFERRED_ATTRIBUTION_FIELDS = [
2028
+ "fbclid",
2029
+ "gclid",
2030
+ "ttclid",
2031
+ "utmSource",
2032
+ "utmMedium",
2033
+ "utmCampaign",
2034
+ "utmContent",
2035
+ "utmTerm",
2036
+ "tiktokCampaignId",
2037
+ "tiktokAdgroupId",
2038
+ "tiktokAdId"
2039
+ ];
1976
2040
  }
1977
2041
  });
1978
2042
 
@@ -2547,8 +2611,8 @@ var init_NotificationHandlerSetup = __esm({
2547
2611
  // src/core/version.ts
2548
2612
  function resolveVersion() {
2549
2613
  try {
2550
- if ("2.5.1") {
2551
- return "2.5.1";
2614
+ if ("2.5.3") {
2615
+ return "2.5.3";
2552
2616
  }
2553
2617
  } catch {
2554
2618
  }
@@ -3167,99 +3231,236 @@ var init_notifications = __esm({
3167
3231
  }
3168
3232
  });
3169
3233
 
3170
- // src/domains/paywall/paywallCloseReason.ts
3171
- function toCanonicalReason(reason) {
3172
- if (reason in CLOSE_REASON_MAP) return CLOSE_REASON_MAP[reason];
3173
- return reason;
3174
- }
3175
- var CLOSE_REASON_MAP;
3176
- var init_paywallCloseReason = __esm({
3177
- "src/domains/paywall/paywallCloseReason.ts"() {
3234
+ // src/domains/onboarding/OnboardingError.ts
3235
+ var OnboardingError, ONBOARDING_ERROR_CODES;
3236
+ var init_OnboardingError = __esm({
3237
+ "src/domains/onboarding/OnboardingError.ts"() {
3178
3238
  "use strict";
3179
- CLOSE_REASON_MAP = {
3180
- dismissed: "dismiss",
3181
- purchased: "purchase",
3182
- backgrounded: "dismiss",
3183
- timeout: "timeout"
3239
+ init_PaywalloError();
3240
+ OnboardingError = class extends PaywalloError {
3241
+ constructor(code, message) {
3242
+ super("onboarding", code, message);
3243
+ this.name = "OnboardingError";
3244
+ }
3245
+ };
3246
+ ONBOARDING_ERROR_CODES = {
3247
+ NOT_INITIALIZED: "ONBOARDING_NOT_INITIALIZED",
3248
+ INVALID_STEP_NAME: "ONBOARDING_INVALID_STEP_NAME"
3184
3249
  };
3185
3250
  }
3186
3251
  });
3187
3252
 
3188
- // src/domains/paywall/emitPaywallClosed.ts
3189
- async function emitPaywallClosed(opts) {
3190
- try {
3191
- const api = PaywalloClient.getApiClient();
3192
- if (!api) return;
3193
- const sessionId = PaywalloClient.getSessionId();
3194
- const closedAt = /* @__PURE__ */ new Date();
3195
- const durationS = Math.max(0, Math.round((closedAt.getTime() - opts.presentedAt) / 1e3));
3196
- await api.trackEvent("paywall", PaywalloClient.getDistinctId(), {
3197
- type: "closed",
3198
- paywall_id: opts.paywallId,
3199
- placement: opts.placement,
3200
- closed_at: closedAt.toISOString(),
3201
- duration_s: durationS,
3202
- close_reason: toCanonicalReason(opts.reason),
3203
- ...opts.variantId && { variant_id: opts.variantId },
3204
- ...opts.variantKey && { variant_key: opts.variantKey },
3205
- ...opts.campaignId && { campaign_id: opts.campaignId },
3206
- ...typeof opts.scrollDepth === "number" && { scroll_depth: opts.scrollDepth },
3207
- ...sessionId && { sessionId }
3208
- });
3209
- } catch (err) {
3210
- if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] emitPaywallClosed failed", err);
3211
- }
3212
- }
3213
- var init_emitPaywallClosed = __esm({
3214
- "src/domains/paywall/emitPaywallClosed.ts"() {
3215
- "use strict";
3216
- init_PaywalloClient();
3217
- init_paywallCloseReason();
3218
- }
3219
- });
3220
-
3221
- // src/domains/paywall/extractFirstPageBackground.ts
3222
- function extractFirstPageBackground(craftData) {
3223
- if (!craftData) return DEFAULT_BACKGROUND;
3224
- try {
3225
- const data = JSON.parse(craftData);
3226
- const nodes = { ...data.nodes, ...data.sharedNodes };
3227
- const rootId = data.rootId;
3228
- if (!rootId || !nodes[rootId]) return DEFAULT_BACKGROUND;
3229
- const firstPageId = nodes[rootId].children?.[0];
3230
- if (!firstPageId) return DEFAULT_BACKGROUND;
3231
- const bg = nodes[firstPageId]?.props?.backgroundColor;
3232
- return typeof bg === "string" && bg.length > 0 && bg !== "transparent" ? bg : DEFAULT_BACKGROUND;
3233
- } catch {
3234
- return DEFAULT_BACKGROUND;
3235
- }
3236
- }
3237
- function isColorDark(color) {
3238
- const hex = color.replace("#", "").trim();
3239
- const normalized = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex;
3240
- if (normalized.length !== 6) return false;
3241
- const r = parseInt(normalized.slice(0, 2), 16);
3242
- const g = parseInt(normalized.slice(2, 4), 16);
3243
- const b = parseInt(normalized.slice(4, 6), 16);
3244
- if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return false;
3245
- const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
3246
- return luminance < 0.5;
3247
- }
3248
- var DEFAULT_BACKGROUND;
3249
- var init_extractFirstPageBackground = __esm({
3250
- "src/domains/paywall/extractFirstPageBackground.ts"() {
3253
+ // src/domains/onboarding/OnboardingManager.ts
3254
+ var OnboardingManager, onboardingManager;
3255
+ var init_OnboardingManager = __esm({
3256
+ "src/domains/onboarding/OnboardingManager.ts"() {
3251
3257
  "use strict";
3252
- DEFAULT_BACKGROUND = "#FFFFFF";
3253
- }
3254
- });
3255
-
3256
- // src/domains/paywall/parseWebViewMessage.ts
3257
- function deriveMessageId(message) {
3258
- if (message.id) return message.id;
3259
- const payloadKey = message.payload?.productId ?? message.payload?.url ?? (message.payload?.timestamp != null ? String(message.payload.timestamp) : "");
3260
- return `${message.type}:${payloadKey}`;
3261
- }
3262
- function parseWebViewMessage(raw) {
3258
+ init_OnboardingError();
3259
+ OnboardingManager = class {
3260
+ constructor() {
3261
+ this.pipeline = null;
3262
+ this.debug = false;
3263
+ this.lastStep = null;
3264
+ this.finished = false;
3265
+ }
3266
+ /**
3267
+ * Injects runtime dependencies. Called by PaywalloClient during init.
3268
+ */
3269
+ injectDeps(config) {
3270
+ this.pipeline = config.pipeline;
3271
+ this.debug = config.debug ?? false;
3272
+ }
3273
+ /**
3274
+ * Tracks an onboarding step view.
3275
+ * Saves the step name internally for implicit drop detection on the backend.
3276
+ *
3277
+ * @param stepName - Unique name for this onboarding step.
3278
+ * @param order - Optional explicit display order (accepts decimals, e.g. 2.1 for A/B variants).
3279
+ */
3280
+ async step(stepName, order) {
3281
+ this.assertConfig();
3282
+ this.validateStepName(stepName, "step");
3283
+ this.lastStep = stepName;
3284
+ const stepPayload = { step_name: stepName };
3285
+ if (order !== void 0 && Number.isFinite(order) && order >= 0) {
3286
+ stepPayload.order = order;
3287
+ }
3288
+ const payload = {
3289
+ family: "onboarding",
3290
+ type: "step",
3291
+ ...stepPayload
3292
+ };
3293
+ return this.emit("onboarding", payload, "step");
3294
+ }
3295
+ /**
3296
+ * Tracks successful completion of the onboarding flow.
3297
+ * Marks the flow as finished.
3298
+ */
3299
+ async complete() {
3300
+ this.assertConfig();
3301
+ this.finished = true;
3302
+ return this.emit("onboarding", { family: "onboarding", type: "complete" }, "complete");
3303
+ }
3304
+ /**
3305
+ * Tracks an explicit drop (abandonment) at the given step.
3306
+ * Marks the flow as finished.
3307
+ *
3308
+ * @param stepName - The step name where the user dropped off.
3309
+ */
3310
+ async drop(stepName) {
3311
+ this.assertConfig();
3312
+ this.validateStepName(stepName, "drop");
3313
+ this.finished = true;
3314
+ const payload = {
3315
+ family: "onboarding",
3316
+ type: "drop",
3317
+ ...{ step_name: stepName }
3318
+ };
3319
+ return this.emit("onboarding", payload, "drop");
3320
+ }
3321
+ /** Returns the last step name seen, or null if no step was tracked yet. */
3322
+ getLastStep() {
3323
+ return this.lastStep;
3324
+ }
3325
+ /** Returns whether the flow has been marked as finished (complete or drop). */
3326
+ isFinished() {
3327
+ return this.finished;
3328
+ }
3329
+ async emit(eventName, properties, logLabel) {
3330
+ const pipeline = this.pipeline;
3331
+ const distinctId = pipeline.getDistinctId();
3332
+ if (!distinctId) {
3333
+ if (this.debug) console.log(`[Paywallo:Onboarding] skipped (no distinctId): ${logLabel}`);
3334
+ return;
3335
+ }
3336
+ await pipeline.trackEvent(eventName, distinctId, properties);
3337
+ if (this.debug) {
3338
+ console.log(`[Paywallo:Onboarding] event emitted: ${eventName}.${logLabel}`, properties);
3339
+ }
3340
+ }
3341
+ assertConfig() {
3342
+ if (!this.pipeline) {
3343
+ throw new OnboardingError(
3344
+ ONBOARDING_ERROR_CODES.NOT_INITIALIZED,
3345
+ "OnboardingManager not initialized. Call Paywallo.init() first."
3346
+ );
3347
+ }
3348
+ }
3349
+ validateStepName(stepName, method) {
3350
+ if (typeof stepName !== "string" || stepName.trim().length === 0) {
3351
+ throw new OnboardingError(
3352
+ ONBOARDING_ERROR_CODES.INVALID_STEP_NAME,
3353
+ `OnboardingManager.${method}(): stepName must be a non-empty string.`
3354
+ );
3355
+ }
3356
+ }
3357
+ };
3358
+ onboardingManager = new OnboardingManager();
3359
+ }
3360
+ });
3361
+
3362
+ // src/domains/onboarding/index.ts
3363
+ var init_onboarding = __esm({
3364
+ "src/domains/onboarding/index.ts"() {
3365
+ "use strict";
3366
+ init_OnboardingError();
3367
+ init_OnboardingManager();
3368
+ }
3369
+ });
3370
+
3371
+ // src/domains/paywall/paywallCloseReason.ts
3372
+ function toCanonicalReason(reason) {
3373
+ if (reason in CLOSE_REASON_MAP) return CLOSE_REASON_MAP[reason];
3374
+ return reason;
3375
+ }
3376
+ var CLOSE_REASON_MAP;
3377
+ var init_paywallCloseReason = __esm({
3378
+ "src/domains/paywall/paywallCloseReason.ts"() {
3379
+ "use strict";
3380
+ CLOSE_REASON_MAP = {
3381
+ dismissed: "dismiss",
3382
+ purchased: "purchase",
3383
+ backgrounded: "dismiss",
3384
+ timeout: "timeout"
3385
+ };
3386
+ }
3387
+ });
3388
+
3389
+ // src/domains/paywall/emitPaywallClosed.ts
3390
+ async function emitPaywallClosed(opts) {
3391
+ try {
3392
+ const api = PaywalloClient.getApiClient();
3393
+ if (!api) return;
3394
+ const sessionId = PaywalloClient.getSessionId();
3395
+ const closedAt = /* @__PURE__ */ new Date();
3396
+ const durationS = Math.max(0, Math.round((closedAt.getTime() - opts.presentedAt) / 1e3));
3397
+ await api.trackEvent("paywall", PaywalloClient.getDistinctId(), {
3398
+ type: "closed",
3399
+ paywall_id: opts.paywallId,
3400
+ placement: opts.placement,
3401
+ closed_at: closedAt.toISOString(),
3402
+ duration_s: durationS,
3403
+ close_reason: toCanonicalReason(opts.reason),
3404
+ ...opts.variantId && { variant_id: opts.variantId },
3405
+ ...opts.variantKey && { variant_key: opts.variantKey },
3406
+ ...opts.campaignId && { campaign_id: opts.campaignId },
3407
+ ...typeof opts.scrollDepth === "number" && { scroll_depth: opts.scrollDepth },
3408
+ ...sessionId && { sessionId }
3409
+ });
3410
+ } catch (err) {
3411
+ if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] emitPaywallClosed failed", err);
3412
+ }
3413
+ }
3414
+ var init_emitPaywallClosed = __esm({
3415
+ "src/domains/paywall/emitPaywallClosed.ts"() {
3416
+ "use strict";
3417
+ init_PaywalloClient();
3418
+ init_paywallCloseReason();
3419
+ }
3420
+ });
3421
+
3422
+ // src/domains/paywall/extractFirstPageBackground.ts
3423
+ function extractFirstPageBackground(craftData) {
3424
+ if (!craftData) return DEFAULT_BACKGROUND;
3425
+ try {
3426
+ const data = JSON.parse(craftData);
3427
+ const nodes = { ...data.nodes, ...data.sharedNodes };
3428
+ const rootId = data.rootId;
3429
+ if (!rootId || !nodes[rootId]) return DEFAULT_BACKGROUND;
3430
+ const firstPageId = nodes[rootId].children?.[0];
3431
+ if (!firstPageId) return DEFAULT_BACKGROUND;
3432
+ const bg = nodes[firstPageId]?.props?.backgroundColor;
3433
+ return typeof bg === "string" && bg.length > 0 && bg !== "transparent" ? bg : DEFAULT_BACKGROUND;
3434
+ } catch {
3435
+ return DEFAULT_BACKGROUND;
3436
+ }
3437
+ }
3438
+ function isColorDark(color) {
3439
+ const hex = color.replace("#", "").trim();
3440
+ const normalized = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex;
3441
+ if (normalized.length !== 6) return false;
3442
+ const r = parseInt(normalized.slice(0, 2), 16);
3443
+ const g = parseInt(normalized.slice(2, 4), 16);
3444
+ const b = parseInt(normalized.slice(4, 6), 16);
3445
+ if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return false;
3446
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
3447
+ return luminance < 0.5;
3448
+ }
3449
+ var DEFAULT_BACKGROUND;
3450
+ var init_extractFirstPageBackground = __esm({
3451
+ "src/domains/paywall/extractFirstPageBackground.ts"() {
3452
+ "use strict";
3453
+ DEFAULT_BACKGROUND = "#FFFFFF";
3454
+ }
3455
+ });
3456
+
3457
+ // src/domains/paywall/parseWebViewMessage.ts
3458
+ function deriveMessageId(message) {
3459
+ if (message.id) return message.id;
3460
+ const payloadKey = message.payload?.productId ?? message.payload?.url ?? (message.payload?.timestamp != null ? String(message.payload.timestamp) : "");
3461
+ return `${message.type}:${payloadKey}`;
3462
+ }
3463
+ function parseWebViewMessage(raw) {
3263
3464
  try {
3264
3465
  const parsed = JSON.parse(raw);
3265
3466
  if (typeof parsed?.type !== "string" || !ALLOWED_MESSAGE_TYPES.has(parsed.type)) {
@@ -3905,17 +4106,292 @@ var init_PaywallWebView = __esm({
3905
4106
  }
3906
4107
  });
3907
4108
 
4109
+ // src/domains/iap/PurchaseError.ts
4110
+ function createPurchaseError(code, message) {
4111
+ return new PurchaseError(
4112
+ PURCHASE_ERROR_CODES[code],
4113
+ message ?? DEFAULT_MESSAGES[code] ?? "Unknown purchase error",
4114
+ code === "USER_CANCELLED"
4115
+ );
4116
+ }
4117
+ var PurchaseError, PURCHASE_ERROR_CODES, DEFAULT_MESSAGES;
4118
+ var init_PurchaseError = __esm({
4119
+ "src/domains/iap/PurchaseError.ts"() {
4120
+ "use strict";
4121
+ init_PaywalloError();
4122
+ PurchaseError = class extends PaywalloError {
4123
+ constructor(code, message, userCancelled = false, httpStatus) {
4124
+ super("purchase", code, message);
4125
+ this.name = "PurchaseError";
4126
+ this.userCancelled = userCancelled;
4127
+ this.httpStatus = httpStatus;
4128
+ }
4129
+ };
4130
+ PURCHASE_ERROR_CODES = {
4131
+ NOT_INITIALIZED: "PURCHASE_NOT_INITIALIZED",
4132
+ PRODUCT_NOT_FOUND: "PRODUCT_NOT_FOUND",
4133
+ PURCHASE_FAILED: "PURCHASE_FAILED",
4134
+ RESTORE_FAILED: "RESTORE_FAILED",
4135
+ VALIDATION_FAILED: "VALIDATION_FAILED",
4136
+ USER_CANCELLED: "USER_CANCELLED",
4137
+ NETWORK_ERROR: "PURCHASE_NETWORK_ERROR",
4138
+ STORE_ERROR: "PURCHASE_STORE_ERROR",
4139
+ PENDING_PURCHASE: "PURCHASE_PENDING",
4140
+ DEFERRED_PURCHASE: "PURCHASE_DEFERRED",
4141
+ STORE_NOT_AVAILABLE: "PURCHASE_STORE_NOT_AVAILABLE"
4142
+ };
4143
+ DEFAULT_MESSAGES = {
4144
+ NOT_INITIALIZED: "Purchase controller not initialized. Call init() first.",
4145
+ PRODUCT_NOT_FOUND: "Product not found in store.",
4146
+ PURCHASE_FAILED: "Purchase failed. Please try again.",
4147
+ RESTORE_FAILED: "Failed to restore purchases. Please try again.",
4148
+ VALIDATION_FAILED: "Failed to validate purchase with server.",
4149
+ USER_CANCELLED: "Purchase was cancelled.",
4150
+ NETWORK_ERROR: "Network error. Please check your connection.",
4151
+ STORE_ERROR: "Store error. Please try again later.",
4152
+ PENDING_PURCHASE: "Purchase is pending approval.",
4153
+ DEFERRED_PURCHASE: "Purchase is deferred (e.g., parental approval required).",
4154
+ STORE_NOT_AVAILABLE: "In-app purchases are not available on this device."
4155
+ };
4156
+ }
4157
+ });
4158
+
4159
+ // src/domains/iap/NativeStoreKit.ts
4160
+ var NativeStoreKit_exports = {};
4161
+ __export(NativeStoreKit_exports, {
4162
+ NativeStoreKit: () => NativeStoreKit,
4163
+ nativeStoreKit: () => nativeStoreKit
4164
+ });
4165
+ import { NativeEventEmitter as NativeEventEmitter3, NativeModules as NativeModules6, Platform as Platform11 } from "react-native";
4166
+ function isAvailable3() {
4167
+ return PaywalloStoreKitNative != null;
4168
+ }
4169
+ function mapNativeProduct(native) {
4170
+ const product = {
4171
+ productId: native.productId,
4172
+ title: native.title,
4173
+ description: native.description,
4174
+ price: native.price,
4175
+ priceValue: native.priceValue,
4176
+ currency: native.currency ?? "USD",
4177
+ localizedPrice: native.localizedPrice,
4178
+ type: mapProductType(native.type),
4179
+ subscriptionPeriod: native.subscriptionPeriod,
4180
+ introductoryPrice: native.introductoryPrice,
4181
+ introductoryPriceValue: native.introductoryPriceValue,
4182
+ freeTrialPeriod: native.freeTrialPeriod
4183
+ };
4184
+ if (native.subscriptionPeriod) {
4185
+ const prices = calculatePerPeriodPrices(native.priceValue, native.subscriptionPeriod);
4186
+ product.pricePerMonth = prices.perMonth;
4187
+ product.pricePerWeek = prices.perWeek;
4188
+ product.pricePerDay = prices.perDay;
4189
+ }
4190
+ return product;
4191
+ }
4192
+ function mapProductType(type) {
4193
+ switch (type) {
4194
+ case "subscription":
4195
+ case "autoRenewable":
4196
+ return "subscription";
4197
+ case "consumable":
4198
+ return "consumable";
4199
+ default:
4200
+ return "non_consumable";
4201
+ }
4202
+ }
4203
+ function calculatePerPeriodPrices(priceValue, period) {
4204
+ const totalDays = periodToDays(period);
4205
+ if (totalDays <= 0) return {};
4206
+ const perDay = priceValue / totalDays;
4207
+ const perWeek = perDay * 7;
4208
+ const perMonth = perDay * 30;
4209
+ return {
4210
+ perDay: perDay.toFixed(2),
4211
+ perWeek: perWeek.toFixed(2),
4212
+ perMonth: perMonth.toFixed(2)
4213
+ };
4214
+ }
4215
+ function periodToDays(period) {
4216
+ const match = period.match(/^P(\d+)([DWMY])$/);
4217
+ if (!match) return 0;
4218
+ const value = parseInt(match[1], 10);
4219
+ switch (match[2]) {
4220
+ case "D":
4221
+ return value;
4222
+ case "W":
4223
+ return value * 7;
4224
+ case "M":
4225
+ return value * 30;
4226
+ case "Y":
4227
+ return value * 365;
4228
+ default:
4229
+ return 0;
4230
+ }
4231
+ }
4232
+ function mapNativePurchase(native) {
4233
+ return {
4234
+ productId: native.productId,
4235
+ transactionId: native.transactionId,
4236
+ transactionDate: native.transactionDate,
4237
+ receipt: native.receipt,
4238
+ platform: Platform11.OS
4239
+ };
4240
+ }
4241
+ function normalizeTransactionUpdate(raw) {
4242
+ if (!raw || typeof raw !== "object") return null;
4243
+ const r = raw;
4244
+ const type = r.type;
4245
+ if (type !== "renewed" && type !== "refunded" && type !== "canceled") return null;
4246
+ const transactionId = typeof r.transactionId === "string" ? r.transactionId : null;
4247
+ const productId = typeof r.productId === "string" ? r.productId : null;
4248
+ if (!transactionId) return null;
4249
+ const update = {
4250
+ type,
4251
+ transactionId,
4252
+ productId: productId ?? ""
4253
+ };
4254
+ if (typeof r.amount === "number") update.amount = r.amount;
4255
+ if (typeof r.currency === "string") update.currency = r.currency;
4256
+ return update;
4257
+ }
4258
+ var TRANSACTION_UPDATE_EVENT, PaywalloStoreKitNative, nativeStoreKit, NativeStoreKit;
4259
+ var init_NativeStoreKit = __esm({
4260
+ "src/domains/iap/NativeStoreKit.ts"() {
4261
+ "use strict";
4262
+ init_PaywalloClient();
4263
+ init_uuid();
4264
+ init_IdentityManager();
4265
+ init_PurchaseError();
4266
+ TRANSACTION_UPDATE_EVENT = "PaywalloTransactionUpdate";
4267
+ PaywalloStoreKitNative = NativeModules6.PaywalloStoreKit ?? null;
4268
+ nativeStoreKit = {
4269
+ isAvailable: isAvailable3,
4270
+ async getProducts(productIds) {
4271
+ if (!PaywalloStoreKitNative) {
4272
+ return [];
4273
+ }
4274
+ const nativeProducts = await PaywalloStoreKitNative.getProducts(productIds);
4275
+ return nativeProducts.map(mapNativeProduct);
4276
+ },
4277
+ async purchase(productId) {
4278
+ if (!PaywalloStoreKitNative) {
4279
+ throw new PurchaseError(
4280
+ PURCHASE_ERROR_CODES.STORE_NOT_AVAILABLE,
4281
+ "StoreKit native module not available"
4282
+ );
4283
+ }
4284
+ const distinctId = identityManager.getDistinctId();
4285
+ const rawUuid = distinctId?.startsWith("$paywallo_anon:") ? distinctId.slice("$paywallo_anon:".length) : distinctId;
4286
+ const appAccountToken = isValidUUIDv4(rawUuid) ? rawUuid : null;
4287
+ if (distinctId && appAccountToken === null) {
4288
+ if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] distinctId is not a valid UUID v4 \u2014 passing null to appAccountToken:", distinctId);
4289
+ }
4290
+ try {
4291
+ const result = await PaywalloStoreKitNative.purchase(productId, appAccountToken);
4292
+ if (!result || result.pending) {
4293
+ return null;
4294
+ }
4295
+ return mapNativePurchase(result);
4296
+ } catch (err) {
4297
+ const message = err instanceof Error ? err.message : String(err);
4298
+ if (message.toLowerCase().includes("cancel")) {
4299
+ throw new PurchaseError(PURCHASE_ERROR_CODES.USER_CANCELLED, message, true);
4300
+ }
4301
+ throw new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, message);
4302
+ }
4303
+ },
4304
+ async finishTransaction(transactionId) {
4305
+ if (!PaywalloStoreKitNative) return;
4306
+ try {
4307
+ await PaywalloStoreKitNative.finishTransaction(transactionId);
4308
+ } catch (err) {
4309
+ console.error("[Paywallo] finishTransaction native error", err);
4310
+ }
4311
+ },
4312
+ async getActiveTransactions() {
4313
+ if (!PaywalloStoreKitNative) return [];
4314
+ try {
4315
+ const results = await PaywalloStoreKitNative.getActiveTransactions();
4316
+ return results.map(mapNativePurchase);
4317
+ } catch (err) {
4318
+ console.error("[Paywallo] getActiveTransactions failed", err);
4319
+ return [];
4320
+ }
4321
+ },
4322
+ /**
4323
+ * Subscribes to native transaction update events (renewals, refunds,
4324
+ * cancellations) from StoreKit 2 (`Transaction.updates`) on iOS or
4325
+ * `PurchasesUpdatedListener` + refund diff on Android.
4326
+ *
4327
+ * Returns a subscription with a `remove()` method. The caller is
4328
+ * responsible for cleanup (typically on `Paywallo.reset()`).
4329
+ *
4330
+ * Returns `null` when the native module isn't available (non-mobile env,
4331
+ * native module not linked) — callers should treat null as a no-op.
4332
+ */
4333
+ subscribeToTransactionUpdates(listener) {
4334
+ if (!PaywalloStoreKitNative) return null;
4335
+ try {
4336
+ const EmitterCtor = NativeEventEmitter3;
4337
+ const emitter = new EmitterCtor(PaywalloStoreKitNative);
4338
+ const sub = emitter.addListener(TRANSACTION_UPDATE_EVENT, (payload) => {
4339
+ const update = normalizeTransactionUpdate(payload);
4340
+ if (update) listener(update);
4341
+ });
4342
+ return { remove: () => sub.remove() };
4343
+ } catch (err) {
4344
+ if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] subscribeToTransactionUpdates failed", err);
4345
+ return null;
4346
+ }
4347
+ }
4348
+ };
4349
+ NativeStoreKit = nativeStoreKit;
4350
+ }
4351
+ });
4352
+
3908
4353
  // src/domains/iap/IAPTransactionEmitter.ts
3909
4354
  var IAPTransactionEmitter;
3910
4355
  var init_IAPTransactionEmitter = __esm({
3911
4356
  "src/domains/iap/IAPTransactionEmitter.ts"() {
3912
4357
  "use strict";
3913
4358
  init_PaywalloClient();
4359
+ init_NativeStoreKit();
3914
4360
  IAPTransactionEmitter = class {
3915
4361
  constructor(getProductFromCache, getApiClientFn, getDebug) {
3916
4362
  this.getProductFromCache = getProductFromCache;
3917
4363
  this.getApiClientFn = getApiClientFn;
3918
4364
  this.getDebug = getDebug;
4365
+ this.transactionUpdateSubscription = null;
4366
+ }
4367
+ /**
4368
+ * Emits `transaction {type: "completed"}` after a server-validated purchase.
4369
+ * Fire-and-forget — a tracking failure must never poison the purchase flow.
4370
+ */
4371
+ async emitTransactionCompleted(purchase, productId, options) {
4372
+ try {
4373
+ const apiClient = this.getApiClientFn();
4374
+ if (!apiClient) return;
4375
+ const distinctId = PaywalloClient.getDistinctId();
4376
+ if (!distinctId) return;
4377
+ const product = this.getProductFromCache(productId);
4378
+ const properties = {
4379
+ type: "completed",
4380
+ tx_id: purchase.transactionId,
4381
+ product_id: purchase.productId
4382
+ };
4383
+ if (product?.priceValue !== void 0 && product.priceValue > 0) {
4384
+ properties.amount = product.priceValue;
4385
+ }
4386
+ if (product?.currency) {
4387
+ properties.currency = product.currency;
4388
+ }
4389
+ if (options?.paywallId) properties.paywall_id = options.paywallId;
4390
+ if (options?.variantId) properties.variant_id = options.variantId;
4391
+ await apiClient.trackEvent("transaction", distinctId, properties, void 0, { priority: "critical" });
4392
+ } catch (err) {
4393
+ if (this.getDebug()) console.log("[Paywallo PURCHASE] transaction event emission failed", err);
4394
+ }
3919
4395
  }
3920
4396
  /**
3921
4397
  * Emits `checkout_started` quando o usuário inicia a compra (toca em comprar),
@@ -3940,19 +4416,69 @@ var init_IAPTransactionEmitter = __esm({
3940
4416
  });
3941
4417
  if (this.getDebug()) console.log("[Paywallo PURCHASE] emitted checkout_started", productId);
3942
4418
  } catch (err) {
3943
- if (this.getDebug()) console.log("[Paywallo PURCHASE] checkout_started emission failed", err);
4419
+ if (this.getDebug()) console.log("[Paywallo PURCHASE] checkout_started emission failed", err);
4420
+ }
4421
+ }
4422
+ /**
4423
+ * Forwards a native transaction update to the EventBatcher.
4424
+ * Public so tests can exercise the wiring without a fake NativeEventEmitter.
4425
+ */
4426
+ async emitTransactionUpdate(update) {
4427
+ try {
4428
+ const apiClient = this.getApiClientFn() ?? PaywalloClient.getApiClient();
4429
+ if (!apiClient) {
4430
+ if (this.getDebug()) console.log("[Paywallo IAP] transaction update dropped (no apiClient)", update.type);
4431
+ return;
4432
+ }
4433
+ const distinctId = PaywalloClient.getDistinctId();
4434
+ if (!distinctId) {
4435
+ if (this.getDebug()) console.log("[Paywallo IAP] transaction update dropped (no distinctId)", update.type);
4436
+ return;
4437
+ }
4438
+ const properties = {
4439
+ type: update.type,
4440
+ tx_id: update.transactionId,
4441
+ product_id: update.productId
4442
+ };
4443
+ if (update.type !== "canceled") {
4444
+ const cached2 = this.getProductFromCache(update.productId);
4445
+ const amount = update.amount ?? cached2?.priceValue;
4446
+ const currency = update.currency ?? cached2?.currency;
4447
+ if (typeof amount === "number" && amount > 0) properties.amount = amount;
4448
+ if (currency) properties.currency = currency;
4449
+ }
4450
+ await apiClient.trackEvent("transaction", distinctId, properties, void 0, { priority: "critical" });
4451
+ if (this.getDebug()) console.log(`[Paywallo IAP] emitted transaction {${update.type}}`, update.transactionId);
4452
+ } catch (err) {
4453
+ if (this.getDebug()) console.log("[Paywallo IAP] transaction update emission failed", err);
3944
4454
  }
3945
4455
  }
3946
4456
  /**
3947
- * No-op. Native transaction update listener removed — sale events are sourced
3948
- * exclusively from server-side webhooks (Apple/Google/Superwall/RevenueCat).
4457
+ * Starts listening to native Transaction.updates. Idempotent.
3949
4458
  */
3950
4459
  startListener() {
4460
+ if (this.transactionUpdateSubscription) return;
4461
+ this.transactionUpdateSubscription = nativeStoreKit.subscribeToTransactionUpdates(
4462
+ (update) => {
4463
+ void this.emitTransactionUpdate(update);
4464
+ }
4465
+ );
4466
+ if (this.getDebug() && this.transactionUpdateSubscription) {
4467
+ console.log("[Paywallo IAP] transaction update listener started");
4468
+ }
3951
4469
  }
3952
4470
  /**
3953
- * No-op. See startListener.
4471
+ * Removes the native listener. Safe to call multiple times.
3954
4472
  */
3955
4473
  stopListener() {
4474
+ if (!this.transactionUpdateSubscription) return;
4475
+ try {
4476
+ this.transactionUpdateSubscription.remove();
4477
+ } catch (err) {
4478
+ if (this.getDebug()) console.log("[Paywallo IAP] transaction listener removal failed", err);
4479
+ }
4480
+ this.transactionUpdateSubscription = null;
4481
+ if (this.getDebug()) console.log("[Paywallo IAP] transaction update listener stopped");
3956
4482
  }
3957
4483
  };
3958
4484
  }
@@ -4841,58 +5367,8 @@ var init_queue = __esm({
4841
5367
  }
4842
5368
  });
4843
5369
 
4844
- // src/domains/iap/PurchaseError.ts
4845
- function createPurchaseError(code, message) {
4846
- return new PurchaseError(
4847
- PURCHASE_ERROR_CODES[code],
4848
- message ?? DEFAULT_MESSAGES[code] ?? "Unknown purchase error",
4849
- code === "USER_CANCELLED"
4850
- );
4851
- }
4852
- var PurchaseError, PURCHASE_ERROR_CODES, DEFAULT_MESSAGES;
4853
- var init_PurchaseError = __esm({
4854
- "src/domains/iap/PurchaseError.ts"() {
4855
- "use strict";
4856
- init_PaywalloError();
4857
- PurchaseError = class extends PaywalloError {
4858
- constructor(code, message, userCancelled = false, httpStatus) {
4859
- super("purchase", code, message);
4860
- this.name = "PurchaseError";
4861
- this.userCancelled = userCancelled;
4862
- this.httpStatus = httpStatus;
4863
- }
4864
- };
4865
- PURCHASE_ERROR_CODES = {
4866
- NOT_INITIALIZED: "PURCHASE_NOT_INITIALIZED",
4867
- PRODUCT_NOT_FOUND: "PRODUCT_NOT_FOUND",
4868
- PURCHASE_FAILED: "PURCHASE_FAILED",
4869
- RESTORE_FAILED: "RESTORE_FAILED",
4870
- VALIDATION_FAILED: "VALIDATION_FAILED",
4871
- USER_CANCELLED: "USER_CANCELLED",
4872
- NETWORK_ERROR: "PURCHASE_NETWORK_ERROR",
4873
- STORE_ERROR: "PURCHASE_STORE_ERROR",
4874
- PENDING_PURCHASE: "PURCHASE_PENDING",
4875
- DEFERRED_PURCHASE: "PURCHASE_DEFERRED",
4876
- STORE_NOT_AVAILABLE: "PURCHASE_STORE_NOT_AVAILABLE"
4877
- };
4878
- DEFAULT_MESSAGES = {
4879
- NOT_INITIALIZED: "Purchase controller not initialized. Call init() first.",
4880
- PRODUCT_NOT_FOUND: "Product not found in store.",
4881
- PURCHASE_FAILED: "Purchase failed. Please try again.",
4882
- RESTORE_FAILED: "Failed to restore purchases. Please try again.",
4883
- VALIDATION_FAILED: "Failed to validate purchase with server.",
4884
- USER_CANCELLED: "Purchase was cancelled.",
4885
- NETWORK_ERROR: "Network error. Please check your connection.",
4886
- STORE_ERROR: "Store error. Please try again later.",
4887
- PENDING_PURCHASE: "Purchase is pending approval.",
4888
- DEFERRED_PURCHASE: "Purchase is deferred (e.g., parental approval required).",
4889
- STORE_NOT_AVAILABLE: "In-app purchases are not available on this device."
4890
- };
4891
- }
4892
- });
4893
-
4894
5370
  // src/domains/iap/IAPValidator.ts
4895
- import { Platform as Platform11 } from "react-native";
5371
+ import { Platform as Platform12 } from "react-native";
4896
5372
  var IAPValidator;
4897
5373
  var init_IAPValidator = __esm({
4898
5374
  "src/domains/iap/IAPValidator.ts"() {
@@ -4922,7 +5398,7 @@ var init_IAPValidator = __esm({
4922
5398
  async validateWithServer(purchase, productId, options) {
4923
5399
  const apiClient = this.getApiClientFn();
4924
5400
  const product = this.getProductFromCache(productId);
4925
- const platform = Platform11.OS;
5401
+ const platform = Platform12.OS;
4926
5402
  const distinctId = PaywalloClient.getDistinctId();
4927
5403
  return this.validateWithRetry(
4928
5404
  () => apiClient.validatePurchase(
@@ -4957,7 +5433,7 @@ var init_IAPValidator = __esm({
4957
5433
  try {
4958
5434
  const apiClient = this.getApiClientFn();
4959
5435
  const product = this.getProductFromCache(productId);
4960
- const platform = Platform11.OS;
5436
+ const platform = Platform12.OS;
4961
5437
  const distinctId = PaywalloClient.getDistinctId();
4962
5438
  const appKey = apiClient.getAppKey();
4963
5439
  const body = {
@@ -4976,210 +5452,16 @@ var init_IAPValidator = __esm({
4976
5452
  "POST",
4977
5453
  "/sdk/purchases/validate",
4978
5454
  body,
4979
- { "X-App-Key": appKey },
4980
- `validate:${purchase.transactionId}`,
4981
- "critical"
4982
- );
4983
- if (this.getDebug()) console.log("[Paywallo PURCHASE] validation retry enqueued", purchase.transactionId);
4984
- } catch (err) {
4985
- console.error("[Paywallo PURCHASE] failed to enqueue validation retry", err);
4986
- }
4987
- }
4988
- };
4989
- }
4990
- });
4991
-
4992
- // src/domains/iap/NativeStoreKit.ts
4993
- var NativeStoreKit_exports = {};
4994
- __export(NativeStoreKit_exports, {
4995
- NativeStoreKit: () => NativeStoreKit,
4996
- nativeStoreKit: () => nativeStoreKit
4997
- });
4998
- import { NativeEventEmitter as NativeEventEmitter3, NativeModules as NativeModules6, Platform as Platform12 } from "react-native";
4999
- function isAvailable3() {
5000
- return PaywalloStoreKitNative != null;
5001
- }
5002
- function mapNativeProduct(native) {
5003
- const product = {
5004
- productId: native.productId,
5005
- title: native.title,
5006
- description: native.description,
5007
- price: native.price,
5008
- priceValue: native.priceValue,
5009
- currency: native.currency ?? "USD",
5010
- localizedPrice: native.localizedPrice,
5011
- type: mapProductType(native.type),
5012
- subscriptionPeriod: native.subscriptionPeriod,
5013
- introductoryPrice: native.introductoryPrice,
5014
- introductoryPriceValue: native.introductoryPriceValue,
5015
- freeTrialPeriod: native.freeTrialPeriod
5016
- };
5017
- if (native.subscriptionPeriod) {
5018
- const prices = calculatePerPeriodPrices(native.priceValue, native.subscriptionPeriod);
5019
- product.pricePerMonth = prices.perMonth;
5020
- product.pricePerWeek = prices.perWeek;
5021
- product.pricePerDay = prices.perDay;
5022
- }
5023
- return product;
5024
- }
5025
- function mapProductType(type) {
5026
- switch (type) {
5027
- case "subscription":
5028
- case "autoRenewable":
5029
- return "subscription";
5030
- case "consumable":
5031
- return "consumable";
5032
- default:
5033
- return "non_consumable";
5034
- }
5035
- }
5036
- function calculatePerPeriodPrices(priceValue, period) {
5037
- const totalDays = periodToDays(period);
5038
- if (totalDays <= 0) return {};
5039
- const perDay = priceValue / totalDays;
5040
- const perWeek = perDay * 7;
5041
- const perMonth = perDay * 30;
5042
- return {
5043
- perDay: perDay.toFixed(2),
5044
- perWeek: perWeek.toFixed(2),
5045
- perMonth: perMonth.toFixed(2)
5046
- };
5047
- }
5048
- function periodToDays(period) {
5049
- const match = period.match(/^P(\d+)([DWMY])$/);
5050
- if (!match) return 0;
5051
- const value = parseInt(match[1], 10);
5052
- switch (match[2]) {
5053
- case "D":
5054
- return value;
5055
- case "W":
5056
- return value * 7;
5057
- case "M":
5058
- return value * 30;
5059
- case "Y":
5060
- return value * 365;
5061
- default:
5062
- return 0;
5063
- }
5064
- }
5065
- function mapNativePurchase(native) {
5066
- return {
5067
- productId: native.productId,
5068
- transactionId: native.transactionId,
5069
- transactionDate: native.transactionDate,
5070
- receipt: native.receipt,
5071
- platform: Platform12.OS
5072
- };
5073
- }
5074
- function normalizeTransactionUpdate(raw) {
5075
- if (!raw || typeof raw !== "object") return null;
5076
- const r = raw;
5077
- const type = r.type;
5078
- if (type !== "renewed" && type !== "refunded" && type !== "canceled") return null;
5079
- const transactionId = typeof r.transactionId === "string" ? r.transactionId : null;
5080
- const productId = typeof r.productId === "string" ? r.productId : null;
5081
- if (!transactionId) return null;
5082
- const update = {
5083
- type,
5084
- transactionId,
5085
- productId: productId ?? ""
5086
- };
5087
- if (typeof r.amount === "number") update.amount = r.amount;
5088
- if (typeof r.currency === "string") update.currency = r.currency;
5089
- return update;
5090
- }
5091
- var TRANSACTION_UPDATE_EVENT, PaywalloStoreKitNative, nativeStoreKit, NativeStoreKit;
5092
- var init_NativeStoreKit = __esm({
5093
- "src/domains/iap/NativeStoreKit.ts"() {
5094
- "use strict";
5095
- init_PaywalloClient();
5096
- init_uuid();
5097
- init_IdentityManager();
5098
- init_PurchaseError();
5099
- TRANSACTION_UPDATE_EVENT = "PaywalloTransactionUpdate";
5100
- PaywalloStoreKitNative = NativeModules6.PaywalloStoreKit ?? null;
5101
- nativeStoreKit = {
5102
- isAvailable: isAvailable3,
5103
- async getProducts(productIds) {
5104
- if (!PaywalloStoreKitNative) {
5105
- return [];
5106
- }
5107
- const nativeProducts = await PaywalloStoreKitNative.getProducts(productIds);
5108
- return nativeProducts.map(mapNativeProduct);
5109
- },
5110
- async purchase(productId) {
5111
- if (!PaywalloStoreKitNative) {
5112
- throw new PurchaseError(
5113
- PURCHASE_ERROR_CODES.STORE_NOT_AVAILABLE,
5114
- "StoreKit native module not available"
5115
- );
5116
- }
5117
- const distinctId = identityManager.getDistinctId();
5118
- const rawUuid = distinctId?.startsWith("$paywallo_anon:") ? distinctId.slice("$paywallo_anon:".length) : distinctId;
5119
- const appAccountToken = isValidUUIDv4(rawUuid) ? rawUuid : null;
5120
- if (distinctId && appAccountToken === null) {
5121
- if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] distinctId is not a valid UUID v4 \u2014 passing null to appAccountToken:", distinctId);
5122
- }
5123
- try {
5124
- const result = await PaywalloStoreKitNative.purchase(productId, appAccountToken);
5125
- if (!result || result.pending) {
5126
- return null;
5127
- }
5128
- return mapNativePurchase(result);
5129
- } catch (err) {
5130
- const message = err instanceof Error ? err.message : String(err);
5131
- if (message.toLowerCase().includes("cancel")) {
5132
- throw new PurchaseError(PURCHASE_ERROR_CODES.USER_CANCELLED, message, true);
5133
- }
5134
- throw new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, message);
5135
- }
5136
- },
5137
- async finishTransaction(transactionId) {
5138
- if (!PaywalloStoreKitNative) return;
5139
- try {
5140
- await PaywalloStoreKitNative.finishTransaction(transactionId);
5141
- } catch (err) {
5142
- console.error("[Paywallo] finishTransaction native error", err);
5143
- }
5144
- },
5145
- async getActiveTransactions() {
5146
- if (!PaywalloStoreKitNative) return [];
5147
- try {
5148
- const results = await PaywalloStoreKitNative.getActiveTransactions();
5149
- return results.map(mapNativePurchase);
5150
- } catch (err) {
5151
- console.error("[Paywallo] getActiveTransactions failed", err);
5152
- return [];
5153
- }
5154
- },
5155
- /**
5156
- * Subscribes to native transaction update events (renewals, refunds,
5157
- * cancellations) from StoreKit 2 (`Transaction.updates`) on iOS or
5158
- * `PurchasesUpdatedListener` + refund diff on Android.
5159
- *
5160
- * Returns a subscription with a `remove()` method. The caller is
5161
- * responsible for cleanup (typically on `Paywallo.reset()`).
5162
- *
5163
- * Returns `null` when the native module isn't available (non-mobile env,
5164
- * native module not linked) — callers should treat null as a no-op.
5165
- */
5166
- subscribeToTransactionUpdates(listener) {
5167
- if (!PaywalloStoreKitNative) return null;
5168
- try {
5169
- const EmitterCtor = NativeEventEmitter3;
5170
- const emitter = new EmitterCtor(PaywalloStoreKitNative);
5171
- const sub = emitter.addListener(TRANSACTION_UPDATE_EVENT, (payload) => {
5172
- const update = normalizeTransactionUpdate(payload);
5173
- if (update) listener(update);
5174
- });
5175
- return { remove: () => sub.remove() };
5455
+ { "X-App-Key": appKey },
5456
+ `validate:${purchase.transactionId}`,
5457
+ "critical"
5458
+ );
5459
+ if (this.getDebug()) console.log("[Paywallo PURCHASE] validation retry enqueued", purchase.transactionId);
5176
5460
  } catch (err) {
5177
- if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] subscribeToTransactionUpdates failed", err);
5178
- return null;
5461
+ console.error("[Paywallo PURCHASE] failed to enqueue validation retry", err);
5179
5462
  }
5180
5463
  }
5181
5464
  };
5182
- NativeStoreKit = nativeStoreKit;
5183
5465
  }
5184
5466
  });
5185
5467
 
@@ -5283,8 +5565,8 @@ var init_IAPService = __esm({
5283
5565
  stopTransactionListener() {
5284
5566
  this.emitter.stopListener();
5285
5567
  }
5286
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
5287
- async emitTransactionUpdate(_update) {
5568
+ async emitTransactionUpdate(update) {
5569
+ return this.emitter.emitTransactionUpdate(update);
5288
5570
  }
5289
5571
  async purchase(productId, options) {
5290
5572
  if (!nativeStoreKit.isAvailable()) {
@@ -5313,6 +5595,10 @@ var init_IAPService = __esm({
5313
5595
  if (this.debug) console.log(`[Paywallo PURCHASE] ${productId} server validated`);
5314
5596
  await this.finishTransaction(purchase.transactionId);
5315
5597
  if (this.debug) console.log(`[Paywallo PURCHASE] ${productId} finished`);
5598
+ await this.emitter.emitTransactionCompleted(purchase, productId, {
5599
+ paywallId: options?.paywallId,
5600
+ variantId: options?.variantId
5601
+ });
5316
5602
  }).catch(async (err) => {
5317
5603
  const kind = this.validator.classifyValidationError(err);
5318
5604
  if (kind === "server4xx") {
@@ -6413,143 +6699,6 @@ var init_session = __esm({
6413
6699
  }
6414
6700
  });
6415
6701
 
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");
6502
- }
6503
- /** Returns the last step name seen, or null if no step was tracked yet. */
6504
- getLastStep() {
6505
- return this.lastStep;
6506
- }
6507
- /** Returns whether the flow has been marked as finished (complete or drop). */
6508
- isFinished() {
6509
- return this.finished;
6510
- }
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);
6521
- }
6522
- }
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
- );
6529
- }
6530
- }
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
- );
6537
- }
6538
- }
6539
- };
6540
- onboardingManager = new OnboardingManager();
6541
- }
6542
- });
6543
-
6544
- // src/domains/onboarding/index.ts
6545
- var init_onboarding = __esm({
6546
- "src/domains/onboarding/index.ts"() {
6547
- "use strict";
6548
- init_OnboardingError();
6549
- init_OnboardingManager();
6550
- }
6551
- });
6552
-
6553
6702
  // src/domains/analytics/AnalyticsError.ts
6554
6703
  var AnalyticsError, ANALYTICS_ERROR_CODES;
6555
6704
  var init_AnalyticsError = __esm({
@@ -6622,7 +6771,21 @@ var init_schemas = __esm({
6622
6771
  // `closed`-specific
6623
6772
  closed_at: z.string().optional(),
6624
6773
  duration_s: z.number().nonnegative().optional(),
6625
- close_reason: 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: z.enum([
6780
+ "dismiss",
6781
+ "cta",
6782
+ "purchase",
6783
+ "error",
6784
+ "timeout",
6785
+ "purchased",
6786
+ "dismissed",
6787
+ "restored"
6788
+ ]).optional(),
6626
6789
  scroll_depth: z.number().min(0).max(1).optional(),
6627
6790
  // `purchased`-specific (mínimo — transaction canônica leva detalhes)
6628
6791
  product_id: z.string().optional()
@@ -6630,21 +6793,33 @@ var init_schemas = __esm({
6630
6793
  transactionEventSchema = z.object({
6631
6794
  type: 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: 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: z.string().min(1).optional(),
6808
+ tx_id: z.string().min(1).optional(),
6640
6809
  amount: 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: z.number().nonnegative().optional(),
6641
6813
  currency: z.string().length(3).optional(),
6642
6814
  product_id: z.string().optional(),
6643
6815
  subscription_id: z.string().optional(),
6644
6816
  paywall_id: z.string().optional(),
6645
6817
  variant_id: z.string().optional(),
6646
6818
  reason: 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 = z.object({
6649
6824
  type: z.enum(["step", "complete", "drop"]),
6650
6825
  step_index: 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;
@@ -8904,11 +9084,11 @@ var init_PaywalloClient = __esm({
8904
9084
  init_campaign();
8905
9085
  init_identity();
8906
9086
  init_notifications();
9087
+ init_onboarding();
8907
9088
  init_paywall();
8908
9089
  init_plan();
8909
9090
  init_session();
8910
9091
  init_ClientError();
8911
- init_onboarding();
8912
9092
  init_network();
8913
9093
  init_PaywalloAnalytics();
8914
9094
  init_PaywalloFlags();
@@ -9536,8 +9716,41 @@ function usePlanPurchase() {
9536
9716
  };
9537
9717
  }
9538
9718
 
9719
+ // src/hooks/usePaywalloScreenTracking.ts
9720
+ init_PaywalloClient();
9721
+ import { useCallback as useCallback11, useRef as useRef8 } from "react";
9722
+ function isNavState(value) {
9723
+ return typeof value === "object" && value !== null && "routes" in value && "index" in value && Array.isArray(value.routes) && typeof value.index === "number";
9724
+ }
9725
+ function getActiveRouteName(state) {
9726
+ try {
9727
+ if (!isNavState(state)) return null;
9728
+ const route = state.routes[state.index];
9729
+ if (!route) return null;
9730
+ if (isNavState(route.state)) {
9731
+ return getActiveRouteName(route.state);
9732
+ }
9733
+ return typeof route.name === "string" ? route.name : null;
9734
+ } catch {
9735
+ return null;
9736
+ }
9737
+ }
9738
+ function usePaywalloScreenTracking() {
9739
+ const lastScreenRef = useRef8(null);
9740
+ return useCallback11((state) => {
9741
+ if (!PaywalloClient.isReady()) return;
9742
+ const screenName = getActiveRouteName(state);
9743
+ if (!screenName || screenName === lastScreenRef.current) return;
9744
+ lastScreenRef.current = screenName;
9745
+ void PaywalloClient.track("screen_view", {
9746
+ properties: { screen_name: screenName }
9747
+ }).catch(() => {
9748
+ });
9749
+ }, []);
9750
+ }
9751
+
9539
9752
  // src/hooks/usePlans.ts
9540
- import { useCallback as useCallback11, useEffect as useEffect10, useState as useState10 } from "react";
9753
+ import { useCallback as useCallback12, useEffect as useEffect10, useState as useState10 } from "react";
9541
9754
  async function getPlanService() {
9542
9755
  try {
9543
9756
  const mod = await Promise.resolve().then(() => (init_PlanService(), PlanService_exports));
@@ -9551,7 +9764,7 @@ function usePlans() {
9551
9764
  const [currentPlan, setCurrentPlan] = useState10(null);
9552
9765
  const [isLoading, setIsLoading] = useState10(true);
9553
9766
  const [error, setError] = useState10(null);
9554
- const fetchPlans = useCallback11(async (forceRefresh = false) => {
9767
+ const fetchPlans = useCallback12(async (forceRefresh = false) => {
9555
9768
  setIsLoading(true);
9556
9769
  setError(null);
9557
9770
  try {
@@ -9572,7 +9785,7 @@ function usePlans() {
9572
9785
  setIsLoading(false);
9573
9786
  }
9574
9787
  }, []);
9575
- const refresh = useCallback11(() => {
9788
+ const refresh = useCallback12(() => {
9576
9789
  return fetchPlans(true);
9577
9790
  }, [fetchPlans]);
9578
9791
  useEffect10(() => {
@@ -9582,7 +9795,7 @@ function usePlans() {
9582
9795
  }
9583
9796
 
9584
9797
  // src/hooks/useProducts.ts
9585
- import { useCallback as useCallback12, useEffect as useEffect11, useState as useState11 } from "react";
9798
+ import { useCallback as useCallback13, useEffect as useEffect11, useState as useState11 } from "react";
9586
9799
 
9587
9800
  // src/utils/productMappers.ts
9588
9801
  function mapToIAPProduct(product) {
@@ -9625,7 +9838,7 @@ function useProducts(initialProductIds) {
9625
9838
  const [formattedProducts, setFormattedProducts] = useState11([]);
9626
9839
  const [isLoading, setIsLoading] = useState11(false);
9627
9840
  const [error, setError] = useState11(null);
9628
- const loadProducts = useCallback12(async (productIds) => {
9841
+ const loadProducts = useCallback13(async (productIds) => {
9629
9842
  setIsLoading(true);
9630
9843
  setError(null);
9631
9844
  try {
@@ -9644,19 +9857,19 @@ function useProducts(initialProductIds) {
9644
9857
  void loadProducts(initialProductIds);
9645
9858
  }
9646
9859
  }, []);
9647
- const getProduct = useCallback12(
9860
+ const getProduct = useCallback13(
9648
9861
  (productId) => {
9649
9862
  return products.find((p) => p.productId === productId);
9650
9863
  },
9651
9864
  [products]
9652
9865
  );
9653
- const getFormattedProduct = useCallback12(
9866
+ const getFormattedProduct = useCallback13(
9654
9867
  (productId) => {
9655
9868
  return formattedProducts.find((p) => p.productId === productId);
9656
9869
  },
9657
9870
  [formattedProducts]
9658
9871
  );
9659
- const getPriceInfo = useCallback12(
9872
+ const getPriceInfo = useCallback13(
9660
9873
  (productId) => {
9661
9874
  const p = formattedProducts.find((fp) => fp.productId === productId);
9662
9875
  if (!p) return void 0;
@@ -9685,14 +9898,14 @@ function useProducts(initialProductIds) {
9685
9898
  }
9686
9899
 
9687
9900
  // src/hooks/usePurchase.ts
9688
- import { useCallback as useCallback13, useState as useState12 } from "react";
9901
+ import { useCallback as useCallback14, useState as useState12 } from "react";
9689
9902
  function toIAPPurchase(p) {
9690
9903
  return { productId: p.productId, transactionId: p.transactionId, transactionDate: p.transactionDate, transactionReceipt: p.receipt };
9691
9904
  }
9692
9905
  function usePurchase() {
9693
9906
  const [state, setState] = useState12("idle");
9694
9907
  const [error, setError] = useState12(null);
9695
- const purchase = useCallback13(async (productId) => {
9908
+ const purchase = useCallback14(async (productId) => {
9696
9909
  setState("purchasing");
9697
9910
  setError(null);
9698
9911
  try {
@@ -9720,7 +9933,7 @@ function usePurchase() {
9720
9933
  return { status: "failed", error: purchaseError };
9721
9934
  }
9722
9935
  }, []);
9723
- const restore = useCallback13(async () => {
9936
+ const restore = useCallback14(async () => {
9724
9937
  setState("restoring");
9725
9938
  setError(null);
9726
9939
  try {
@@ -9739,14 +9952,14 @@ function usePurchase() {
9739
9952
  }
9740
9953
 
9741
9954
  // src/hooks/useSubscription.ts
9742
- import { useCallback as useCallback14, useEffect as useEffect12, useState as useState13 } from "react";
9955
+ import { useCallback as useCallback15, useEffect as useEffect12, useState as useState13 } from "react";
9743
9956
  init_SubscriptionManager();
9744
9957
  var EMPTY_ENTITLEMENTS = [];
9745
9958
  function useSubscription() {
9746
9959
  const [status, setStatus] = useState13(null);
9747
9960
  const [isLoading, setIsLoading] = useState13(true);
9748
9961
  const [error, setError] = useState13(null);
9749
- const refresh = useCallback14(async () => {
9962
+ const refresh = useCallback15(async () => {
9750
9963
  setIsLoading(true);
9751
9964
  setError(null);
9752
9965
  try {
@@ -9759,7 +9972,7 @@ function useSubscription() {
9759
9972
  setIsLoading(false);
9760
9973
  }
9761
9974
  }, []);
9762
- const restore = useCallback14(async () => {
9975
+ const restore = useCallback15(async () => {
9763
9976
  setIsLoading(true);
9764
9977
  setError(null);
9765
9978
  try {
@@ -9799,39 +10012,6 @@ function useSubscription() {
9799
10012
  };
9800
10013
  }
9801
10014
 
9802
- // src/hooks/usePaywalloScreenTracking.ts
9803
- init_PaywalloClient();
9804
- import { useCallback as useCallback15, useRef as useRef8 } from "react";
9805
- function isNavState(value) {
9806
- return typeof value === "object" && value !== null && "routes" in value && "index" in value && Array.isArray(value.routes) && typeof value.index === "number";
9807
- }
9808
- function getActiveRouteName(state) {
9809
- try {
9810
- if (!isNavState(state)) return null;
9811
- const route = state.routes[state.index];
9812
- if (!route) return null;
9813
- if (isNavState(route.state)) {
9814
- return getActiveRouteName(route.state);
9815
- }
9816
- return typeof route.name === "string" ? route.name : null;
9817
- } catch {
9818
- return null;
9819
- }
9820
- }
9821
- function usePaywalloScreenTracking() {
9822
- const lastScreenRef = useRef8(null);
9823
- return useCallback15((state) => {
9824
- if (!PaywalloClient.isReady()) return;
9825
- const screenName = getActiveRouteName(state);
9826
- if (!screenName || screenName === lastScreenRef.current) return;
9827
- lastScreenRef.current = screenName;
9828
- void PaywalloClient.track("screen_view", {
9829
- properties: { screen_name: screenName }
9830
- }).catch(() => {
9831
- });
9832
- }, []);
9833
- }
9834
-
9835
10015
  // src/PaywalloProvider.tsx
9836
10016
  import { useCallback as useCallback20, useEffect as useEffect14, useLayoutEffect, useMemo as useMemo3, useRef as useRef12, useState as useState17 } from "react";
9837
10017
  import { Animated as Animated4, Dimensions as Dimensions5, Easing as Easing2, StyleSheet as StyleSheet5 } from "react-native";
@@ -9839,6 +10019,7 @@ init_PaywalloClient();
9839
10019
  init_NativeStorage();
9840
10020
  init_SecureStorage();
9841
10021
  init_campaign();
10022
+ init_identity();
9842
10023
  init_paywall();
9843
10024
  init_paywall();
9844
10025
  init_paywall();
@@ -9850,7 +10031,9 @@ init_paywall();
9850
10031
  init_identity();
9851
10032
  var CONFIG_POLL_INTERVAL_MS = 250;
9852
10033
  var CONFIG_POLL_MAX_ATTEMPTS = 40;
10034
+ var CONFIG_RETRY_DELAYS_MS = [3e4, 6e4, 12e4];
9853
10035
  var lastSignature = null;
10036
+ var retryTimer = null;
9854
10037
  function log(debug, msg, data) {
9855
10038
  if (!debug) return;
9856
10039
  if (data !== void 0) console.log(`[Paywallo:SuperwallAttr] ${msg}`, data);
@@ -9917,28 +10100,37 @@ async function waitForConfigured(mod, debug) {
9917
10100
  status = String(await mod.getConfigurationStatus());
9918
10101
  } catch (err) {
9919
10102
  log(debug, "getConfigurationStatus error", { err: String(err) });
9920
- return false;
10103
+ return "failed";
9921
10104
  }
9922
- if (status === "CONFIGURED") return true;
10105
+ if (status === "CONFIGURED") return "configured";
9923
10106
  if (status === "FAILED") {
9924
10107
  log(debug, "Superwall configuration FAILED \u2014 skipping attribute push");
9925
- return false;
10108
+ return "failed";
9926
10109
  }
9927
10110
  await delay(CONFIG_POLL_INTERVAL_MS);
9928
10111
  }
9929
10112
  log(debug, "Superwall not configured within timeout \u2014 skipping attribute push");
9930
- return false;
10113
+ return "timeout";
9931
10114
  }
9932
10115
  async function pushAttributionToSuperwall(debug = false) {
10116
+ clearRetryTimer();
10117
+ await attemptPush(debug, 0);
10118
+ }
10119
+ async function attemptPush(debug, retryIndex) {
10120
+ const native = await resolveSuperwallNativeModule(debug);
10121
+ if (!native) return;
10122
+ const waited = await waitForConfigured(native, debug);
10123
+ if (waited === "timeout") {
10124
+ scheduleRetry(debug, retryIndex);
10125
+ return;
10126
+ }
10127
+ if (waited !== "configured") return;
9933
10128
  const attrs = buildSuperwallAttributes(attributionTracker.get());
9934
10129
  const signature = JSON.stringify(attrs);
9935
10130
  if (signature === lastSignature) {
9936
10131
  log(debug, "attributes unchanged \u2014 skipping push");
9937
10132
  return;
9938
10133
  }
9939
- const native = await resolveSuperwallNativeModule(debug);
9940
- if (!native) return;
9941
- if (!await waitForConfigured(native, debug)) return;
9942
10134
  try {
9943
10135
  await native.setUserAttributes(attrs);
9944
10136
  lastSignature = signature;
@@ -9947,6 +10139,28 @@ async function pushAttributionToSuperwall(debug = false) {
9947
10139
  log(debug, "setUserAttributes error", { err: String(err) });
9948
10140
  }
9949
10141
  }
10142
+ function scheduleRetry(debug, retryIndex) {
10143
+ const delayMs = CONFIG_RETRY_DELAYS_MS[retryIndex];
10144
+ if (delayMs === void 0) {
10145
+ log(debug, "Superwall never configured \u2014 giving up attribute push");
10146
+ return;
10147
+ }
10148
+ clearRetryTimer();
10149
+ log(debug, "scheduling attribute push retry", { retryIndex, delayMs });
10150
+ retryTimer = setTimeout(() => {
10151
+ retryTimer = null;
10152
+ void attemptPush(debug, retryIndex + 1);
10153
+ }, delayMs);
10154
+ }
10155
+ function clearRetryTimer() {
10156
+ if (retryTimer !== null) {
10157
+ clearTimeout(retryTimer);
10158
+ retryTimer = null;
10159
+ }
10160
+ }
10161
+ function cancelSuperwallAttributeRetry() {
10162
+ clearRetryTimer();
10163
+ }
9950
10164
 
9951
10165
  // src/domains/paywall/SuperwallAutoBridge.ts
9952
10166
  init_PaywalloClient();
@@ -9979,6 +10193,7 @@ function buildDeterministicEventId(parts) {
9979
10193
 
9980
10194
  // src/domains/paywall/SuperwallAutoBridge.ts
9981
10195
  var bridgeStarted = false;
10196
+ var startInProgress = false;
9982
10197
  var unsubscribe = null;
9983
10198
  var debugEnabled = false;
9984
10199
  function log2(msg, data) {
@@ -10019,8 +10234,7 @@ function mapDismissToCloseReason(resultType) {
10019
10234
  case "purchased":
10020
10235
  return "purchased";
10021
10236
  case "restored":
10022
- return "purchased";
10023
- // restore counts as a purchase outcome for funnel
10237
+ return "restored";
10024
10238
  case "declined":
10025
10239
  return "dismissed";
10026
10240
  default:
@@ -10056,17 +10270,66 @@ function buildCallbacks() {
10056
10270
  const identifier = "paywallIdentifier" in safe && typeof safe.paywallIdentifier === "string" ? safe.paywallIdentifier : productId;
10057
10271
  log2("onPurchase", { productId, identifier });
10058
10272
  void trackPaywallEvent("purchased", identifier, ts, { product_id: productId });
10273
+ },
10274
+ // Global Superwall event listener — used specifically for `transactionComplete`
10275
+ // which is the only event that exposes the full product (currency/price) +
10276
+ // transaction (storeTransactionId) shape needed to create a server-side
10277
+ // `transactions` row. Other events are skipped (paywallOpen/Dismiss already
10278
+ // handled above with their typed callbacks).
10279
+ onSuperwallEvent: (info) => {
10280
+ const ev = info?.event;
10281
+ if (!ev || ev.event !== "transactionComplete") return;
10282
+ const ts = Date.now();
10283
+ const product = ev.product;
10284
+ const transaction = ev.transaction;
10285
+ if (!product) {
10286
+ log2("transactionComplete missing product \u2014 skipping transaction track");
10287
+ return;
10288
+ }
10289
+ const productId = product.productIdentifier ?? product.id ?? "unknown";
10290
+ const currency = product.currencyCode ?? "USD";
10291
+ const price = product.price ?? 0;
10292
+ const transactionId = transaction?.purchaseToken ?? transaction?.storeTransactionId ?? transaction?.originalTransactionIdentifier ?? `${productId}_${String(ts)}`;
10293
+ const introPrice = product.trialPeriodPrice ?? 0;
10294
+ const isTrial = product.hasFreeTrial === true && introPrice <= 0;
10295
+ const isPaidIntro = introPrice > 0;
10296
+ const paywallIdentifier = ev.paywallInfo?.identifier;
10297
+ log2("onSuperwallEvent: transactionComplete", { productId, currency, price, introPrice, transactionId, isTrial });
10298
+ void (async () => {
10299
+ try {
10300
+ await PaywalloClient.track("transaction", {
10301
+ priority: "critical",
10302
+ properties: {
10303
+ type: isTrial ? "trial_started" : "completed",
10304
+ // Server's IngestTranslator looks for `transaction_id`/`product_id`
10305
+ // /`amount`/`currency`/`paywall_id` keys. A free trial MUST carry
10306
+ // amount=0 (no revenue at start); a paid intro offer carries its
10307
+ // intro amount (real revenue); a normal purchase carries `price`.
10308
+ // `full_price` carries the recurring full price so the server can
10309
+ // value the eventual conversion (free trial) or keep the headline
10310
+ // price alongside the discounted intro amount (paid intro).
10311
+ transaction_id: transactionId,
10312
+ product_id: productId,
10313
+ amount: isTrial ? 0 : isPaidIntro ? introPrice : price,
10314
+ currency,
10315
+ ...isTrial || isPaidIntro ? { full_price: price } : {},
10316
+ ...paywallIdentifier ? { paywall_id: paywallIdentifier } : {}
10317
+ }
10318
+ });
10319
+ } catch (err) {
10320
+ log2("transactionComplete track error", { err: String(err) });
10321
+ }
10322
+ })();
10059
10323
  }
10060
- // onSuperwallEvent removed — transaction sale events are sourced exclusively
10061
- // from server-side webhooks (Apple/Google/Superwall/RevenueCat).
10062
10324
  };
10063
10325
  }
10064
10326
  function startSuperwallAutoBridge(debug = false) {
10065
10327
  debugEnabled = debug;
10066
- if (bridgeStarted) {
10067
- log2("already started, skipping");
10328
+ if (bridgeStarted || startInProgress) {
10329
+ log2("already started or starting, skipping");
10068
10330
  return;
10069
10331
  }
10332
+ startInProgress = true;
10070
10333
  void detectAndStart();
10071
10334
  }
10072
10335
  async function detectAndStart() {
@@ -10077,6 +10340,7 @@ async function detectAndStart() {
10077
10340
  const internal = await import("expo-superwall/build/src/internal/superwallEventBridge");
10078
10341
  if (typeof internal.subscribeToSuperwallEvents !== "function") {
10079
10342
  log2("expo-superwall subscribeToSuperwallEvents not found \u2014 bridge inactive");
10343
+ startInProgress = false;
10080
10344
  return;
10081
10345
  }
10082
10346
  subscribeFn = internal.subscribeToSuperwallEvents;
@@ -10085,6 +10349,11 @@ async function detectAndStart() {
10085
10349
  }
10086
10350
  } catch {
10087
10351
  log2("expo-superwall not installed \u2014 bridge inactive");
10352
+ startInProgress = false;
10353
+ return;
10354
+ }
10355
+ if (!startInProgress) {
10356
+ log2("start cancelled by stop \u2014 bridge inactive");
10088
10357
  return;
10089
10358
  }
10090
10359
  try {
@@ -10094,7 +10363,18 @@ async function detectAndStart() {
10094
10363
  log2("bridge started");
10095
10364
  } catch (err) {
10096
10365
  log2("subscribe failed", { err: String(err) });
10366
+ } finally {
10367
+ startInProgress = false;
10368
+ }
10369
+ }
10370
+ function stopSuperwallAutoBridge() {
10371
+ if (unsubscribe) {
10372
+ unsubscribe();
10373
+ unsubscribe = null;
10097
10374
  }
10375
+ bridgeStarted = false;
10376
+ startInProgress = false;
10377
+ log2("bridge stopped");
10098
10378
  }
10099
10379
 
10100
10380
  // src/PaywalloProvider.tsx
@@ -10661,6 +10941,9 @@ function PaywalloProvider({ children, config }) {
10661
10941
  const configRef = useRef12(config);
10662
10942
  useEffect14(() => {
10663
10943
  let mounted = true;
10944
+ const unsubscribeAttribution = attributionTracker.onCapture(() => {
10945
+ void pushAttributionToSuperwall(configRef.current.debug);
10946
+ });
10664
10947
  const bootstrap = async () => {
10665
10948
  if (!PaywalloClient.isReady()) {
10666
10949
  initLocalization({ detectDevice: true });
@@ -10671,11 +10954,10 @@ function PaywalloProvider({ children, config }) {
10671
10954
  setInitError(err instanceof Error ? err : new ClientError(CLIENT_ERROR_CODES.NOT_INITIALIZED, String(err)));
10672
10955
  return;
10673
10956
  }
10674
- if (!mounted) return;
10675
- startSuperwallAutoBridge(configRef.current.debug);
10676
- void pushAttributionToSuperwall(configRef.current.debug);
10677
10957
  }
10678
10958
  if (!mounted) return;
10959
+ startSuperwallAutoBridge(configRef.current.debug);
10960
+ void pushAttributionToSuperwall(configRef.current.debug);
10679
10961
  setDistinctId(PaywalloClient.getDistinctId());
10680
10962
  setIsInitialized(true);
10681
10963
  if (!autoPreloadTriggeredRef.current) {
@@ -10693,6 +10975,9 @@ function PaywalloProvider({ children, config }) {
10693
10975
  void bootstrap();
10694
10976
  return () => {
10695
10977
  mounted = false;
10978
+ unsubscribeAttribution();
10979
+ stopSuperwallAutoBridge();
10980
+ cancelSuperwallAttributeRetry();
10696
10981
  };
10697
10982
  }, [preloadCampaign]);
10698
10983
  const getPaywallConfig = useCallback20(async (p) => {