@virex-tech/paywallo-sdk 2.2.4 → 2.2.6

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
@@ -814,9 +814,9 @@ var init_CampaignGateService = __esm({
814
814
  }
815
815
  }
816
816
  getPreloadedCampaign(placement) {
817
- const cached = this.preloadedCampaigns.get(placement);
818
- if (!cached) return null;
819
- const age = Date.now() - cached.preloadedAt;
817
+ const cached2 = this.preloadedCampaigns.get(placement);
818
+ if (!cached2) return null;
819
+ const age = Date.now() - cached2.preloadedAt;
820
820
  if (age >= PRELOAD_CACHE_TTL_MS) {
821
821
  this.preloadedCampaigns.delete(placement);
822
822
  return null;
@@ -825,7 +825,7 @@ var init_CampaignGateService = __esm({
825
825
  void this.preloadCampaign(this.apiClientRef, placement).catch(() => {
826
826
  });
827
827
  }
828
- return cached.campaign;
828
+ return cached2.campaign;
829
829
  }
830
830
  async preloadAllActive(apiClient, debug) {
831
831
  this.apiClientRef = apiClient;
@@ -2008,13 +2008,21 @@ var init_NativePushBridge = __esm({
2008
2008
  if (!raw || typeof raw !== "object") return null;
2009
2009
  const r = raw;
2010
2010
  const notification = r["notification"];
2011
+ const asString = (v) => typeof v === "string" ? v : void 0;
2011
2012
  return {
2012
- messageId: typeof r["messageId"] === "string" ? r["messageId"] : void 0,
2013
+ messageId: asString(r["messageId"]),
2013
2014
  data: typeof r["data"] === "object" && r["data"] !== null ? r["data"] : void 0,
2014
2015
  notification: notification && typeof notification === "object" ? {
2015
- title: typeof notification["title"] === "string" ? notification["title"] : void 0,
2016
- body: typeof notification["body"] === "string" ? notification["body"] : void 0
2016
+ title: asString(notification["title"]),
2017
+ body: asString(notification["body"]),
2018
+ imageUrl: asString(notification["imageUrl"])
2017
2019
  } : void 0,
2020
+ // Pass through top-level title/body/imageUrl emitted by the native modules
2021
+ // (both iOS and Android currently emit these at the top level rather than
2022
+ // nested under `notification`).
2023
+ title: asString(r["title"]),
2024
+ body: asString(r["body"]),
2025
+ imageUrl: asString(r["imageUrl"]),
2018
2026
  sentTime: typeof r["sentTime"] === "number" ? r["sentTime"] : void 0
2019
2027
  };
2020
2028
  } catch {
@@ -2149,12 +2157,13 @@ function extractPayload(data) {
2149
2157
  }
2150
2158
  function setupForegroundHandler(deps, onReceive) {
2151
2159
  const { bridge, tracker, debug } = deps;
2152
- const unsubscribe = bridge.onMessage(async (message) => {
2160
+ const unsubscribe2 = bridge.onMessage(async (message) => {
2153
2161
  const data = message.data ?? {};
2154
2162
  const payload = extractPayload({
2155
2163
  ...data,
2156
- title: data["title"] ?? message.notification?.title ?? "",
2157
- body: data["body"] ?? message.notification?.body ?? ""
2164
+ title: data["title"] ?? message.notification?.title ?? message.title ?? "",
2165
+ body: data["body"] ?? message.notification?.body ?? message.body ?? "",
2166
+ imageUrl: data["imageUrl"] ?? message.notification?.imageUrl ?? message.imageUrl
2158
2167
  });
2159
2168
  if (debug) console.log("[Paywallo PUSH] foreground message received:", payload.notificationId);
2160
2169
  onReceive(payload);
@@ -2166,7 +2175,7 @@ function setupForegroundHandler(deps, onReceive) {
2166
2175
  messageId
2167
2176
  });
2168
2177
  });
2169
- return unsubscribe;
2178
+ return unsubscribe2;
2170
2179
  }
2171
2180
  var init_ForegroundHandler = __esm({
2172
2181
  "src/domains/notifications/handlers/ForegroundHandler.ts"() {
@@ -2345,8 +2354,8 @@ async function getInitialNotificationPayload(bridge) {
2345
2354
  notificationId: String(data["notificationId"] ?? data["notification_id"] ?? ""),
2346
2355
  campaignId: String(data["campaignId"] ?? data["campaign_id"] ?? ""),
2347
2356
  variantKey: data["variantKey"] != null ? String(data["variantKey"]) : void 0,
2348
- title: String(data["title"] ?? message.notification?.title ?? ""),
2349
- body: String(data["body"] ?? message.notification?.body ?? ""),
2357
+ title: String(data["title"] ?? message.notification?.title ?? message.title ?? ""),
2358
+ body: String(data["body"] ?? message.notification?.body ?? message.body ?? ""),
2350
2359
  data
2351
2360
  };
2352
2361
  }
@@ -2361,8 +2370,8 @@ var init_NotificationHandlerSetup = __esm({
2361
2370
  // src/core/version.ts
2362
2371
  function resolveVersion() {
2363
2372
  try {
2364
- if ("2.2.4") {
2365
- return "2.2.4";
2373
+ if ("2.2.6") {
2374
+ return "2.2.6";
2366
2375
  }
2367
2376
  } catch {
2368
2377
  }
@@ -2435,9 +2444,6 @@ function mapPermissionStatus(status) {
2435
2444
  function detectPlatform2() {
2436
2445
  return Platform6.OS === "ios" ? "ios" : "android";
2437
2446
  }
2438
- function detectEnvironment() {
2439
- return typeof __DEV__ !== "undefined" && __DEV__ ? "sandbox" : "production";
2440
- }
2441
2447
  async function getAppVersion() {
2442
2448
  try {
2443
2449
  const { collectDeviceInfo: collectDeviceInfo2 } = await Promise.resolve().then(() => (init_deviceInfo(), deviceInfo_exports));
@@ -2477,7 +2483,7 @@ async function clearPersistedToken(secureStorage2) {
2477
2483
  } catch {
2478
2484
  }
2479
2485
  }
2480
- async function buildRegistration(token, getDeviceId, getDistinctId) {
2486
+ async function buildRegistration(token, getDeviceId, getDistinctId, environment) {
2481
2487
  const appVersion = await getAppVersion();
2482
2488
  return {
2483
2489
  token,
@@ -2488,7 +2494,7 @@ async function buildRegistration(token, getDeviceId, getDistinctId) {
2488
2494
  deviceId: getDeviceId?.() ?? void 0,
2489
2495
  locale: await getLocale(),
2490
2496
  timezone: getTimezone2(),
2491
- environment: detectEnvironment()
2497
+ environment
2492
2498
  };
2493
2499
  }
2494
2500
  async function registerToken(deps, token) {
@@ -2497,7 +2503,8 @@ async function registerToken(deps, token) {
2497
2503
  if (debug) console.log("[Paywallo PUSH] httpClient not ready, skipping token registration");
2498
2504
  return;
2499
2505
  }
2500
- const registration = await buildRegistration(token, getDeviceId, getDistinctId);
2506
+ const { environment } = deps;
2507
+ const registration = await buildRegistration(token, getDeviceId, getDistinctId, environment);
2501
2508
  try {
2502
2509
  await httpClient.post(
2503
2510
  TOKEN_ENDPOINT,
@@ -2547,7 +2554,8 @@ async function registerAndPersistToken(deps, token) {
2547
2554
  appKey: deps.appKey,
2548
2555
  getDeviceId: deps.getDeviceId,
2549
2556
  getDistinctId: deps.getDistinctId,
2550
- debug: deps.debug
2557
+ debug: deps.debug,
2558
+ environment: deps.environment
2551
2559
  },
2552
2560
  token
2553
2561
  );
@@ -2564,7 +2572,8 @@ function subscribeTokenRefresh(bridge, deps, onToken) {
2564
2572
  appKey: deps.appKey,
2565
2573
  getDeviceId: deps.getDeviceId,
2566
2574
  getDistinctId: deps.getDistinctId,
2567
- debug: deps.debug
2575
+ debug: deps.debug,
2576
+ environment: deps.environment
2568
2577
  },
2569
2578
  newToken
2570
2579
  ).catch(() => void 0);
@@ -2772,6 +2781,7 @@ var init_NotificationsManager = __esm({
2772
2781
  this.getDistinctId = null;
2773
2782
  this.appKey = null;
2774
2783
  this.apiBaseUrl = null;
2784
+ this.environment = "production";
2775
2785
  this.tokenRefreshCleanup = null;
2776
2786
  this.foregroundCleanup = null;
2777
2787
  this.pendingHandlersConfig = null;
@@ -2791,6 +2801,7 @@ var init_NotificationsManager = __esm({
2791
2801
  this.secureStorage = deps.secureStorage;
2792
2802
  this.getDeviceId = deps.getDeviceId;
2793
2803
  if (deps.getDistinctId) this.getDistinctId = deps.getDistinctId;
2804
+ if (deps.environment) this.environment = deps.environment;
2794
2805
  }
2795
2806
  async initialize(config) {
2796
2807
  this.applyConfig(config);
@@ -2833,7 +2844,8 @@ var init_NotificationsManager = __esm({
2833
2844
  appKey: this.appKey,
2834
2845
  getDeviceId: this.getDeviceId,
2835
2846
  getDistinctId: this.getDistinctId,
2836
- debug: this.debug
2847
+ debug: this.debug,
2848
+ environment: this.environment
2837
2849
  };
2838
2850
  }
2839
2851
  async doRegisterToken(token) {
@@ -3905,6 +3917,32 @@ var init_IAPTransactionEmitter = __esm({
3905
3917
  if (this.getDebug()) console.log("[Paywallo PURCHASE] transaction event emission failed", err);
3906
3918
  }
3907
3919
  }
3920
+ /**
3921
+ * Emits `checkout_started` quando o usuário inicia a compra (toca em comprar),
3922
+ * antes do StoreKit/Billing abrir — conta mesmo que a sheet nativa seja cancelada.
3923
+ * Vira InitiateCheckout no Meta/TikTok. Fire-and-forget — uma falha de tracking
3924
+ * nunca pode travar a compra.
3925
+ */
3926
+ async emitCheckoutStarted(productId) {
3927
+ try {
3928
+ const apiClient = this.getApiClientFn();
3929
+ if (!apiClient) return;
3930
+ const distinctId = PaywalloClient.getDistinctId();
3931
+ if (!distinctId) return;
3932
+ const product = this.getProductFromCache(productId);
3933
+ const properties = {
3934
+ product_id: productId,
3935
+ amount: product?.priceValue ?? 0,
3936
+ currency: product?.currency ?? "USD"
3937
+ };
3938
+ await apiClient.trackEvent("checkout_started", distinctId, properties, void 0, {
3939
+ priority: "critical"
3940
+ });
3941
+ if (this.getDebug()) console.log("[Paywallo PURCHASE] emitted checkout_started", productId);
3942
+ } catch (err) {
3943
+ if (this.getDebug()) console.log("[Paywallo PURCHASE] checkout_started emission failed", err);
3944
+ }
3945
+ }
3908
3946
  /**
3909
3947
  * Forwards a native transaction update to the EventBatcher.
3910
3948
  * Public so tests can exercise the wiring without a fake NativeEventEmitter.
@@ -3927,9 +3965,9 @@ var init_IAPTransactionEmitter = __esm({
3927
3965
  product_id: update.productId
3928
3966
  };
3929
3967
  if (update.type !== "canceled") {
3930
- const cached = this.getProductFromCache(update.productId);
3931
- const amount = update.amount ?? cached?.priceValue;
3932
- const currency = update.currency ?? cached?.currency;
3968
+ const cached2 = this.getProductFromCache(update.productId);
3969
+ const amount = update.amount ?? cached2?.priceValue;
3970
+ const currency = update.currency ?? cached2?.currency;
3933
3971
  if (typeof amount === "number" && amount > 0) properties.amount = amount;
3934
3972
  if (currency) properties.currency = currency;
3935
3973
  }
@@ -4108,10 +4146,10 @@ var init_OfflineQueueStorage = __esm({
4108
4146
  init_NativeStorage();
4109
4147
  init_QueueJournal();
4110
4148
  OfflineQueueStorage = class {
4111
- constructor(config, emit, log) {
4149
+ constructor(config, emit, log2) {
4112
4150
  this.config = config;
4113
4151
  this.emit = emit;
4114
- this.log = log;
4152
+ this.log = log2;
4115
4153
  }
4116
4154
  async loadFromStorage(queue) {
4117
4155
  const { merged, recovered, hadJournal } = await mergeJournalIntoMain(
@@ -4718,12 +4756,25 @@ var init_QueueProcessor = __esm({
4718
4756
  const freshHeaders = this.config.getFreshHeaders?.() ?? {};
4719
4757
  const headers = { ...persistedHeaders, ...freshHeaders };
4720
4758
  this.log("Sending event batch", { size: batch.length, batchIndex: Math.floor(i / BATCH_SIZE) });
4721
- const response = await this.httpClient.request("/sdk/ingest/batch", {
4722
- method: "POST",
4723
- body: { events: bodies },
4724
- headers,
4725
- skipRetry: false
4726
- });
4759
+ let response;
4760
+ try {
4761
+ response = await this.httpClient.request("/sdk/ingest/batch", {
4762
+ method: "POST",
4763
+ body: { events: bodies },
4764
+ headers,
4765
+ skipRetry: false
4766
+ });
4767
+ } catch (error) {
4768
+ for (const item of batch) {
4769
+ retryIds.add(item.id);
4770
+ }
4771
+ console.error("[Paywallo:QUEUE] event batch network error \u2014 items will retry", { size: batch.length, error: error instanceof Error ? error.message : String(error) });
4772
+ this.log("Event batch threw network error - will retry", {
4773
+ size: batch.length,
4774
+ error: error instanceof Error ? error.message : String(error)
4775
+ });
4776
+ continue;
4777
+ }
4727
4778
  if (response.ok) {
4728
4779
  for (const item of batch) {
4729
4780
  await offlineQueue.removeItem(item.id);
@@ -4735,14 +4786,25 @@ var init_QueueProcessor = __esm({
4735
4786
  await offlineQueue.removeItem(item.id);
4736
4787
  failed++;
4737
4788
  }
4789
+ const responseError = typeof response.data === "object" && response.data !== null && "error" in response.data ? response.data.error : void 0;
4790
+ const isDuplicate = typeof responseError === "string" && responseError.toUpperCase().includes("DUPLICATE");
4791
+ if (isDuplicate) {
4792
+ if (this.config.debug) {
4793
+ console.log("[Paywallo:QUEUE] event batch deduplicated by server \u2014 items dropped (benign)", { status: response.status, size: batch.length, error: responseError });
4794
+ }
4795
+ } else {
4796
+ console.error("[Paywallo:QUEUE] event batch failed PERMANENTLY (4xx) \u2014 items dropped", { status: response.status, size: batch.length, responseBody: typeof response.data === "string" ? response.data.slice(0, 1e3) : JSON.stringify(response.data).slice(0, 1e3), firstItemBody: typeof batch[0].body === "string" ? batch[0].body.slice(0, 1e3) : JSON.stringify(batch[0].body).slice(0, 1e3) });
4797
+ }
4738
4798
  this.log("Event batch failed permanently (4xx) - removing from queue", {
4739
4799
  status: response.status,
4740
- size: batch.length
4800
+ size: batch.length,
4801
+ isDuplicate
4741
4802
  });
4742
4803
  } else {
4743
4804
  for (const item of batch) {
4744
4805
  retryIds.add(item.id);
4745
4806
  }
4807
+ console.error("[Paywallo:QUEUE] event batch server error (5xx) \u2014 will retry", { status: response.status, size: batch.length });
4746
4808
  this.log("Event batch failed with server error - will retry", {
4747
4809
  status: response.status,
4748
4810
  size: batch.length
@@ -4763,12 +4825,22 @@ var init_QueueProcessor = __esm({
4763
4825
  });
4764
4826
  const persistedHeaders = item.headers ?? {};
4765
4827
  const freshHeaders = this.config.getFreshHeaders?.() ?? {};
4766
- const response = await this.httpClient.request(item.url, {
4767
- method: item.method,
4768
- body: item.body,
4769
- headers: { ...persistedHeaders, ...freshHeaders },
4770
- skipRetry: false
4771
- });
4828
+ let response;
4829
+ try {
4830
+ response = await this.httpClient.request(item.url, {
4831
+ method: item.method,
4832
+ body: item.body,
4833
+ headers: { ...persistedHeaders, ...freshHeaders },
4834
+ skipRetry: false
4835
+ });
4836
+ } catch (error) {
4837
+ console.error("[Paywallo:QUEUE] individual item network error \u2014 will retry", { id: item.id, url: item.url, error: error instanceof Error ? error.message : String(error) });
4838
+ this.log("Request threw network error - will retry", {
4839
+ id: item.id,
4840
+ error: error instanceof Error ? error.message : String(error)
4841
+ });
4842
+ return { success: false, permanent: false };
4843
+ }
4772
4844
  if (response.ok) {
4773
4845
  this.log("Queue item processed successfully", { id: item.id });
4774
4846
  return { success: true, permanent: false };
@@ -5059,6 +5131,7 @@ var init_IAPService = __esm({
5059
5131
  this.purchaseInFlight = productId;
5060
5132
  if (this.debug) console.log(`[Paywallo PURCHASE] starting ${productId}`);
5061
5133
  try {
5134
+ void this.emitter.emitCheckoutStarted(productId);
5062
5135
  const purchase = await nativeStoreKit.purchase(productId);
5063
5136
  if (!purchase) {
5064
5137
  if (this.debug) console.log(`[Paywallo PURCHASE] ${productId} cancelled`);
@@ -5853,10 +5926,10 @@ var init_PlanService = __esm({
5853
5926
  }
5854
5927
  async getCurrentPlan(forceRefresh = false) {
5855
5928
  if (!forceRefresh) {
5856
- const cached = this.cache.get();
5857
- if (cached && !cached.isStale) {
5858
- const current = cached.data.plans.find(
5859
- (p) => p.identifier === cached.data.currentIdentifier
5929
+ const cached2 = this.cache.get();
5930
+ if (cached2 && !cached2.isStale) {
5931
+ const current = cached2.data.plans.find(
5932
+ (p) => p.identifier === cached2.data.currentIdentifier
5860
5933
  );
5861
5934
  return current ?? null;
5862
5935
  }
@@ -5883,8 +5956,8 @@ var init_PlanService = __esm({
5883
5956
  }
5884
5957
  async getAllPlans(forceRefresh = false) {
5885
5958
  if (!forceRefresh) {
5886
- const cached = this.cache.get();
5887
- if (cached && !cached.isStale) return cached.data;
5959
+ const cached2 = this.cache.get();
5960
+ if (cached2 && !cached2.isStale) return cached2.data;
5888
5961
  }
5889
5962
  if (!this.cache.get()) {
5890
5963
  try {
@@ -6002,6 +6075,163 @@ var init_analytics = __esm({
6002
6075
  }
6003
6076
  });
6004
6077
 
6078
+ // src/core/events/schemas.ts
6079
+ import { z } from "zod";
6080
+ var lifecycleEventSchema, identifyEventSchema, paywallEventSchema, transactionEventSchema, onboardingEventSchema, notificationEventSchema, customEventSchema, EVENT_SCHEMAS;
6081
+ var init_schemas = __esm({
6082
+ "src/core/events/schemas.ts"() {
6083
+ "use strict";
6084
+ lifecycleEventSchema = z.object({
6085
+ type: z.enum([
6086
+ "install",
6087
+ "cold_start",
6088
+ "foreground",
6089
+ "background",
6090
+ "session_start",
6091
+ "session_end"
6092
+ ]),
6093
+ session_id: z.string().optional(),
6094
+ duration_s: z.number().nonnegative().optional(),
6095
+ first_ever: z.boolean().optional(),
6096
+ // Contexto leve transportado em lifecycle (útil pra cold_start/install)
6097
+ app_version: z.string().optional(),
6098
+ device_type: z.enum(["ios", "android"]).optional(),
6099
+ os_version: z.string().optional(),
6100
+ // Timestamps opcionais — o EventBatcher injeta `timestamp` no envelope
6101
+ started_at: z.string().optional(),
6102
+ ended_at: z.string().optional()
6103
+ }).strict();
6104
+ identifyEventSchema = z.object({
6105
+ distinct_id: z.string().min(1),
6106
+ email: z.email().optional(),
6107
+ traits: z.record(z.string(), z.unknown()).optional(),
6108
+ attribution: z.record(z.string(), z.unknown()).optional(),
6109
+ device: z.record(z.string(), z.unknown()).optional()
6110
+ }).strict();
6111
+ paywallEventSchema = z.object({
6112
+ type: z.enum(["viewed", "closed", "purchased"]),
6113
+ paywall_id: z.string().min(1),
6114
+ placement: z.string().optional(),
6115
+ variant_id: z.string().optional(),
6116
+ variant_key: z.string().optional(),
6117
+ campaign_id: z.string().optional(),
6118
+ // `closed`-specific
6119
+ closed_at: z.string().optional(),
6120
+ duration_s: z.number().nonnegative().optional(),
6121
+ close_reason: z.enum(["dismiss", "cta", "purchase", "error", "timeout"]).optional(),
6122
+ scroll_depth: z.number().min(0).max(1).optional(),
6123
+ // `purchased`-specific (mínimo — transaction canônica leva detalhes)
6124
+ product_id: z.string().optional()
6125
+ }).strict();
6126
+ transactionEventSchema = z.object({
6127
+ type: z.enum([
6128
+ "completed",
6129
+ "failed",
6130
+ "refunded",
6131
+ "renewed",
6132
+ "canceled",
6133
+ "expired"
6134
+ ]),
6135
+ tx_id: z.string().min(1),
6136
+ amount: z.number().optional(),
6137
+ currency: z.string().length(3).optional(),
6138
+ product_id: z.string().optional(),
6139
+ subscription_id: z.string().optional(),
6140
+ paywall_id: z.string().optional(),
6141
+ variant_id: z.string().optional(),
6142
+ reason: z.string().optional()
6143
+ }).strict();
6144
+ onboardingEventSchema = z.object({
6145
+ type: z.enum(["step", "complete", "drop"]),
6146
+ step_index: z.number().int().nonnegative().optional(),
6147
+ step_name: z.string().optional(),
6148
+ variant_key: z.string().optional(),
6149
+ time_on_prev_s: z.number().nonnegative().optional()
6150
+ }).strict();
6151
+ notificationEventSchema = z.object({
6152
+ type: z.enum(["delivered", "displayed", "clicked", "dismissed"]),
6153
+ notification_id: z.string().optional(),
6154
+ campaign_id: z.string().optional(),
6155
+ variant_key: z.string().nullable().optional(),
6156
+ message_id: z.string().optional(),
6157
+ device_id: z.string().optional(),
6158
+ push_platform: z.enum(["ios", "android"]).optional(),
6159
+ timezone: z.string().optional(),
6160
+ failure_reason: z.string().optional(),
6161
+ app_user_id: z.string().optional()
6162
+ }).strict();
6163
+ customEventSchema = z.object({
6164
+ name: z.string().min(1).regex(
6165
+ /^(\$[a-zA-Z0-9_]+|[a-z][a-z0-9_]*)$/,
6166
+ "custom event name must be snake_case or $reserved"
6167
+ ),
6168
+ props: z.record(z.string(), z.unknown()).optional()
6169
+ }).strict();
6170
+ EVENT_SCHEMAS = {
6171
+ lifecycle: lifecycleEventSchema,
6172
+ identify: identifyEventSchema,
6173
+ paywall: paywallEventSchema,
6174
+ transaction: transactionEventSchema,
6175
+ onboarding: onboardingEventSchema,
6176
+ notification: notificationEventSchema,
6177
+ custom: customEventSchema
6178
+ };
6179
+ }
6180
+ });
6181
+
6182
+ // src/core/events/eventFamilies.ts
6183
+ function isDeprecatedEvent(eventName) {
6184
+ return DEPRECATED_EVENT_NAMES.includes(eventName);
6185
+ }
6186
+ function isCanonicalFamily(name) {
6187
+ return CANONICAL_FAMILIES.includes(name);
6188
+ }
6189
+ function detectFamily(eventName) {
6190
+ if (isCanonicalFamily(eventName) && eventName !== "custom") {
6191
+ return eventName;
6192
+ }
6193
+ return "custom";
6194
+ }
6195
+ function validateEvent(eventName, properties) {
6196
+ const family = detectFamily(eventName);
6197
+ const schema = EVENT_SCHEMAS[family];
6198
+ const input = family === "custom" ? { name: eventName, ...properties ? { props: properties } : {} } : properties ?? {};
6199
+ const parsed = schema.safeParse(input);
6200
+ if (parsed.success) {
6201
+ return { ok: true, family, data: parsed.data };
6202
+ }
6203
+ return { ok: false, family, error: parsed.error };
6204
+ }
6205
+ function formatValidationError(error) {
6206
+ return error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
6207
+ }
6208
+ var CANONICAL_FAMILIES, DEPRECATED_EVENT_NAMES;
6209
+ var init_eventFamilies = __esm({
6210
+ "src/core/events/eventFamilies.ts"() {
6211
+ "use strict";
6212
+ init_schemas();
6213
+ CANONICAL_FAMILIES = [
6214
+ "lifecycle",
6215
+ "identify",
6216
+ "paywall",
6217
+ "transaction",
6218
+ "onboarding",
6219
+ "notification",
6220
+ "custom"
6221
+ ];
6222
+ DEPRECATED_EVENT_NAMES = [
6223
+ "$paywall_purchased",
6224
+ "$paywall_product_selected",
6225
+ "$core_action",
6226
+ "$campaign_impression",
6227
+ "$app_open",
6228
+ "$app_background",
6229
+ "$app_foreground",
6230
+ "session.end"
6231
+ ];
6232
+ }
6233
+ });
6234
+
6005
6235
  // src/core/PaywalloAnalytics.ts
6006
6236
  function isValidEventName(name) {
6007
6237
  if (name.startsWith("$")) return /^\$[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
@@ -6010,7 +6240,7 @@ function isValidEventName(name) {
6010
6240
  async function trackEvent(apiClient, state, eventName, options) {
6011
6241
  const distinctId = identityManager.getDistinctId();
6012
6242
  if (!distinctId) {
6013
- if (state.config?.debug) console.log(`[Paywallo TRACK] ${eventName} skipped (no distinctId)`);
6243
+ console.error("[Paywallo:DROP] event dropped \u2014 no distinctId available yet", { eventName });
6014
6244
  return;
6015
6245
  }
6016
6246
  if (!isValidEventName(eventName)) {
@@ -6019,10 +6249,14 @@ async function trackEvent(apiClient, state, eventName, options) {
6019
6249
  }
6020
6250
  if (state.config?.debug && eventName.startsWith("$purchase")) console.log(`[Paywallo TRACK] ${eventName}`, options?.properties ?? {});
6021
6251
  const sessionId = sessionManager.getSessionId();
6252
+ const family = detectFamily(eventName);
6253
+ const isCustomFamily = family === "custom";
6254
+ const identityProps = isCustomFamily ? identityManager.getProperties() : {};
6255
+ const sessionProp = isCustomFamily && sessionId ? { sessionId } : {};
6022
6256
  await apiClient.trackEvent(
6023
6257
  eventName,
6024
6258
  distinctId,
6025
- { ...identityManager.getProperties(), ...options?.properties, ...sessionId && { sessionId } },
6259
+ { ...identityProps, ...options?.properties, ...sessionProp },
6026
6260
  options?.timestamp,
6027
6261
  options?.priority ? { priority: options.priority } : void 0
6028
6262
  );
@@ -6052,6 +6286,7 @@ var init_PaywalloAnalytics = __esm({
6052
6286
  init_identity();
6053
6287
  init_session();
6054
6288
  init_ClientError();
6289
+ init_eventFamilies();
6055
6290
  }
6056
6291
  });
6057
6292
 
@@ -6316,49 +6551,49 @@ var init_SubscriptionCache = __esm({
6316
6551
  await this.storage.remove(keyFor(distinctId));
6317
6552
  return null;
6318
6553
  }
6319
- const cached = parsed;
6320
- if (cached.data.subscription?.expiresAt) {
6321
- cached.data.subscription.expiresAt = new Date(cached.data.subscription.expiresAt);
6554
+ const cached2 = parsed;
6555
+ if (cached2.data.subscription?.expiresAt) {
6556
+ cached2.data.subscription.expiresAt = new Date(cached2.data.subscription.expiresAt);
6322
6557
  }
6323
- if (cached.data.subscription?.originalPurchaseDate) {
6324
- cached.data.subscription.originalPurchaseDate = new Date(
6325
- cached.data.subscription.originalPurchaseDate
6558
+ if (cached2.data.subscription?.originalPurchaseDate) {
6559
+ cached2.data.subscription.originalPurchaseDate = new Date(
6560
+ cached2.data.subscription.originalPurchaseDate
6326
6561
  );
6327
6562
  }
6328
- if (cached.data.subscription?.latestPurchaseDate) {
6329
- cached.data.subscription.latestPurchaseDate = new Date(
6330
- cached.data.subscription.latestPurchaseDate
6563
+ if (cached2.data.subscription?.latestPurchaseDate) {
6564
+ cached2.data.subscription.latestPurchaseDate = new Date(
6565
+ cached2.data.subscription.latestPurchaseDate
6331
6566
  );
6332
6567
  }
6333
- if (cached.data.subscription?.cancellationDate) {
6334
- cached.data.subscription.cancellationDate = new Date(
6335
- cached.data.subscription.cancellationDate
6568
+ if (cached2.data.subscription?.cancellationDate) {
6569
+ cached2.data.subscription.cancellationDate = new Date(
6570
+ cached2.data.subscription.cancellationDate
6336
6571
  );
6337
6572
  }
6338
- if (cached.data.subscription?.gracePeriodExpiresAt) {
6339
- cached.data.subscription.gracePeriodExpiresAt = new Date(
6340
- cached.data.subscription.gracePeriodExpiresAt
6573
+ if (cached2.data.subscription?.gracePeriodExpiresAt) {
6574
+ cached2.data.subscription.gracePeriodExpiresAt = new Date(
6575
+ cached2.data.subscription.gracePeriodExpiresAt
6341
6576
  );
6342
6577
  }
6343
- cached.isStale = this.isExpired(cached);
6344
- if (!cached.isStale) {
6345
- this.memoryCache.set(distinctId, cached);
6578
+ cached2.isStale = this.isExpired(cached2);
6579
+ if (!cached2.isStale) {
6580
+ this.memoryCache.set(distinctId, cached2);
6346
6581
  }
6347
- return cached;
6582
+ return cached2;
6348
6583
  } catch {
6349
6584
  return null;
6350
6585
  }
6351
6586
  }
6352
6587
  async set(distinctId, data) {
6353
6588
  if (!distinctId) return;
6354
- const cached = {
6589
+ const cached2 = {
6355
6590
  data,
6356
6591
  cachedAt: Date.now(),
6357
6592
  isStale: false
6358
6593
  };
6359
- this.memoryCache.set(distinctId, cached);
6594
+ this.memoryCache.set(distinctId, cached2);
6360
6595
  try {
6361
- await this.storage.set(keyFor(distinctId), JSON.stringify(cached));
6596
+ await this.storage.set(keyFor(distinctId), JSON.stringify(cached2));
6362
6597
  await this.addToIndex(distinctId);
6363
6598
  } catch {
6364
6599
  }
@@ -6402,8 +6637,8 @@ var init_SubscriptionCache = __esm({
6402
6637
  ...allKeys.map((k) => this.storage.remove(keyFor(k)))
6403
6638
  ]);
6404
6639
  }
6405
- isExpired(cached) {
6406
- return Date.now() - cached.cachedAt > this.ttl;
6640
+ isExpired(cached2) {
6641
+ return Date.now() - cached2.cachedAt > this.ttl;
6407
6642
  }
6408
6643
  setTTL(ttl) {
6409
6644
  this.ttl = ttl;
@@ -6459,9 +6694,9 @@ var init_SubscriptionManager = __esm({
6459
6694
  return this.getEmptyStatus();
6460
6695
  }
6461
6696
  if (!forceRefresh) {
6462
- const cached = await this.cache.get(this.cacheKey());
6463
- if (cached && !cached.isStale) {
6464
- return cached.data;
6697
+ const cached2 = await this.cache.get(this.cacheKey());
6698
+ if (cached2 && !cached2.isStale) {
6699
+ return cached2.data;
6465
6700
  }
6466
6701
  }
6467
6702
  try {
@@ -6471,9 +6706,9 @@ var init_SubscriptionManager = __esm({
6471
6706
  return status;
6472
6707
  } catch (error) {
6473
6708
  this.log("Failed to fetch subscription status", error);
6474
- const cached = await this.cache.get(this.cacheKey());
6475
- if (cached) {
6476
- return cached.data;
6709
+ const cached2 = await this.cache.get(this.cacheKey());
6710
+ if (cached2) {
6711
+ return cached2.data;
6477
6712
  }
6478
6713
  return this.getEmptyStatus();
6479
6714
  }
@@ -6888,163 +7123,6 @@ var init_ApiClientQueue = __esm({
6888
7123
  }
6889
7124
  });
6890
7125
 
6891
- // src/core/events/schemas.ts
6892
- import { z } from "zod";
6893
- var lifecycleEventSchema, identifyEventSchema, paywallEventSchema, transactionEventSchema, onboardingEventSchema, notificationEventSchema, customEventSchema, EVENT_SCHEMAS;
6894
- var init_schemas = __esm({
6895
- "src/core/events/schemas.ts"() {
6896
- "use strict";
6897
- lifecycleEventSchema = z.object({
6898
- type: z.enum([
6899
- "install",
6900
- "cold_start",
6901
- "foreground",
6902
- "background",
6903
- "session_start",
6904
- "session_end"
6905
- ]),
6906
- session_id: z.string().optional(),
6907
- duration_s: z.number().nonnegative().optional(),
6908
- first_ever: z.boolean().optional(),
6909
- // Contexto leve transportado em lifecycle (útil pra cold_start/install)
6910
- app_version: z.string().optional(),
6911
- device_type: z.enum(["ios", "android"]).optional(),
6912
- os_version: z.string().optional(),
6913
- // Timestamps opcionais — o EventBatcher injeta `timestamp` no envelope
6914
- started_at: z.string().optional(),
6915
- ended_at: z.string().optional()
6916
- }).strict();
6917
- identifyEventSchema = z.object({
6918
- distinct_id: z.string().min(1),
6919
- email: z.email().optional(),
6920
- traits: z.record(z.string(), z.unknown()).optional(),
6921
- attribution: z.record(z.string(), z.unknown()).optional(),
6922
- device: z.record(z.string(), z.unknown()).optional()
6923
- }).strict();
6924
- paywallEventSchema = z.object({
6925
- type: z.enum(["viewed", "closed", "purchased"]),
6926
- paywall_id: z.string().min(1),
6927
- placement: z.string().optional(),
6928
- variant_id: z.string().optional(),
6929
- variant_key: z.string().optional(),
6930
- campaign_id: z.string().optional(),
6931
- // `closed`-specific
6932
- closed_at: z.string().optional(),
6933
- duration_s: z.number().nonnegative().optional(),
6934
- close_reason: z.enum(["dismiss", "cta", "purchase", "error", "timeout"]).optional(),
6935
- scroll_depth: z.number().min(0).max(1).optional(),
6936
- // `purchased`-specific (mínimo — transaction canônica leva detalhes)
6937
- product_id: z.string().optional()
6938
- }).strict();
6939
- transactionEventSchema = z.object({
6940
- type: z.enum([
6941
- "completed",
6942
- "failed",
6943
- "refunded",
6944
- "renewed",
6945
- "canceled",
6946
- "expired"
6947
- ]),
6948
- tx_id: z.string().min(1),
6949
- amount: z.number().optional(),
6950
- currency: z.string().length(3).optional(),
6951
- product_id: z.string().optional(),
6952
- subscription_id: z.string().optional(),
6953
- paywall_id: z.string().optional(),
6954
- variant_id: z.string().optional(),
6955
- reason: z.string().optional()
6956
- }).strict();
6957
- onboardingEventSchema = z.object({
6958
- type: z.enum(["step", "complete", "drop"]),
6959
- step_index: z.number().int().nonnegative().optional(),
6960
- step_name: z.string().optional(),
6961
- variant_key: z.string().optional(),
6962
- time_on_prev_s: z.number().nonnegative().optional()
6963
- }).strict();
6964
- notificationEventSchema = z.object({
6965
- type: z.enum(["delivered", "displayed", "clicked", "dismissed"]),
6966
- notification_id: z.string().optional(),
6967
- campaign_id: z.string().optional(),
6968
- variant_key: z.string().nullable().optional(),
6969
- message_id: z.string().optional(),
6970
- device_id: z.string().optional(),
6971
- push_platform: z.enum(["ios", "android"]).optional(),
6972
- timezone: z.string().optional(),
6973
- failure_reason: z.string().optional(),
6974
- app_user_id: z.string().optional()
6975
- }).strict();
6976
- customEventSchema = z.object({
6977
- name: z.string().min(1).regex(
6978
- /^(\$[a-zA-Z0-9_]+|[a-z][a-z0-9_]*)$/,
6979
- "custom event name must be snake_case or $reserved"
6980
- ),
6981
- props: z.record(z.string(), z.unknown()).optional()
6982
- }).strict();
6983
- EVENT_SCHEMAS = {
6984
- lifecycle: lifecycleEventSchema,
6985
- identify: identifyEventSchema,
6986
- paywall: paywallEventSchema,
6987
- transaction: transactionEventSchema,
6988
- onboarding: onboardingEventSchema,
6989
- notification: notificationEventSchema,
6990
- custom: customEventSchema
6991
- };
6992
- }
6993
- });
6994
-
6995
- // src/core/events/eventFamilies.ts
6996
- function isDeprecatedEvent(eventName) {
6997
- return DEPRECATED_EVENT_NAMES.includes(eventName);
6998
- }
6999
- function isCanonicalFamily(name) {
7000
- return CANONICAL_FAMILIES.includes(name);
7001
- }
7002
- function detectFamily(eventName) {
7003
- if (isCanonicalFamily(eventName) && eventName !== "custom") {
7004
- return eventName;
7005
- }
7006
- return "custom";
7007
- }
7008
- function validateEvent(eventName, properties) {
7009
- const family = detectFamily(eventName);
7010
- const schema = EVENT_SCHEMAS[family];
7011
- const input = family === "custom" ? { name: eventName, ...properties ? { props: properties } : {} } : properties ?? {};
7012
- const parsed = schema.safeParse(input);
7013
- if (parsed.success) {
7014
- return { ok: true, family, data: parsed.data };
7015
- }
7016
- return { ok: false, family, error: parsed.error };
7017
- }
7018
- function formatValidationError(error) {
7019
- return error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
7020
- }
7021
- var CANONICAL_FAMILIES, DEPRECATED_EVENT_NAMES;
7022
- var init_eventFamilies = __esm({
7023
- "src/core/events/eventFamilies.ts"() {
7024
- "use strict";
7025
- init_schemas();
7026
- CANONICAL_FAMILIES = [
7027
- "lifecycle",
7028
- "identify",
7029
- "paywall",
7030
- "transaction",
7031
- "onboarding",
7032
- "notification",
7033
- "custom"
7034
- ];
7035
- DEPRECATED_EVENT_NAMES = [
7036
- "$paywall_purchased",
7037
- "$paywall_product_selected",
7038
- "$core_action",
7039
- "$campaign_impression",
7040
- "$app_open",
7041
- "$app_background",
7042
- "$app_foreground",
7043
- "session.end"
7044
- ];
7045
- }
7046
- });
7047
-
7048
7126
  // src/core/eventEnvelopeV2.ts
7049
7127
  import { Platform as Platform13 } from "react-native";
7050
7128
  import { z as z2 } from "zod";
@@ -7074,6 +7152,9 @@ function buildV2Envelope(events, providerContext) {
7074
7152
  if (context.distinct_id === void 0 && events.length > 0) {
7075
7153
  context.distinct_id = events[0].distinctId;
7076
7154
  }
7155
+ if (!context.distinct_id || context.distinct_id.length === 0) {
7156
+ console.error("[Paywallo:ENVELOPE] distinct_id is empty after resolution \u2014 envelope will fail schema", { providerContext, eventCount: events.length, firstEventName: events[0]?.eventName });
7157
+ }
7077
7158
  return { context, events: v2Events };
7078
7159
  }
7079
7160
  function resolveProviderContext(provider) {
@@ -7125,12 +7206,18 @@ var init_eventEnvelopeV2 = __esm({
7125
7206
  session_id: z2.string().optional(),
7126
7207
  device_id: z2.string().optional(),
7127
7208
  app_version: z2.string().optional(),
7209
+ app_build: z2.string().optional(),
7210
+ bundle_id: z2.string().optional(),
7128
7211
  sdk_version: z2.string().optional(),
7129
7212
  platform: z2.string().optional(),
7130
7213
  os_version: z2.string().optional(),
7131
7214
  device_model: z2.string().optional(),
7132
7215
  timezone: z2.string().optional(),
7133
7216
  locale: z2.string().optional(),
7217
+ carrier: z2.string().optional(),
7218
+ screen_width: z2.number().optional(),
7219
+ screen_height: z2.number().optional(),
7220
+ screen_density: z2.number().optional(),
7134
7221
  attribution: z2.record(z2.string(), z2.unknown()).optional(),
7135
7222
  ids: z2.record(z2.string(), z2.unknown()).optional()
7136
7223
  });
@@ -7143,12 +7230,21 @@ var init_eventEnvelopeV2 = __esm({
7143
7230
  session_id: ["session_id", "sessionId"],
7144
7231
  device_id: ["device_id", "deviceId"],
7145
7232
  app_version: ["app_version", "appVersion"],
7233
+ // F2.2 — alias só snake_case canônico: campos vêm do context provider, não
7234
+ // de properties de evento. Evita promover (e deletar) chaves do payload do
7235
+ // $app_installed que o server lê (screenWidth etc.).
7236
+ app_build: ["app_build"],
7237
+ bundle_id: ["bundle_id"],
7146
7238
  sdk_version: ["sdk_version", "sdkVersion"],
7147
7239
  platform: ["platform"],
7148
7240
  os_version: ["os_version", "osVersion", "systemVersion"],
7149
7241
  device_model: ["device_model", "deviceModel", "model"],
7150
7242
  timezone: ["timezone", "timeZone"],
7151
7243
  locale: ["locale"],
7244
+ carrier: ["carrier_ctx"],
7245
+ screen_width: ["screen_width"],
7246
+ screen_height: ["screen_height"],
7247
+ screen_density: ["screen_density"],
7152
7248
  attribution: ["attribution"],
7153
7249
  ids: ["ids"]
7154
7250
  };
@@ -7275,8 +7371,8 @@ var init_EventBatcher = __esm({
7275
7371
  for (const event of drained) {
7276
7372
  try {
7277
7373
  await this.postBatch([event], `event_critical:${event.eventName}`, "critical");
7278
- } catch {
7279
- if (this.debug) console.log(`[Paywallo] critical event ${event.eventName} post failed`);
7374
+ } catch (err) {
7375
+ console.error("[Paywallo:CRITICAL] critical event post failed", { eventName: event.eventName, error: err instanceof Error ? err.message : String(err) });
7280
7376
  }
7281
7377
  }
7282
7378
  }
@@ -7289,8 +7385,8 @@ var init_EventBatcher = __esm({
7289
7385
  }
7290
7386
  try {
7291
7387
  await this.postBatch(batch, `event_batch:${batch.length}`);
7292
- } catch {
7293
- if (this.debug) console.log(`[Paywallo] normal batch post failed \u2014 OfflineQueue will handle durability`);
7388
+ } catch (err) {
7389
+ console.error("[Paywallo:NORMAL] batch post failed \u2014 relying on OfflineQueue", { batchSize: batch.length, error: err instanceof Error ? err.message : String(err) });
7294
7390
  }
7295
7391
  }
7296
7392
  async postBatch(events, label, priority = "normal") {
@@ -7305,6 +7401,7 @@ var init_EventBatcher = __esm({
7305
7401
  const parsed = v2EnvelopeSchema.safeParse(envelope);
7306
7402
  if (!parsed.success) {
7307
7403
  if (this.debug) console.log("[Paywallo EVENTS] V2 envelope schema validation failed:", parsed.error.issues);
7404
+ console.error("[Paywallo:CRITICAL] V2 envelope schema validation failed", { issues: parsed.error.issues, eventCount: events.length, firstEventName: events[0]?.eventName, context: providerContext });
7308
7405
  throw parsed.error;
7309
7406
  }
7310
7407
  await this.post(V2_BATCH_ENDPOINT, parsed.data, label, priority);
@@ -7887,7 +7984,8 @@ function setupNotifications(apiClient, config, environment, eventPipeline) {
7887
7984
  httpClient: apiClient.getHttpClient(),
7888
7985
  secureStorage: new SecureStorage(config.debug),
7889
7986
  getDeviceId: () => identityManager.getDeviceId(),
7890
- getDistinctId: () => identityManager.getDistinctId()
7987
+ getDistinctId: () => identityManager.getDistinctId(),
7988
+ environment: environment === "Sandbox" ? "sandbox" : "production"
7891
7989
  });
7892
7990
  notificationsManager.setupHandlers({ eventBatcher: eventPipeline });
7893
7991
  notificationsManager.initialize({
@@ -7971,7 +8069,38 @@ var init_PaywalloState = __esm({
7971
8069
  }
7972
8070
  });
7973
8071
 
8072
+ // src/utils/detectEnvironment.ts
8073
+ import { NativeModules as NativeModules8 } from "react-native";
8074
+ async function detectEnvironment() {
8075
+ if (cached !== null) return cached;
8076
+ if (typeof globalThis.__DEV__ !== "undefined" && globalThis.__DEV__) {
8077
+ cached = "sandbox";
8078
+ return cached;
8079
+ }
8080
+ if (!nativeModule) {
8081
+ cached = "production";
8082
+ return cached;
8083
+ }
8084
+ try {
8085
+ const result = await nativeModule.detect();
8086
+ cached = result === "sandbox" ? "sandbox" : "production";
8087
+ return cached;
8088
+ } catch {
8089
+ cached = "production";
8090
+ return cached;
8091
+ }
8092
+ }
8093
+ var nativeModule, cached;
8094
+ var init_detectEnvironment = __esm({
8095
+ "src/utils/detectEnvironment.ts"() {
8096
+ "use strict";
8097
+ nativeModule = NativeModules8.PaywalloEnv ?? null;
8098
+ cached = null;
8099
+ }
8100
+ });
8101
+
7974
8102
  // src/core/PaywalloInitializer.ts
8103
+ import { Dimensions as Dimensions4, PixelRatio as PixelRatio2 } from "react-native";
7975
8104
  async function initWithRetry(state, config) {
7976
8105
  while (state.initAttempts <= MAX_INIT_RETRIES) {
7977
8106
  try {
@@ -8050,7 +8179,13 @@ async function resolveSessionFlags(state, keys, apiClient, distinctId) {
8050
8179
  }
8051
8180
  }
8052
8181
  async function doInit(state, config) {
8053
- const environment = config.environment ?? (typeof __DEV__ !== "undefined" && __DEV__ ? "Sandbox" : "Production");
8182
+ let environment;
8183
+ if (config.environment) {
8184
+ environment = config.environment;
8185
+ } else {
8186
+ const detected = await detectEnvironment();
8187
+ environment = detected === "sandbox" ? "Sandbox" : "Production";
8188
+ }
8054
8189
  const offlineQueueEnabled = config.offlineQueueEnabled !== false;
8055
8190
  await Promise.all([
8056
8191
  networkMonitor.initialize({ debug: config.debug }),
@@ -8138,15 +8273,31 @@ async function doInit(state, config) {
8138
8273
  timezone = opts.timeZone || void 0;
8139
8274
  } catch {
8140
8275
  }
8276
+ let screenWidth;
8277
+ let screenHeight;
8278
+ let screenDensity;
8279
+ try {
8280
+ const screen = Dimensions4.get("screen");
8281
+ screenWidth = Math.round(screen.width) || void 0;
8282
+ screenHeight = Math.round(screen.height) || void 0;
8283
+ screenDensity = PixelRatio2.get() || void 0;
8284
+ } catch {
8285
+ }
8141
8286
  return {
8142
8287
  distinct_id: identityManager.getDistinctId() || void 0,
8143
8288
  device_id: identityManager.getDeviceId() ?? void 0,
8144
8289
  session_id: sessionManager.getSessionId() ?? void 0,
8145
8290
  app_version: device?.appVersion && device.appVersion !== "0.0.0" ? device.appVersion : void 0,
8291
+ app_build: device?.buildNumber && device.buildNumber !== "0" ? device.buildNumber : void 0,
8292
+ bundle_id: device?.bundleId || void 0,
8146
8293
  os_version: device?.systemVersion && device.systemVersion !== "0.0.0" ? device.systemVersion : void 0,
8147
8294
  device_model: device?.modelId || device?.model || void 0,
8148
8295
  timezone,
8149
8296
  locale,
8297
+ carrier: device?.carrier || void 0,
8298
+ screen_width: screenWidth,
8299
+ screen_height: screenHeight,
8300
+ screen_density: screenDensity,
8150
8301
  ...attribution && Object.keys(attribution).length > 0 && { attribution },
8151
8302
  ...ids && Object.keys(ids).length > 0 && { ids }
8152
8303
  };
@@ -8218,6 +8369,7 @@ var init_PaywalloInitializer = __esm({
8218
8369
  init_PaywalloState();
8219
8370
  init_queue();
8220
8371
  init_NativeStorage();
8372
+ init_detectEnvironment();
8221
8373
  }
8222
8374
  });
8223
8375
 
@@ -8675,8 +8827,8 @@ var init_OfferingService = __esm({
8675
8827
  this.config = config;
8676
8828
  }
8677
8829
  async getOfferings(ids) {
8678
- const cached = this.getMemCache();
8679
- if (cached && !cached.isStale) return this.filterByIds(cached.data, ids);
8830
+ const cached2 = this.getMemCache();
8831
+ if (cached2 && !cached2.isStale) return this.filterByIds(cached2.data, ids);
8680
8832
  if (!this.cache) {
8681
8833
  try {
8682
8834
  const stored = await secureStorage.get(OFFERINGS_CACHE_KEY);
@@ -9215,9 +9367,10 @@ function useSubscription() {
9215
9367
 
9216
9368
  // src/PaywalloProvider.tsx
9217
9369
  import { useCallback as useCallback19, useEffect as useEffect12, useLayoutEffect, useMemo as useMemo2, useRef as useRef10, useState as useState16 } from "react";
9218
- import { Animated as Animated3, Dimensions as Dimensions4, Easing as Easing2, StyleSheet as StyleSheet3 } from "react-native";
9370
+ import { Animated as Animated3, Dimensions as Dimensions5, Easing as Easing2, StyleSheet as StyleSheet3 } from "react-native";
9219
9371
  init_PaywalloClient();
9220
9372
  init_SecureStorage();
9373
+ init_NativeStorage();
9221
9374
  init_campaign();
9222
9375
  init_paywall();
9223
9376
  init_paywall();
@@ -9249,12 +9402,13 @@ async function detectOsVersion() {
9249
9402
  }
9250
9403
 
9251
9404
  // src/hooks/useAppAutoEvents.ts
9252
- var FIRST_SEEN_KEY = "@paywallo:firstSeen";
9405
+ var FIRST_SEEN_KEY = "@paywallo:firstSeen_v226";
9253
9406
  var DEBOUNCE_MS = 1e3;
9254
9407
  function useAppAutoEvents(config) {
9255
9408
  const { isInitialized, getAppVersion: getAppVersion2, getSessionId, emitLifecycle, storageGet, storageSet, debug } = config;
9256
9409
  const didRunRef = useRef7(false);
9257
9410
  useEffect11(() => {
9411
+ if (debug) console.log("[Paywallo EVENTS] useEffect fired", { isInitialized, didRunRefCurrent: didRunRef.current });
9258
9412
  if (!isInitialized || didRunRef.current) return;
9259
9413
  const timer = setTimeout(() => {
9260
9414
  if (didRunRef.current) return;
@@ -9264,17 +9418,23 @@ function useAppAutoEvents(config) {
9264
9418
  const appVersion = getAppVersion2();
9265
9419
  const deviceType = await detectPlatform3();
9266
9420
  const osVersion = await detectOsVersion();
9421
+ if (debug) console.log("[Paywallo EVENTS] computed device context", { deviceType, osVersion, appVersion, sessionId, hasSessionId: !!sessionId });
9267
9422
  try {
9423
+ if (debug) console.log("[Paywallo EVENTS] checking install \u2014 about to read FIRST_SEEN_KEY");
9268
9424
  const firstSeen = await storageGet(FIRST_SEEN_KEY);
9269
9425
  if (!firstSeen) {
9270
9426
  await emitLifecycle({ type: "install", device_type: deviceType, os_version: osVersion, app_version: appVersion, ...sessionId ? { session_id: sessionId } : {} }, "critical");
9271
9427
  await storageSet(FIRST_SEEN_KEY, (/* @__PURE__ */ new Date()).toISOString());
9428
+ if (debug) console.log("[Paywallo EVENTS] install event emitted (first time)");
9272
9429
  if (debug) console.log("[Paywallo EVENTS] lifecycle install emitted");
9430
+ } else {
9431
+ if (debug) console.log("[Paywallo EVENTS] install skipped \u2014 firstSeen already exists", { firstSeen });
9273
9432
  }
9274
9433
  } catch (err) {
9275
9434
  if (debug) console.log("[Paywallo EVENTS] lifecycle install check failed", err);
9276
9435
  }
9277
9436
  try {
9437
+ if (debug) console.log("[Paywallo EVENTS] emitting cold_start");
9278
9438
  await emitLifecycle({ type: "cold_start", app_version: appVersion, device_type: deviceType, os_version: osVersion, ...sessionId ? { session_id: sessionId } : {} }, "normal");
9279
9439
  if (debug) console.log("[Paywallo EVENTS] lifecycle cold_start emitted");
9280
9440
  } catch (err) {
@@ -9589,9 +9749,9 @@ function subscriptionInfoToSub(s) {
9589
9749
  }
9590
9750
  async function resolveOfflineFromCache(distinctId) {
9591
9751
  try {
9592
- const cached = await subscriptionCache.get(distinctId);
9593
- if (!cached) return false;
9594
- const sub = cached.data.subscription;
9752
+ const cached2 = await subscriptionCache.get(distinctId);
9753
+ if (!cached2) return false;
9754
+ const sub = cached2.data.subscription;
9595
9755
  return isSubscriptionStillActive(
9596
9756
  sub ? { status: sub.status, expiresAt: sub.expiresAt ?? null } : null
9597
9757
  );
@@ -9661,8 +9821,8 @@ function useSubscriptionSync() {
9661
9821
  if (!apiClient || !distinctId) return false;
9662
9822
  const storeKitResult = await checkStoreKitActive(PaywalloClient.getConfig()?.debug);
9663
9823
  if (storeKitResult !== null) return storeKitResult;
9664
- const cached = cacheRef.current;
9665
- if (cached && cached.expiresAt > Date.now()) return isActiveCacheEntry(cached);
9824
+ const cached2 = cacheRef.current;
9825
+ if (cached2 && cached2.expiresAt > Date.now()) return isActiveCacheEntry(cached2);
9666
9826
  const persistedResult = await checkPersistedCache(distinctId, (sub) => setCacheAndState(sub));
9667
9827
  if (persistedResult !== null) return persistedResult;
9668
9828
  try {
@@ -9679,8 +9839,8 @@ function useSubscriptionSync() {
9679
9839
  const apiClient = PaywalloClient.getApiClient();
9680
9840
  const distinctId = PaywalloClient.getDistinctId();
9681
9841
  if (!apiClient || !distinctId) return null;
9682
- const cached = cacheRef.current;
9683
- if (cached && cached.expiresAt > Date.now()) return cached.data;
9842
+ const cached2 = cacheRef.current;
9843
+ if (cached2 && cached2.expiresAt > Date.now()) return cached2.data;
9684
9844
  try {
9685
9845
  return await fetchAndCache();
9686
9846
  } catch {
@@ -9696,6 +9856,196 @@ function useSubscriptionSync() {
9696
9856
  return { subscription, hasActiveSubscription, getSubscription, invalidateCache };
9697
9857
  }
9698
9858
 
9859
+ // src/domains/paywall/SuperwallAutoBridge.ts
9860
+ init_PaywalloClient();
9861
+
9862
+ // src/core/utils/eventId.ts
9863
+ function hashString(input) {
9864
+ let h1 = 3735928559;
9865
+ let h2 = 1103547991;
9866
+ for (let i = 0; i < input.length; i++) {
9867
+ const ch = input.charCodeAt(i);
9868
+ h1 = Math.imul(h1 ^ ch, 2654435769);
9869
+ h2 = Math.imul(h2 ^ ch, 1598716949);
9870
+ }
9871
+ h1 = Math.imul(h1 ^ h1 >>> 16, 73244475);
9872
+ h1 ^= Math.imul(h2 ^ h2 >>> 13, 351452291);
9873
+ h2 = Math.imul(h2 ^ h2 >>> 16, 73244475);
9874
+ h2 ^= Math.imul(h1 ^ h1 >>> 13, 351452291);
9875
+ const toHex = (n) => (n >>> 0).toString(16).padStart(8, "0");
9876
+ return toHex(h1) + toHex(h2);
9877
+ }
9878
+ function buildDeterministicEventId(parts) {
9879
+ const raw = hashString(parts.join("|"));
9880
+ const p1 = raw.slice(0, 8);
9881
+ const p2 = raw.slice(8, 12);
9882
+ const p3 = "4" + raw.slice(13, 16);
9883
+ const p4 = (parseInt(raw[16], 16) & 3 | 8).toString(16) + raw.slice(17, 20);
9884
+ const p5 = raw.padEnd(32, "0").slice(20, 32);
9885
+ return `${p1}-${p2}-${p3}-${p4}-${p5}`;
9886
+ }
9887
+
9888
+ // src/domains/paywall/SuperwallAutoBridge.ts
9889
+ var bridgeStarted = false;
9890
+ var unsubscribe = null;
9891
+ var debugEnabled = false;
9892
+ function log(msg, data) {
9893
+ if (!debugEnabled) return;
9894
+ if (data !== void 0) {
9895
+ console.log(`[Paywallo:SuperwallBridge] ${msg}`, data);
9896
+ } else {
9897
+ console.log(`[Paywallo:SuperwallBridge] ${msg}`);
9898
+ }
9899
+ }
9900
+ function minuteBucket(ts) {
9901
+ return String(Math.floor(ts / 6e4));
9902
+ }
9903
+ function buildEventId(type, identifier, ts) {
9904
+ return buildDeterministicEventId(["sw", type, identifier, minuteBucket(ts)]);
9905
+ }
9906
+ async function trackPaywallEvent(type, identifier, ts, extra) {
9907
+ const _eventId = buildEventId(type, identifier, ts);
9908
+ try {
9909
+ const cleanExtra = {};
9910
+ if (extra) {
9911
+ for (const [k, v] of Object.entries(extra)) if (v !== void 0) cleanExtra[k] = v;
9912
+ }
9913
+ await PaywalloClient.track("paywall", {
9914
+ priority: "critical",
9915
+ properties: {
9916
+ type,
9917
+ paywall_id: identifier,
9918
+ ...cleanExtra
9919
+ }
9920
+ });
9921
+ } catch (err) {
9922
+ log("track error", { type, identifier, eventId: _eventId, err: String(err) });
9923
+ }
9924
+ }
9925
+ function mapDismissToCloseReason(resultType) {
9926
+ switch (resultType) {
9927
+ case "purchased":
9928
+ return "purchased";
9929
+ case "restored":
9930
+ return "purchased";
9931
+ // restore counts as a purchase outcome for funnel
9932
+ case "declined":
9933
+ return "dismissed";
9934
+ default:
9935
+ return void 0;
9936
+ }
9937
+ }
9938
+ function buildCallbacks() {
9939
+ return {
9940
+ onPaywallPresent: (info) => {
9941
+ const ts = Date.now();
9942
+ log("onPaywallPresent", { identifier: info.identifier });
9943
+ void trackPaywallEvent("viewed", info.identifier, ts, {
9944
+ placement: info.presentedByEventWithName,
9945
+ variant_id: info.experiment?.variant?.id,
9946
+ campaign_id: info.experiment?.id
9947
+ });
9948
+ },
9949
+ onPaywallDismiss: (info, result) => {
9950
+ const ts = Date.now();
9951
+ log("onPaywallDismiss", { identifier: info.identifier, result: result.type });
9952
+ void trackPaywallEvent("closed", info.identifier, ts, {
9953
+ placement: info.presentedByEventWithName,
9954
+ variant_id: info.experiment?.variant?.id,
9955
+ campaign_id: info.experiment?.id,
9956
+ close_reason: mapDismissToCloseReason(result.type),
9957
+ closed_at: new Date(ts).toISOString()
9958
+ });
9959
+ },
9960
+ onPurchase: (params) => {
9961
+ const ts = Date.now();
9962
+ const safe = params !== null && typeof params === "object" ? params : {};
9963
+ const productId = "productId" in safe && typeof safe.productId === "string" ? safe.productId : "unknown";
9964
+ const identifier = "paywallIdentifier" in safe && typeof safe.paywallIdentifier === "string" ? safe.paywallIdentifier : productId;
9965
+ log("onPurchase", { productId, identifier });
9966
+ void trackPaywallEvent("purchased", identifier, ts, { product_id: productId });
9967
+ },
9968
+ // Global Superwall event listener — used specifically for `transactionComplete`
9969
+ // which is the only event that exposes the full product (currency/price) +
9970
+ // transaction (storeTransactionId) shape needed to create a server-side
9971
+ // `transactions` row. Other events are skipped (paywallOpen/Dismiss already
9972
+ // handled above with their typed callbacks).
9973
+ onSuperwallEvent: (info) => {
9974
+ const ev = info?.event;
9975
+ if (!ev || ev.event !== "transactionComplete") return;
9976
+ const ts = Date.now();
9977
+ const product = ev.product;
9978
+ const transaction = ev.transaction;
9979
+ if (!product) {
9980
+ log("transactionComplete missing product \u2014 skipping transaction track");
9981
+ return;
9982
+ }
9983
+ const productId = product.productIdentifier ?? product.id ?? "unknown";
9984
+ const currency = product.currencyCode ?? "USD";
9985
+ const price = product.price ?? 0;
9986
+ const transactionId = transaction?.storeTransactionId ?? transaction?.originalTransactionIdentifier ?? `${productId}_${String(ts)}`;
9987
+ const isTrial = product.hasFreeTrial === true || (product.trialPeriodPrice ?? 0) > 0;
9988
+ const paywallIdentifier = ev.paywallInfo?.identifier;
9989
+ log("onSuperwallEvent: transactionComplete", { productId, currency, price, transactionId, isTrial });
9990
+ void (async () => {
9991
+ try {
9992
+ await PaywalloClient.track("transaction", {
9993
+ priority: "critical",
9994
+ properties: {
9995
+ type: isTrial ? "trial_started" : "completed",
9996
+ // Server's IngestTranslator looks for `transaction_id`/`product_id`
9997
+ // /`amount`/`currency`/`paywall_id` keys (not `tx_id`). PurchaseHandler
9998
+ // then resolves type=trial/new/renewal based on amount being 0 vs >0.
9999
+ transaction_id: transactionId,
10000
+ product_id: productId,
10001
+ amount: price,
10002
+ currency,
10003
+ ...paywallIdentifier ? { paywall_id: paywallIdentifier } : {}
10004
+ }
10005
+ });
10006
+ } catch (err) {
10007
+ log("transactionComplete track error", { err: String(err) });
10008
+ }
10009
+ })();
10010
+ }
10011
+ };
10012
+ }
10013
+ function startSuperwallAutoBridge(debug = false) {
10014
+ debugEnabled = debug;
10015
+ if (bridgeStarted) {
10016
+ log("already started, skipping");
10017
+ return;
10018
+ }
10019
+ void detectAndStart();
10020
+ }
10021
+ async function detectAndStart() {
10022
+ let subscribeFn;
10023
+ try {
10024
+ const mod = await import("expo-superwall");
10025
+ if (!mod || typeof mod.subscribeToSuperwallEvents !== "function") {
10026
+ const internal = await import("expo-superwall/build/src/internal/superwallEventBridge");
10027
+ if (typeof internal.subscribeToSuperwallEvents !== "function") {
10028
+ log("expo-superwall subscribeToSuperwallEvents not found \u2014 bridge inactive");
10029
+ return;
10030
+ }
10031
+ subscribeFn = internal.subscribeToSuperwallEvents;
10032
+ } else {
10033
+ subscribeFn = mod.subscribeToSuperwallEvents;
10034
+ }
10035
+ } catch {
10036
+ log("expo-superwall not installed \u2014 bridge inactive");
10037
+ return;
10038
+ }
10039
+ try {
10040
+ const callbacks = buildCallbacks();
10041
+ unsubscribe = subscribeFn(void 0, () => callbacks);
10042
+ bridgeStarted = true;
10043
+ log("bridge started");
10044
+ } catch (err) {
10045
+ log("subscribe failed", { err: String(err) });
10046
+ }
10047
+ }
10048
+
9699
10049
  // src/PaywalloProvider.tsx
9700
10050
  init_localization();
9701
10051
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -9725,8 +10075,18 @@ function PaywalloProvider({ children, config }) {
9725
10075
  for (const [k, v] of Object.entries(props)) if (v !== void 0) flat[k] = v;
9726
10076
  return PaywalloClient.track("lifecycle", { properties: flat, priority });
9727
10077
  },
9728
- storageGet: (key) => secureStorageRef.current.get(key),
9729
- storageSet: (key, value) => secureStorageRef.current.set(key, value),
10078
+ storageGet: async (key) => {
10079
+ if (key === "@paywallo:firstSeen_v226") {
10080
+ return nativeStorage.get(key);
10081
+ }
10082
+ return secureStorageRef.current.get(key);
10083
+ },
10084
+ storageSet: async (key, value) => {
10085
+ if (key === "@paywallo:firstSeen_v226") {
10086
+ return nativeStorage.set(key, value);
10087
+ }
10088
+ return secureStorageRef.current.set(key, value);
10089
+ },
9730
10090
  debug: config.debug
9731
10091
  });
9732
10092
  const triggerRepreload = useCallback19((placement) => {
@@ -9734,7 +10094,7 @@ function PaywalloProvider({ children, config }) {
9734
10094
  });
9735
10095
  }, []);
9736
10096
  const { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady: baseHandlePreloadedWebViewReady, handlePreloadedClose: baseHandlePreloadedClose, setShowingPreloadedWebView, presentedAtRef } = usePaywallPresenter(preloadedWebView, setPreloadedWebView);
9737
- const SCREEN_HEIGHT = useMemo2(() => Dimensions4.get("window").height, []);
10097
+ const SCREEN_HEIGHT = useMemo2(() => Dimensions5.get("window").height, []);
9738
10098
  const slideAnim = useRef10(new Animated3.Value(SCREEN_HEIGHT)).current;
9739
10099
  const handlePreloadedWebViewReady = useCallback19(() => {
9740
10100
  if (PaywalloClient.getConfig()?.debug) {
@@ -9773,6 +10133,7 @@ function PaywalloProvider({ children, config }) {
9773
10133
  PaywalloClient.init(configRef.current).then(async () => {
9774
10134
  if (!mounted) return;
9775
10135
  setDistinctId(PaywalloClient.getDistinctId());
10136
+ startSuperwallAutoBridge(configRef.current.debug);
9776
10137
  setIsInitialized(true);
9777
10138
  if (!autoPreloadTriggeredRef.current) {
9778
10139
  autoPreloadTriggeredRef.current = true;