@virex-tech/paywallo-sdk 2.2.5 → 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;
@@ -2157,7 +2157,7 @@ function extractPayload(data) {
2157
2157
  }
2158
2158
  function setupForegroundHandler(deps, onReceive) {
2159
2159
  const { bridge, tracker, debug } = deps;
2160
- const unsubscribe = bridge.onMessage(async (message) => {
2160
+ const unsubscribe2 = bridge.onMessage(async (message) => {
2161
2161
  const data = message.data ?? {};
2162
2162
  const payload = extractPayload({
2163
2163
  ...data,
@@ -2175,7 +2175,7 @@ function setupForegroundHandler(deps, onReceive) {
2175
2175
  messageId
2176
2176
  });
2177
2177
  });
2178
- return unsubscribe;
2178
+ return unsubscribe2;
2179
2179
  }
2180
2180
  var init_ForegroundHandler = __esm({
2181
2181
  "src/domains/notifications/handlers/ForegroundHandler.ts"() {
@@ -2370,8 +2370,8 @@ var init_NotificationHandlerSetup = __esm({
2370
2370
  // src/core/version.ts
2371
2371
  function resolveVersion() {
2372
2372
  try {
2373
- if ("2.2.5") {
2374
- return "2.2.5";
2373
+ if ("2.2.6") {
2374
+ return "2.2.6";
2375
2375
  }
2376
2376
  } catch {
2377
2377
  }
@@ -2444,9 +2444,6 @@ function mapPermissionStatus(status) {
2444
2444
  function detectPlatform2() {
2445
2445
  return Platform6.OS === "ios" ? "ios" : "android";
2446
2446
  }
2447
- function detectEnvironment() {
2448
- return typeof __DEV__ !== "undefined" && __DEV__ ? "sandbox" : "production";
2449
- }
2450
2447
  async function getAppVersion() {
2451
2448
  try {
2452
2449
  const { collectDeviceInfo: collectDeviceInfo2 } = await Promise.resolve().then(() => (init_deviceInfo(), deviceInfo_exports));
@@ -2486,7 +2483,7 @@ async function clearPersistedToken(secureStorage2) {
2486
2483
  } catch {
2487
2484
  }
2488
2485
  }
2489
- async function buildRegistration(token, getDeviceId, getDistinctId) {
2486
+ async function buildRegistration(token, getDeviceId, getDistinctId, environment) {
2490
2487
  const appVersion = await getAppVersion();
2491
2488
  return {
2492
2489
  token,
@@ -2497,7 +2494,7 @@ async function buildRegistration(token, getDeviceId, getDistinctId) {
2497
2494
  deviceId: getDeviceId?.() ?? void 0,
2498
2495
  locale: await getLocale(),
2499
2496
  timezone: getTimezone2(),
2500
- environment: detectEnvironment()
2497
+ environment
2501
2498
  };
2502
2499
  }
2503
2500
  async function registerToken(deps, token) {
@@ -2506,7 +2503,8 @@ async function registerToken(deps, token) {
2506
2503
  if (debug) console.log("[Paywallo PUSH] httpClient not ready, skipping token registration");
2507
2504
  return;
2508
2505
  }
2509
- const registration = await buildRegistration(token, getDeviceId, getDistinctId);
2506
+ const { environment } = deps;
2507
+ const registration = await buildRegistration(token, getDeviceId, getDistinctId, environment);
2510
2508
  try {
2511
2509
  await httpClient.post(
2512
2510
  TOKEN_ENDPOINT,
@@ -2556,7 +2554,8 @@ async function registerAndPersistToken(deps, token) {
2556
2554
  appKey: deps.appKey,
2557
2555
  getDeviceId: deps.getDeviceId,
2558
2556
  getDistinctId: deps.getDistinctId,
2559
- debug: deps.debug
2557
+ debug: deps.debug,
2558
+ environment: deps.environment
2560
2559
  },
2561
2560
  token
2562
2561
  );
@@ -2573,7 +2572,8 @@ function subscribeTokenRefresh(bridge, deps, onToken) {
2573
2572
  appKey: deps.appKey,
2574
2573
  getDeviceId: deps.getDeviceId,
2575
2574
  getDistinctId: deps.getDistinctId,
2576
- debug: deps.debug
2575
+ debug: deps.debug,
2576
+ environment: deps.environment
2577
2577
  },
2578
2578
  newToken
2579
2579
  ).catch(() => void 0);
@@ -2781,6 +2781,7 @@ var init_NotificationsManager = __esm({
2781
2781
  this.getDistinctId = null;
2782
2782
  this.appKey = null;
2783
2783
  this.apiBaseUrl = null;
2784
+ this.environment = "production";
2784
2785
  this.tokenRefreshCleanup = null;
2785
2786
  this.foregroundCleanup = null;
2786
2787
  this.pendingHandlersConfig = null;
@@ -2800,6 +2801,7 @@ var init_NotificationsManager = __esm({
2800
2801
  this.secureStorage = deps.secureStorage;
2801
2802
  this.getDeviceId = deps.getDeviceId;
2802
2803
  if (deps.getDistinctId) this.getDistinctId = deps.getDistinctId;
2804
+ if (deps.environment) this.environment = deps.environment;
2803
2805
  }
2804
2806
  async initialize(config) {
2805
2807
  this.applyConfig(config);
@@ -2842,7 +2844,8 @@ var init_NotificationsManager = __esm({
2842
2844
  appKey: this.appKey,
2843
2845
  getDeviceId: this.getDeviceId,
2844
2846
  getDistinctId: this.getDistinctId,
2845
- debug: this.debug
2847
+ debug: this.debug,
2848
+ environment: this.environment
2846
2849
  };
2847
2850
  }
2848
2851
  async doRegisterToken(token) {
@@ -3914,6 +3917,32 @@ var init_IAPTransactionEmitter = __esm({
3914
3917
  if (this.getDebug()) console.log("[Paywallo PURCHASE] transaction event emission failed", err);
3915
3918
  }
3916
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
+ }
3917
3946
  /**
3918
3947
  * Forwards a native transaction update to the EventBatcher.
3919
3948
  * Public so tests can exercise the wiring without a fake NativeEventEmitter.
@@ -3936,9 +3965,9 @@ var init_IAPTransactionEmitter = __esm({
3936
3965
  product_id: update.productId
3937
3966
  };
3938
3967
  if (update.type !== "canceled") {
3939
- const cached = this.getProductFromCache(update.productId);
3940
- const amount = update.amount ?? cached?.priceValue;
3941
- 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;
3942
3971
  if (typeof amount === "number" && amount > 0) properties.amount = amount;
3943
3972
  if (currency) properties.currency = currency;
3944
3973
  }
@@ -4117,10 +4146,10 @@ var init_OfflineQueueStorage = __esm({
4117
4146
  init_NativeStorage();
4118
4147
  init_QueueJournal();
4119
4148
  OfflineQueueStorage = class {
4120
- constructor(config, emit, log) {
4149
+ constructor(config, emit, log2) {
4121
4150
  this.config = config;
4122
4151
  this.emit = emit;
4123
- this.log = log;
4152
+ this.log = log2;
4124
4153
  }
4125
4154
  async loadFromStorage(queue) {
4126
4155
  const { merged, recovered, hadJournal } = await mergeJournalIntoMain(
@@ -4739,6 +4768,7 @@ var init_QueueProcessor = __esm({
4739
4768
  for (const item of batch) {
4740
4769
  retryIds.add(item.id);
4741
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) });
4742
4772
  this.log("Event batch threw network error - will retry", {
4743
4773
  size: batch.length,
4744
4774
  error: error instanceof Error ? error.message : String(error)
@@ -4756,14 +4786,25 @@ var init_QueueProcessor = __esm({
4756
4786
  await offlineQueue.removeItem(item.id);
4757
4787
  failed++;
4758
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
+ }
4759
4798
  this.log("Event batch failed permanently (4xx) - removing from queue", {
4760
4799
  status: response.status,
4761
- size: batch.length
4800
+ size: batch.length,
4801
+ isDuplicate
4762
4802
  });
4763
4803
  } else {
4764
4804
  for (const item of batch) {
4765
4805
  retryIds.add(item.id);
4766
4806
  }
4807
+ console.error("[Paywallo:QUEUE] event batch server error (5xx) \u2014 will retry", { status: response.status, size: batch.length });
4767
4808
  this.log("Event batch failed with server error - will retry", {
4768
4809
  status: response.status,
4769
4810
  size: batch.length
@@ -4793,6 +4834,7 @@ var init_QueueProcessor = __esm({
4793
4834
  skipRetry: false
4794
4835
  });
4795
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) });
4796
4838
  this.log("Request threw network error - will retry", {
4797
4839
  id: item.id,
4798
4840
  error: error instanceof Error ? error.message : String(error)
@@ -5089,6 +5131,7 @@ var init_IAPService = __esm({
5089
5131
  this.purchaseInFlight = productId;
5090
5132
  if (this.debug) console.log(`[Paywallo PURCHASE] starting ${productId}`);
5091
5133
  try {
5134
+ void this.emitter.emitCheckoutStarted(productId);
5092
5135
  const purchase = await nativeStoreKit.purchase(productId);
5093
5136
  if (!purchase) {
5094
5137
  if (this.debug) console.log(`[Paywallo PURCHASE] ${productId} cancelled`);
@@ -5883,10 +5926,10 @@ var init_PlanService = __esm({
5883
5926
  }
5884
5927
  async getCurrentPlan(forceRefresh = false) {
5885
5928
  if (!forceRefresh) {
5886
- const cached = this.cache.get();
5887
- if (cached && !cached.isStale) {
5888
- const current = cached.data.plans.find(
5889
- (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
5890
5933
  );
5891
5934
  return current ?? null;
5892
5935
  }
@@ -5913,8 +5956,8 @@ var init_PlanService = __esm({
5913
5956
  }
5914
5957
  async getAllPlans(forceRefresh = false) {
5915
5958
  if (!forceRefresh) {
5916
- const cached = this.cache.get();
5917
- if (cached && !cached.isStale) return cached.data;
5959
+ const cached2 = this.cache.get();
5960
+ if (cached2 && !cached2.isStale) return cached2.data;
5918
5961
  }
5919
5962
  if (!this.cache.get()) {
5920
5963
  try {
@@ -6032,6 +6075,163 @@ var init_analytics = __esm({
6032
6075
  }
6033
6076
  });
6034
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
+
6035
6235
  // src/core/PaywalloAnalytics.ts
6036
6236
  function isValidEventName(name) {
6037
6237
  if (name.startsWith("$")) return /^\$[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
@@ -6040,7 +6240,7 @@ function isValidEventName(name) {
6040
6240
  async function trackEvent(apiClient, state, eventName, options) {
6041
6241
  const distinctId = identityManager.getDistinctId();
6042
6242
  if (!distinctId) {
6043
- 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 });
6044
6244
  return;
6045
6245
  }
6046
6246
  if (!isValidEventName(eventName)) {
@@ -6049,10 +6249,14 @@ async function trackEvent(apiClient, state, eventName, options) {
6049
6249
  }
6050
6250
  if (state.config?.debug && eventName.startsWith("$purchase")) console.log(`[Paywallo TRACK] ${eventName}`, options?.properties ?? {});
6051
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 } : {};
6052
6256
  await apiClient.trackEvent(
6053
6257
  eventName,
6054
6258
  distinctId,
6055
- { ...identityManager.getProperties(), ...options?.properties, ...sessionId && { sessionId } },
6259
+ { ...identityProps, ...options?.properties, ...sessionProp },
6056
6260
  options?.timestamp,
6057
6261
  options?.priority ? { priority: options.priority } : void 0
6058
6262
  );
@@ -6082,6 +6286,7 @@ var init_PaywalloAnalytics = __esm({
6082
6286
  init_identity();
6083
6287
  init_session();
6084
6288
  init_ClientError();
6289
+ init_eventFamilies();
6085
6290
  }
6086
6291
  });
6087
6292
 
@@ -6346,49 +6551,49 @@ var init_SubscriptionCache = __esm({
6346
6551
  await this.storage.remove(keyFor(distinctId));
6347
6552
  return null;
6348
6553
  }
6349
- const cached = parsed;
6350
- if (cached.data.subscription?.expiresAt) {
6351
- 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);
6352
6557
  }
6353
- if (cached.data.subscription?.originalPurchaseDate) {
6354
- cached.data.subscription.originalPurchaseDate = new Date(
6355
- cached.data.subscription.originalPurchaseDate
6558
+ if (cached2.data.subscription?.originalPurchaseDate) {
6559
+ cached2.data.subscription.originalPurchaseDate = new Date(
6560
+ cached2.data.subscription.originalPurchaseDate
6356
6561
  );
6357
6562
  }
6358
- if (cached.data.subscription?.latestPurchaseDate) {
6359
- cached.data.subscription.latestPurchaseDate = new Date(
6360
- cached.data.subscription.latestPurchaseDate
6563
+ if (cached2.data.subscription?.latestPurchaseDate) {
6564
+ cached2.data.subscription.latestPurchaseDate = new Date(
6565
+ cached2.data.subscription.latestPurchaseDate
6361
6566
  );
6362
6567
  }
6363
- if (cached.data.subscription?.cancellationDate) {
6364
- cached.data.subscription.cancellationDate = new Date(
6365
- cached.data.subscription.cancellationDate
6568
+ if (cached2.data.subscription?.cancellationDate) {
6569
+ cached2.data.subscription.cancellationDate = new Date(
6570
+ cached2.data.subscription.cancellationDate
6366
6571
  );
6367
6572
  }
6368
- if (cached.data.subscription?.gracePeriodExpiresAt) {
6369
- cached.data.subscription.gracePeriodExpiresAt = new Date(
6370
- cached.data.subscription.gracePeriodExpiresAt
6573
+ if (cached2.data.subscription?.gracePeriodExpiresAt) {
6574
+ cached2.data.subscription.gracePeriodExpiresAt = new Date(
6575
+ cached2.data.subscription.gracePeriodExpiresAt
6371
6576
  );
6372
6577
  }
6373
- cached.isStale = this.isExpired(cached);
6374
- if (!cached.isStale) {
6375
- this.memoryCache.set(distinctId, cached);
6578
+ cached2.isStale = this.isExpired(cached2);
6579
+ if (!cached2.isStale) {
6580
+ this.memoryCache.set(distinctId, cached2);
6376
6581
  }
6377
- return cached;
6582
+ return cached2;
6378
6583
  } catch {
6379
6584
  return null;
6380
6585
  }
6381
6586
  }
6382
6587
  async set(distinctId, data) {
6383
6588
  if (!distinctId) return;
6384
- const cached = {
6589
+ const cached2 = {
6385
6590
  data,
6386
6591
  cachedAt: Date.now(),
6387
6592
  isStale: false
6388
6593
  };
6389
- this.memoryCache.set(distinctId, cached);
6594
+ this.memoryCache.set(distinctId, cached2);
6390
6595
  try {
6391
- await this.storage.set(keyFor(distinctId), JSON.stringify(cached));
6596
+ await this.storage.set(keyFor(distinctId), JSON.stringify(cached2));
6392
6597
  await this.addToIndex(distinctId);
6393
6598
  } catch {
6394
6599
  }
@@ -6432,8 +6637,8 @@ var init_SubscriptionCache = __esm({
6432
6637
  ...allKeys.map((k) => this.storage.remove(keyFor(k)))
6433
6638
  ]);
6434
6639
  }
6435
- isExpired(cached) {
6436
- return Date.now() - cached.cachedAt > this.ttl;
6640
+ isExpired(cached2) {
6641
+ return Date.now() - cached2.cachedAt > this.ttl;
6437
6642
  }
6438
6643
  setTTL(ttl) {
6439
6644
  this.ttl = ttl;
@@ -6489,9 +6694,9 @@ var init_SubscriptionManager = __esm({
6489
6694
  return this.getEmptyStatus();
6490
6695
  }
6491
6696
  if (!forceRefresh) {
6492
- const cached = await this.cache.get(this.cacheKey());
6493
- if (cached && !cached.isStale) {
6494
- return cached.data;
6697
+ const cached2 = await this.cache.get(this.cacheKey());
6698
+ if (cached2 && !cached2.isStale) {
6699
+ return cached2.data;
6495
6700
  }
6496
6701
  }
6497
6702
  try {
@@ -6501,9 +6706,9 @@ var init_SubscriptionManager = __esm({
6501
6706
  return status;
6502
6707
  } catch (error) {
6503
6708
  this.log("Failed to fetch subscription status", error);
6504
- const cached = await this.cache.get(this.cacheKey());
6505
- if (cached) {
6506
- return cached.data;
6709
+ const cached2 = await this.cache.get(this.cacheKey());
6710
+ if (cached2) {
6711
+ return cached2.data;
6507
6712
  }
6508
6713
  return this.getEmptyStatus();
6509
6714
  }
@@ -6918,163 +7123,6 @@ var init_ApiClientQueue = __esm({
6918
7123
  }
6919
7124
  });
6920
7125
 
6921
- // src/core/events/schemas.ts
6922
- import { z } from "zod";
6923
- var lifecycleEventSchema, identifyEventSchema, paywallEventSchema, transactionEventSchema, onboardingEventSchema, notificationEventSchema, customEventSchema, EVENT_SCHEMAS;
6924
- var init_schemas = __esm({
6925
- "src/core/events/schemas.ts"() {
6926
- "use strict";
6927
- lifecycleEventSchema = z.object({
6928
- type: z.enum([
6929
- "install",
6930
- "cold_start",
6931
- "foreground",
6932
- "background",
6933
- "session_start",
6934
- "session_end"
6935
- ]),
6936
- session_id: z.string().optional(),
6937
- duration_s: z.number().nonnegative().optional(),
6938
- first_ever: z.boolean().optional(),
6939
- // Contexto leve transportado em lifecycle (útil pra cold_start/install)
6940
- app_version: z.string().optional(),
6941
- device_type: z.enum(["ios", "android"]).optional(),
6942
- os_version: z.string().optional(),
6943
- // Timestamps opcionais — o EventBatcher injeta `timestamp` no envelope
6944
- started_at: z.string().optional(),
6945
- ended_at: z.string().optional()
6946
- }).strict();
6947
- identifyEventSchema = z.object({
6948
- distinct_id: z.string().min(1),
6949
- email: z.email().optional(),
6950
- traits: z.record(z.string(), z.unknown()).optional(),
6951
- attribution: z.record(z.string(), z.unknown()).optional(),
6952
- device: z.record(z.string(), z.unknown()).optional()
6953
- }).strict();
6954
- paywallEventSchema = z.object({
6955
- type: z.enum(["viewed", "closed", "purchased"]),
6956
- paywall_id: z.string().min(1),
6957
- placement: z.string().optional(),
6958
- variant_id: z.string().optional(),
6959
- variant_key: z.string().optional(),
6960
- campaign_id: z.string().optional(),
6961
- // `closed`-specific
6962
- closed_at: z.string().optional(),
6963
- duration_s: z.number().nonnegative().optional(),
6964
- close_reason: z.enum(["dismiss", "cta", "purchase", "error", "timeout"]).optional(),
6965
- scroll_depth: z.number().min(0).max(1).optional(),
6966
- // `purchased`-specific (mínimo — transaction canônica leva detalhes)
6967
- product_id: z.string().optional()
6968
- }).strict();
6969
- transactionEventSchema = z.object({
6970
- type: z.enum([
6971
- "completed",
6972
- "failed",
6973
- "refunded",
6974
- "renewed",
6975
- "canceled",
6976
- "expired"
6977
- ]),
6978
- tx_id: z.string().min(1),
6979
- amount: z.number().optional(),
6980
- currency: z.string().length(3).optional(),
6981
- product_id: z.string().optional(),
6982
- subscription_id: z.string().optional(),
6983
- paywall_id: z.string().optional(),
6984
- variant_id: z.string().optional(),
6985
- reason: z.string().optional()
6986
- }).strict();
6987
- onboardingEventSchema = z.object({
6988
- type: z.enum(["step", "complete", "drop"]),
6989
- step_index: z.number().int().nonnegative().optional(),
6990
- step_name: z.string().optional(),
6991
- variant_key: z.string().optional(),
6992
- time_on_prev_s: z.number().nonnegative().optional()
6993
- }).strict();
6994
- notificationEventSchema = z.object({
6995
- type: z.enum(["delivered", "displayed", "clicked", "dismissed"]),
6996
- notification_id: z.string().optional(),
6997
- campaign_id: z.string().optional(),
6998
- variant_key: z.string().nullable().optional(),
6999
- message_id: z.string().optional(),
7000
- device_id: z.string().optional(),
7001
- push_platform: z.enum(["ios", "android"]).optional(),
7002
- timezone: z.string().optional(),
7003
- failure_reason: z.string().optional(),
7004
- app_user_id: z.string().optional()
7005
- }).strict();
7006
- customEventSchema = z.object({
7007
- name: z.string().min(1).regex(
7008
- /^(\$[a-zA-Z0-9_]+|[a-z][a-z0-9_]*)$/,
7009
- "custom event name must be snake_case or $reserved"
7010
- ),
7011
- props: z.record(z.string(), z.unknown()).optional()
7012
- }).strict();
7013
- EVENT_SCHEMAS = {
7014
- lifecycle: lifecycleEventSchema,
7015
- identify: identifyEventSchema,
7016
- paywall: paywallEventSchema,
7017
- transaction: transactionEventSchema,
7018
- onboarding: onboardingEventSchema,
7019
- notification: notificationEventSchema,
7020
- custom: customEventSchema
7021
- };
7022
- }
7023
- });
7024
-
7025
- // src/core/events/eventFamilies.ts
7026
- function isDeprecatedEvent(eventName) {
7027
- return DEPRECATED_EVENT_NAMES.includes(eventName);
7028
- }
7029
- function isCanonicalFamily(name) {
7030
- return CANONICAL_FAMILIES.includes(name);
7031
- }
7032
- function detectFamily(eventName) {
7033
- if (isCanonicalFamily(eventName) && eventName !== "custom") {
7034
- return eventName;
7035
- }
7036
- return "custom";
7037
- }
7038
- function validateEvent(eventName, properties) {
7039
- const family = detectFamily(eventName);
7040
- const schema = EVENT_SCHEMAS[family];
7041
- const input = family === "custom" ? { name: eventName, ...properties ? { props: properties } : {} } : properties ?? {};
7042
- const parsed = schema.safeParse(input);
7043
- if (parsed.success) {
7044
- return { ok: true, family, data: parsed.data };
7045
- }
7046
- return { ok: false, family, error: parsed.error };
7047
- }
7048
- function formatValidationError(error) {
7049
- return error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
7050
- }
7051
- var CANONICAL_FAMILIES, DEPRECATED_EVENT_NAMES;
7052
- var init_eventFamilies = __esm({
7053
- "src/core/events/eventFamilies.ts"() {
7054
- "use strict";
7055
- init_schemas();
7056
- CANONICAL_FAMILIES = [
7057
- "lifecycle",
7058
- "identify",
7059
- "paywall",
7060
- "transaction",
7061
- "onboarding",
7062
- "notification",
7063
- "custom"
7064
- ];
7065
- DEPRECATED_EVENT_NAMES = [
7066
- "$paywall_purchased",
7067
- "$paywall_product_selected",
7068
- "$core_action",
7069
- "$campaign_impression",
7070
- "$app_open",
7071
- "$app_background",
7072
- "$app_foreground",
7073
- "session.end"
7074
- ];
7075
- }
7076
- });
7077
-
7078
7126
  // src/core/eventEnvelopeV2.ts
7079
7127
  import { Platform as Platform13 } from "react-native";
7080
7128
  import { z as z2 } from "zod";
@@ -7104,6 +7152,9 @@ function buildV2Envelope(events, providerContext) {
7104
7152
  if (context.distinct_id === void 0 && events.length > 0) {
7105
7153
  context.distinct_id = events[0].distinctId;
7106
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
+ }
7107
7158
  return { context, events: v2Events };
7108
7159
  }
7109
7160
  function resolveProviderContext(provider) {
@@ -7155,12 +7206,18 @@ var init_eventEnvelopeV2 = __esm({
7155
7206
  session_id: z2.string().optional(),
7156
7207
  device_id: z2.string().optional(),
7157
7208
  app_version: z2.string().optional(),
7209
+ app_build: z2.string().optional(),
7210
+ bundle_id: z2.string().optional(),
7158
7211
  sdk_version: z2.string().optional(),
7159
7212
  platform: z2.string().optional(),
7160
7213
  os_version: z2.string().optional(),
7161
7214
  device_model: z2.string().optional(),
7162
7215
  timezone: z2.string().optional(),
7163
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(),
7164
7221
  attribution: z2.record(z2.string(), z2.unknown()).optional(),
7165
7222
  ids: z2.record(z2.string(), z2.unknown()).optional()
7166
7223
  });
@@ -7173,12 +7230,21 @@ var init_eventEnvelopeV2 = __esm({
7173
7230
  session_id: ["session_id", "sessionId"],
7174
7231
  device_id: ["device_id", "deviceId"],
7175
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"],
7176
7238
  sdk_version: ["sdk_version", "sdkVersion"],
7177
7239
  platform: ["platform"],
7178
7240
  os_version: ["os_version", "osVersion", "systemVersion"],
7179
7241
  device_model: ["device_model", "deviceModel", "model"],
7180
7242
  timezone: ["timezone", "timeZone"],
7181
7243
  locale: ["locale"],
7244
+ carrier: ["carrier_ctx"],
7245
+ screen_width: ["screen_width"],
7246
+ screen_height: ["screen_height"],
7247
+ screen_density: ["screen_density"],
7182
7248
  attribution: ["attribution"],
7183
7249
  ids: ["ids"]
7184
7250
  };
@@ -7305,8 +7371,8 @@ var init_EventBatcher = __esm({
7305
7371
  for (const event of drained) {
7306
7372
  try {
7307
7373
  await this.postBatch([event], `event_critical:${event.eventName}`, "critical");
7308
- } catch {
7309
- 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) });
7310
7376
  }
7311
7377
  }
7312
7378
  }
@@ -7319,8 +7385,8 @@ var init_EventBatcher = __esm({
7319
7385
  }
7320
7386
  try {
7321
7387
  await this.postBatch(batch, `event_batch:${batch.length}`);
7322
- } catch {
7323
- 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) });
7324
7390
  }
7325
7391
  }
7326
7392
  async postBatch(events, label, priority = "normal") {
@@ -7335,6 +7401,7 @@ var init_EventBatcher = __esm({
7335
7401
  const parsed = v2EnvelopeSchema.safeParse(envelope);
7336
7402
  if (!parsed.success) {
7337
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 });
7338
7405
  throw parsed.error;
7339
7406
  }
7340
7407
  await this.post(V2_BATCH_ENDPOINT, parsed.data, label, priority);
@@ -7917,7 +7984,8 @@ function setupNotifications(apiClient, config, environment, eventPipeline) {
7917
7984
  httpClient: apiClient.getHttpClient(),
7918
7985
  secureStorage: new SecureStorage(config.debug),
7919
7986
  getDeviceId: () => identityManager.getDeviceId(),
7920
- getDistinctId: () => identityManager.getDistinctId()
7987
+ getDistinctId: () => identityManager.getDistinctId(),
7988
+ environment: environment === "Sandbox" ? "sandbox" : "production"
7921
7989
  });
7922
7990
  notificationsManager.setupHandlers({ eventBatcher: eventPipeline });
7923
7991
  notificationsManager.initialize({
@@ -8001,7 +8069,38 @@ var init_PaywalloState = __esm({
8001
8069
  }
8002
8070
  });
8003
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
+
8004
8102
  // src/core/PaywalloInitializer.ts
8103
+ import { Dimensions as Dimensions4, PixelRatio as PixelRatio2 } from "react-native";
8005
8104
  async function initWithRetry(state, config) {
8006
8105
  while (state.initAttempts <= MAX_INIT_RETRIES) {
8007
8106
  try {
@@ -8080,7 +8179,13 @@ async function resolveSessionFlags(state, keys, apiClient, distinctId) {
8080
8179
  }
8081
8180
  }
8082
8181
  async function doInit(state, config) {
8083
- 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
+ }
8084
8189
  const offlineQueueEnabled = config.offlineQueueEnabled !== false;
8085
8190
  await Promise.all([
8086
8191
  networkMonitor.initialize({ debug: config.debug }),
@@ -8168,15 +8273,31 @@ async function doInit(state, config) {
8168
8273
  timezone = opts.timeZone || void 0;
8169
8274
  } catch {
8170
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
+ }
8171
8286
  return {
8172
8287
  distinct_id: identityManager.getDistinctId() || void 0,
8173
8288
  device_id: identityManager.getDeviceId() ?? void 0,
8174
8289
  session_id: sessionManager.getSessionId() ?? void 0,
8175
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,
8176
8293
  os_version: device?.systemVersion && device.systemVersion !== "0.0.0" ? device.systemVersion : void 0,
8177
8294
  device_model: device?.modelId || device?.model || void 0,
8178
8295
  timezone,
8179
8296
  locale,
8297
+ carrier: device?.carrier || void 0,
8298
+ screen_width: screenWidth,
8299
+ screen_height: screenHeight,
8300
+ screen_density: screenDensity,
8180
8301
  ...attribution && Object.keys(attribution).length > 0 && { attribution },
8181
8302
  ...ids && Object.keys(ids).length > 0 && { ids }
8182
8303
  };
@@ -8248,6 +8369,7 @@ var init_PaywalloInitializer = __esm({
8248
8369
  init_PaywalloState();
8249
8370
  init_queue();
8250
8371
  init_NativeStorage();
8372
+ init_detectEnvironment();
8251
8373
  }
8252
8374
  });
8253
8375
 
@@ -8705,8 +8827,8 @@ var init_OfferingService = __esm({
8705
8827
  this.config = config;
8706
8828
  }
8707
8829
  async getOfferings(ids) {
8708
- const cached = this.getMemCache();
8709
- 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);
8710
8832
  if (!this.cache) {
8711
8833
  try {
8712
8834
  const stored = await secureStorage.get(OFFERINGS_CACHE_KEY);
@@ -9245,9 +9367,10 @@ function useSubscription() {
9245
9367
 
9246
9368
  // src/PaywalloProvider.tsx
9247
9369
  import { useCallback as useCallback19, useEffect as useEffect12, useLayoutEffect, useMemo as useMemo2, useRef as useRef10, useState as useState16 } from "react";
9248
- 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";
9249
9371
  init_PaywalloClient();
9250
9372
  init_SecureStorage();
9373
+ init_NativeStorage();
9251
9374
  init_campaign();
9252
9375
  init_paywall();
9253
9376
  init_paywall();
@@ -9279,12 +9402,13 @@ async function detectOsVersion() {
9279
9402
  }
9280
9403
 
9281
9404
  // src/hooks/useAppAutoEvents.ts
9282
- var FIRST_SEEN_KEY = "@paywallo:firstSeen";
9405
+ var FIRST_SEEN_KEY = "@paywallo:firstSeen_v226";
9283
9406
  var DEBOUNCE_MS = 1e3;
9284
9407
  function useAppAutoEvents(config) {
9285
9408
  const { isInitialized, getAppVersion: getAppVersion2, getSessionId, emitLifecycle, storageGet, storageSet, debug } = config;
9286
9409
  const didRunRef = useRef7(false);
9287
9410
  useEffect11(() => {
9411
+ if (debug) console.log("[Paywallo EVENTS] useEffect fired", { isInitialized, didRunRefCurrent: didRunRef.current });
9288
9412
  if (!isInitialized || didRunRef.current) return;
9289
9413
  const timer = setTimeout(() => {
9290
9414
  if (didRunRef.current) return;
@@ -9294,17 +9418,23 @@ function useAppAutoEvents(config) {
9294
9418
  const appVersion = getAppVersion2();
9295
9419
  const deviceType = await detectPlatform3();
9296
9420
  const osVersion = await detectOsVersion();
9421
+ if (debug) console.log("[Paywallo EVENTS] computed device context", { deviceType, osVersion, appVersion, sessionId, hasSessionId: !!sessionId });
9297
9422
  try {
9423
+ if (debug) console.log("[Paywallo EVENTS] checking install \u2014 about to read FIRST_SEEN_KEY");
9298
9424
  const firstSeen = await storageGet(FIRST_SEEN_KEY);
9299
9425
  if (!firstSeen) {
9300
9426
  await emitLifecycle({ type: "install", device_type: deviceType, os_version: osVersion, app_version: appVersion, ...sessionId ? { session_id: sessionId } : {} }, "critical");
9301
9427
  await storageSet(FIRST_SEEN_KEY, (/* @__PURE__ */ new Date()).toISOString());
9428
+ if (debug) console.log("[Paywallo EVENTS] install event emitted (first time)");
9302
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 });
9303
9432
  }
9304
9433
  } catch (err) {
9305
9434
  if (debug) console.log("[Paywallo EVENTS] lifecycle install check failed", err);
9306
9435
  }
9307
9436
  try {
9437
+ if (debug) console.log("[Paywallo EVENTS] emitting cold_start");
9308
9438
  await emitLifecycle({ type: "cold_start", app_version: appVersion, device_type: deviceType, os_version: osVersion, ...sessionId ? { session_id: sessionId } : {} }, "normal");
9309
9439
  if (debug) console.log("[Paywallo EVENTS] lifecycle cold_start emitted");
9310
9440
  } catch (err) {
@@ -9619,9 +9749,9 @@ function subscriptionInfoToSub(s) {
9619
9749
  }
9620
9750
  async function resolveOfflineFromCache(distinctId) {
9621
9751
  try {
9622
- const cached = await subscriptionCache.get(distinctId);
9623
- if (!cached) return false;
9624
- const sub = cached.data.subscription;
9752
+ const cached2 = await subscriptionCache.get(distinctId);
9753
+ if (!cached2) return false;
9754
+ const sub = cached2.data.subscription;
9625
9755
  return isSubscriptionStillActive(
9626
9756
  sub ? { status: sub.status, expiresAt: sub.expiresAt ?? null } : null
9627
9757
  );
@@ -9691,8 +9821,8 @@ function useSubscriptionSync() {
9691
9821
  if (!apiClient || !distinctId) return false;
9692
9822
  const storeKitResult = await checkStoreKitActive(PaywalloClient.getConfig()?.debug);
9693
9823
  if (storeKitResult !== null) return storeKitResult;
9694
- const cached = cacheRef.current;
9695
- if (cached && cached.expiresAt > Date.now()) return isActiveCacheEntry(cached);
9824
+ const cached2 = cacheRef.current;
9825
+ if (cached2 && cached2.expiresAt > Date.now()) return isActiveCacheEntry(cached2);
9696
9826
  const persistedResult = await checkPersistedCache(distinctId, (sub) => setCacheAndState(sub));
9697
9827
  if (persistedResult !== null) return persistedResult;
9698
9828
  try {
@@ -9709,8 +9839,8 @@ function useSubscriptionSync() {
9709
9839
  const apiClient = PaywalloClient.getApiClient();
9710
9840
  const distinctId = PaywalloClient.getDistinctId();
9711
9841
  if (!apiClient || !distinctId) return null;
9712
- const cached = cacheRef.current;
9713
- if (cached && cached.expiresAt > Date.now()) return cached.data;
9842
+ const cached2 = cacheRef.current;
9843
+ if (cached2 && cached2.expiresAt > Date.now()) return cached2.data;
9714
9844
  try {
9715
9845
  return await fetchAndCache();
9716
9846
  } catch {
@@ -9726,6 +9856,196 @@ function useSubscriptionSync() {
9726
9856
  return { subscription, hasActiveSubscription, getSubscription, invalidateCache };
9727
9857
  }
9728
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
+
9729
10049
  // src/PaywalloProvider.tsx
9730
10050
  init_localization();
9731
10051
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -9755,8 +10075,18 @@ function PaywalloProvider({ children, config }) {
9755
10075
  for (const [k, v] of Object.entries(props)) if (v !== void 0) flat[k] = v;
9756
10076
  return PaywalloClient.track("lifecycle", { properties: flat, priority });
9757
10077
  },
9758
- storageGet: (key) => secureStorageRef.current.get(key),
9759
- 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
+ },
9760
10090
  debug: config.debug
9761
10091
  });
9762
10092
  const triggerRepreload = useCallback19((placement) => {
@@ -9764,7 +10094,7 @@ function PaywalloProvider({ children, config }) {
9764
10094
  });
9765
10095
  }, []);
9766
10096
  const { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady: baseHandlePreloadedWebViewReady, handlePreloadedClose: baseHandlePreloadedClose, setShowingPreloadedWebView, presentedAtRef } = usePaywallPresenter(preloadedWebView, setPreloadedWebView);
9767
- const SCREEN_HEIGHT = useMemo2(() => Dimensions4.get("window").height, []);
10097
+ const SCREEN_HEIGHT = useMemo2(() => Dimensions5.get("window").height, []);
9768
10098
  const slideAnim = useRef10(new Animated3.Value(SCREEN_HEIGHT)).current;
9769
10099
  const handlePreloadedWebViewReady = useCallback19(() => {
9770
10100
  if (PaywalloClient.getConfig()?.debug) {
@@ -9803,6 +10133,7 @@ function PaywalloProvider({ children, config }) {
9803
10133
  PaywalloClient.init(configRef.current).then(async () => {
9804
10134
  if (!mounted) return;
9805
10135
  setDistinctId(PaywalloClient.getDistinctId());
10136
+ startSuperwallAutoBridge(configRef.current.debug);
9806
10137
  setIsInitialized(true);
9807
10138
  if (!autoPreloadTriggeredRef.current) {
9808
10139
  autoPreloadTriggeredRef.current = true;