@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.js CHANGED
@@ -830,9 +830,9 @@ var init_CampaignGateService = __esm({
830
830
  }
831
831
  }
832
832
  getPreloadedCampaign(placement) {
833
- const cached = this.preloadedCampaigns.get(placement);
834
- if (!cached) return null;
835
- const age = Date.now() - cached.preloadedAt;
833
+ const cached2 = this.preloadedCampaigns.get(placement);
834
+ if (!cached2) return null;
835
+ const age = Date.now() - cached2.preloadedAt;
836
836
  if (age >= PRELOAD_CACHE_TTL_MS) {
837
837
  this.preloadedCampaigns.delete(placement);
838
838
  return null;
@@ -841,7 +841,7 @@ var init_CampaignGateService = __esm({
841
841
  void this.preloadCampaign(this.apiClientRef, placement).catch(() => {
842
842
  });
843
843
  }
844
- return cached.campaign;
844
+ return cached2.campaign;
845
845
  }
846
846
  async preloadAllActive(apiClient, debug) {
847
847
  this.apiClientRef = apiClient;
@@ -2173,7 +2173,7 @@ function extractPayload(data) {
2173
2173
  }
2174
2174
  function setupForegroundHandler(deps, onReceive) {
2175
2175
  const { bridge, tracker, debug } = deps;
2176
- const unsubscribe = bridge.onMessage(async (message) => {
2176
+ const unsubscribe2 = bridge.onMessage(async (message) => {
2177
2177
  const data = message.data ?? {};
2178
2178
  const payload = extractPayload({
2179
2179
  ...data,
@@ -2191,7 +2191,7 @@ function setupForegroundHandler(deps, onReceive) {
2191
2191
  messageId
2192
2192
  });
2193
2193
  });
2194
- return unsubscribe;
2194
+ return unsubscribe2;
2195
2195
  }
2196
2196
  var init_ForegroundHandler = __esm({
2197
2197
  "src/domains/notifications/handlers/ForegroundHandler.ts"() {
@@ -2386,8 +2386,8 @@ var init_NotificationHandlerSetup = __esm({
2386
2386
  // src/core/version.ts
2387
2387
  function resolveVersion() {
2388
2388
  try {
2389
- if ("2.2.5") {
2390
- return "2.2.5";
2389
+ if ("2.2.6") {
2390
+ return "2.2.6";
2391
2391
  }
2392
2392
  } catch {
2393
2393
  }
@@ -2459,9 +2459,6 @@ function mapPermissionStatus(status) {
2459
2459
  function detectPlatform2() {
2460
2460
  return import_react_native11.Platform.OS === "ios" ? "ios" : "android";
2461
2461
  }
2462
- function detectEnvironment() {
2463
- return typeof __DEV__ !== "undefined" && __DEV__ ? "sandbox" : "production";
2464
- }
2465
2462
  async function getAppVersion() {
2466
2463
  try {
2467
2464
  const { collectDeviceInfo: collectDeviceInfo2 } = await Promise.resolve().then(() => (init_deviceInfo(), deviceInfo_exports));
@@ -2502,7 +2499,7 @@ async function clearPersistedToken(secureStorage2) {
2502
2499
  } catch {
2503
2500
  }
2504
2501
  }
2505
- async function buildRegistration(token, getDeviceId, getDistinctId) {
2502
+ async function buildRegistration(token, getDeviceId, getDistinctId, environment) {
2506
2503
  const appVersion = await getAppVersion();
2507
2504
  return {
2508
2505
  token,
@@ -2513,7 +2510,7 @@ async function buildRegistration(token, getDeviceId, getDistinctId) {
2513
2510
  deviceId: getDeviceId?.() ?? void 0,
2514
2511
  locale: await getLocale(),
2515
2512
  timezone: getTimezone2(),
2516
- environment: detectEnvironment()
2513
+ environment
2517
2514
  };
2518
2515
  }
2519
2516
  async function registerToken(deps, token) {
@@ -2522,7 +2519,8 @@ async function registerToken(deps, token) {
2522
2519
  if (debug) console.log("[Paywallo PUSH] httpClient not ready, skipping token registration");
2523
2520
  return;
2524
2521
  }
2525
- const registration = await buildRegistration(token, getDeviceId, getDistinctId);
2522
+ const { environment } = deps;
2523
+ const registration = await buildRegistration(token, getDeviceId, getDistinctId, environment);
2526
2524
  try {
2527
2525
  await httpClient.post(
2528
2526
  TOKEN_ENDPOINT,
@@ -2572,7 +2570,8 @@ async function registerAndPersistToken(deps, token) {
2572
2570
  appKey: deps.appKey,
2573
2571
  getDeviceId: deps.getDeviceId,
2574
2572
  getDistinctId: deps.getDistinctId,
2575
- debug: deps.debug
2573
+ debug: deps.debug,
2574
+ environment: deps.environment
2576
2575
  },
2577
2576
  token
2578
2577
  );
@@ -2589,7 +2588,8 @@ function subscribeTokenRefresh(bridge, deps, onToken) {
2589
2588
  appKey: deps.appKey,
2590
2589
  getDeviceId: deps.getDeviceId,
2591
2590
  getDistinctId: deps.getDistinctId,
2592
- debug: deps.debug
2591
+ debug: deps.debug,
2592
+ environment: deps.environment
2593
2593
  },
2594
2594
  newToken
2595
2595
  ).catch(() => void 0);
@@ -2797,6 +2797,7 @@ var init_NotificationsManager = __esm({
2797
2797
  this.getDistinctId = null;
2798
2798
  this.appKey = null;
2799
2799
  this.apiBaseUrl = null;
2800
+ this.environment = "production";
2800
2801
  this.tokenRefreshCleanup = null;
2801
2802
  this.foregroundCleanup = null;
2802
2803
  this.pendingHandlersConfig = null;
@@ -2816,6 +2817,7 @@ var init_NotificationsManager = __esm({
2816
2817
  this.secureStorage = deps.secureStorage;
2817
2818
  this.getDeviceId = deps.getDeviceId;
2818
2819
  if (deps.getDistinctId) this.getDistinctId = deps.getDistinctId;
2820
+ if (deps.environment) this.environment = deps.environment;
2819
2821
  }
2820
2822
  async initialize(config) {
2821
2823
  this.applyConfig(config);
@@ -2858,7 +2860,8 @@ var init_NotificationsManager = __esm({
2858
2860
  appKey: this.appKey,
2859
2861
  getDeviceId: this.getDeviceId,
2860
2862
  getDistinctId: this.getDistinctId,
2861
- debug: this.debug
2863
+ debug: this.debug,
2864
+ environment: this.environment
2862
2865
  };
2863
2866
  }
2864
2867
  async doRegisterToken(token) {
@@ -3919,6 +3922,32 @@ var init_IAPTransactionEmitter = __esm({
3919
3922
  if (this.getDebug()) console.log("[Paywallo PURCHASE] transaction event emission failed", err);
3920
3923
  }
3921
3924
  }
3925
+ /**
3926
+ * Emits `checkout_started` quando o usuário inicia a compra (toca em comprar),
3927
+ * antes do StoreKit/Billing abrir — conta mesmo que a sheet nativa seja cancelada.
3928
+ * Vira InitiateCheckout no Meta/TikTok. Fire-and-forget — uma falha de tracking
3929
+ * nunca pode travar a compra.
3930
+ */
3931
+ async emitCheckoutStarted(productId) {
3932
+ try {
3933
+ const apiClient = this.getApiClientFn();
3934
+ if (!apiClient) return;
3935
+ const distinctId = PaywalloClient.getDistinctId();
3936
+ if (!distinctId) return;
3937
+ const product = this.getProductFromCache(productId);
3938
+ const properties = {
3939
+ product_id: productId,
3940
+ amount: product?.priceValue ?? 0,
3941
+ currency: product?.currency ?? "USD"
3942
+ };
3943
+ await apiClient.trackEvent("checkout_started", distinctId, properties, void 0, {
3944
+ priority: "critical"
3945
+ });
3946
+ if (this.getDebug()) console.log("[Paywallo PURCHASE] emitted checkout_started", productId);
3947
+ } catch (err) {
3948
+ if (this.getDebug()) console.log("[Paywallo PURCHASE] checkout_started emission failed", err);
3949
+ }
3950
+ }
3922
3951
  /**
3923
3952
  * Forwards a native transaction update to the EventBatcher.
3924
3953
  * Public so tests can exercise the wiring without a fake NativeEventEmitter.
@@ -3941,9 +3970,9 @@ var init_IAPTransactionEmitter = __esm({
3941
3970
  product_id: update.productId
3942
3971
  };
3943
3972
  if (update.type !== "canceled") {
3944
- const cached = this.getProductFromCache(update.productId);
3945
- const amount = update.amount ?? cached?.priceValue;
3946
- const currency = update.currency ?? cached?.currency;
3973
+ const cached2 = this.getProductFromCache(update.productId);
3974
+ const amount = update.amount ?? cached2?.priceValue;
3975
+ const currency = update.currency ?? cached2?.currency;
3947
3976
  if (typeof amount === "number" && amount > 0) properties.amount = amount;
3948
3977
  if (currency) properties.currency = currency;
3949
3978
  }
@@ -4122,10 +4151,10 @@ var init_OfflineQueueStorage = __esm({
4122
4151
  init_NativeStorage();
4123
4152
  init_QueueJournal();
4124
4153
  OfflineQueueStorage = class {
4125
- constructor(config, emit, log) {
4154
+ constructor(config, emit, log2) {
4126
4155
  this.config = config;
4127
4156
  this.emit = emit;
4128
- this.log = log;
4157
+ this.log = log2;
4129
4158
  }
4130
4159
  async loadFromStorage(queue) {
4131
4160
  const { merged, recovered, hadJournal } = await mergeJournalIntoMain(
@@ -4744,6 +4773,7 @@ var init_QueueProcessor = __esm({
4744
4773
  for (const item of batch) {
4745
4774
  retryIds.add(item.id);
4746
4775
  }
4776
+ console.error("[Paywallo:QUEUE] event batch network error \u2014 items will retry", { size: batch.length, error: error instanceof Error ? error.message : String(error) });
4747
4777
  this.log("Event batch threw network error - will retry", {
4748
4778
  size: batch.length,
4749
4779
  error: error instanceof Error ? error.message : String(error)
@@ -4761,14 +4791,25 @@ var init_QueueProcessor = __esm({
4761
4791
  await offlineQueue.removeItem(item.id);
4762
4792
  failed++;
4763
4793
  }
4794
+ const responseError = typeof response.data === "object" && response.data !== null && "error" in response.data ? response.data.error : void 0;
4795
+ const isDuplicate = typeof responseError === "string" && responseError.toUpperCase().includes("DUPLICATE");
4796
+ if (isDuplicate) {
4797
+ if (this.config.debug) {
4798
+ console.log("[Paywallo:QUEUE] event batch deduplicated by server \u2014 items dropped (benign)", { status: response.status, size: batch.length, error: responseError });
4799
+ }
4800
+ } else {
4801
+ 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) });
4802
+ }
4764
4803
  this.log("Event batch failed permanently (4xx) - removing from queue", {
4765
4804
  status: response.status,
4766
- size: batch.length
4805
+ size: batch.length,
4806
+ isDuplicate
4767
4807
  });
4768
4808
  } else {
4769
4809
  for (const item of batch) {
4770
4810
  retryIds.add(item.id);
4771
4811
  }
4812
+ console.error("[Paywallo:QUEUE] event batch server error (5xx) \u2014 will retry", { status: response.status, size: batch.length });
4772
4813
  this.log("Event batch failed with server error - will retry", {
4773
4814
  status: response.status,
4774
4815
  size: batch.length
@@ -4798,6 +4839,7 @@ var init_QueueProcessor = __esm({
4798
4839
  skipRetry: false
4799
4840
  });
4800
4841
  } catch (error) {
4842
+ 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) });
4801
4843
  this.log("Request threw network error - will retry", {
4802
4844
  id: item.id,
4803
4845
  error: error instanceof Error ? error.message : String(error)
@@ -5094,6 +5136,7 @@ var init_IAPService = __esm({
5094
5136
  this.purchaseInFlight = productId;
5095
5137
  if (this.debug) console.log(`[Paywallo PURCHASE] starting ${productId}`);
5096
5138
  try {
5139
+ void this.emitter.emitCheckoutStarted(productId);
5097
5140
  const purchase = await nativeStoreKit.purchase(productId);
5098
5141
  if (!purchase) {
5099
5142
  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
+ var import_zod, lifecycleEventSchema, identifyEventSchema, paywallEventSchema, transactionEventSchema, onboardingEventSchema, notificationEventSchema, customEventSchema, EVENT_SCHEMAS;
6080
+ var init_schemas = __esm({
6081
+ "src/core/events/schemas.ts"() {
6082
+ "use strict";
6083
+ import_zod = require("zod");
6084
+ lifecycleEventSchema = import_zod.z.object({
6085
+ type: import_zod.z.enum([
6086
+ "install",
6087
+ "cold_start",
6088
+ "foreground",
6089
+ "background",
6090
+ "session_start",
6091
+ "session_end"
6092
+ ]),
6093
+ session_id: import_zod.z.string().optional(),
6094
+ duration_s: import_zod.z.number().nonnegative().optional(),
6095
+ first_ever: import_zod.z.boolean().optional(),
6096
+ // Contexto leve transportado em lifecycle (útil pra cold_start/install)
6097
+ app_version: import_zod.z.string().optional(),
6098
+ device_type: import_zod.z.enum(["ios", "android"]).optional(),
6099
+ os_version: import_zod.z.string().optional(),
6100
+ // Timestamps opcionais — o EventBatcher injeta `timestamp` no envelope
6101
+ started_at: import_zod.z.string().optional(),
6102
+ ended_at: import_zod.z.string().optional()
6103
+ }).strict();
6104
+ identifyEventSchema = import_zod.z.object({
6105
+ distinct_id: import_zod.z.string().min(1),
6106
+ email: import_zod.z.email().optional(),
6107
+ traits: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
6108
+ attribution: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
6109
+ device: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
6110
+ }).strict();
6111
+ paywallEventSchema = import_zod.z.object({
6112
+ type: import_zod.z.enum(["viewed", "closed", "purchased"]),
6113
+ paywall_id: import_zod.z.string().min(1),
6114
+ placement: import_zod.z.string().optional(),
6115
+ variant_id: import_zod.z.string().optional(),
6116
+ variant_key: import_zod.z.string().optional(),
6117
+ campaign_id: import_zod.z.string().optional(),
6118
+ // `closed`-specific
6119
+ closed_at: import_zod.z.string().optional(),
6120
+ duration_s: import_zod.z.number().nonnegative().optional(),
6121
+ close_reason: import_zod.z.enum(["dismiss", "cta", "purchase", "error", "timeout"]).optional(),
6122
+ scroll_depth: import_zod.z.number().min(0).max(1).optional(),
6123
+ // `purchased`-specific (mínimo — transaction canônica leva detalhes)
6124
+ product_id: import_zod.z.string().optional()
6125
+ }).strict();
6126
+ transactionEventSchema = import_zod.z.object({
6127
+ type: import_zod.z.enum([
6128
+ "completed",
6129
+ "failed",
6130
+ "refunded",
6131
+ "renewed",
6132
+ "canceled",
6133
+ "expired"
6134
+ ]),
6135
+ tx_id: import_zod.z.string().min(1),
6136
+ amount: import_zod.z.number().optional(),
6137
+ currency: import_zod.z.string().length(3).optional(),
6138
+ product_id: import_zod.z.string().optional(),
6139
+ subscription_id: import_zod.z.string().optional(),
6140
+ paywall_id: import_zod.z.string().optional(),
6141
+ variant_id: import_zod.z.string().optional(),
6142
+ reason: import_zod.z.string().optional()
6143
+ }).strict();
6144
+ onboardingEventSchema = import_zod.z.object({
6145
+ type: import_zod.z.enum(["step", "complete", "drop"]),
6146
+ step_index: import_zod.z.number().int().nonnegative().optional(),
6147
+ step_name: import_zod.z.string().optional(),
6148
+ variant_key: import_zod.z.string().optional(),
6149
+ time_on_prev_s: import_zod.z.number().nonnegative().optional()
6150
+ }).strict();
6151
+ notificationEventSchema = import_zod.z.object({
6152
+ type: import_zod.z.enum(["delivered", "displayed", "clicked", "dismissed"]),
6153
+ notification_id: import_zod.z.string().optional(),
6154
+ campaign_id: import_zod.z.string().optional(),
6155
+ variant_key: import_zod.z.string().nullable().optional(),
6156
+ message_id: import_zod.z.string().optional(),
6157
+ device_id: import_zod.z.string().optional(),
6158
+ push_platform: import_zod.z.enum(["ios", "android"]).optional(),
6159
+ timezone: import_zod.z.string().optional(),
6160
+ failure_reason: import_zod.z.string().optional(),
6161
+ app_user_id: import_zod.z.string().optional()
6162
+ }).strict();
6163
+ customEventSchema = import_zod.z.object({
6164
+ name: import_zod.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: import_zod.z.record(import_zod.z.string(), import_zod.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
- var import_zod, lifecycleEventSchema, identifyEventSchema, paywallEventSchema, transactionEventSchema, onboardingEventSchema, notificationEventSchema, customEventSchema, EVENT_SCHEMAS;
6923
- var init_schemas = __esm({
6924
- "src/core/events/schemas.ts"() {
6925
- "use strict";
6926
- import_zod = require("zod");
6927
- lifecycleEventSchema = import_zod.z.object({
6928
- type: import_zod.z.enum([
6929
- "install",
6930
- "cold_start",
6931
- "foreground",
6932
- "background",
6933
- "session_start",
6934
- "session_end"
6935
- ]),
6936
- session_id: import_zod.z.string().optional(),
6937
- duration_s: import_zod.z.number().nonnegative().optional(),
6938
- first_ever: import_zod.z.boolean().optional(),
6939
- // Contexto leve transportado em lifecycle (útil pra cold_start/install)
6940
- app_version: import_zod.z.string().optional(),
6941
- device_type: import_zod.z.enum(["ios", "android"]).optional(),
6942
- os_version: import_zod.z.string().optional(),
6943
- // Timestamps opcionais — o EventBatcher injeta `timestamp` no envelope
6944
- started_at: import_zod.z.string().optional(),
6945
- ended_at: import_zod.z.string().optional()
6946
- }).strict();
6947
- identifyEventSchema = import_zod.z.object({
6948
- distinct_id: import_zod.z.string().min(1),
6949
- email: import_zod.z.email().optional(),
6950
- traits: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
6951
- attribution: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
6952
- device: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
6953
- }).strict();
6954
- paywallEventSchema = import_zod.z.object({
6955
- type: import_zod.z.enum(["viewed", "closed", "purchased"]),
6956
- paywall_id: import_zod.z.string().min(1),
6957
- placement: import_zod.z.string().optional(),
6958
- variant_id: import_zod.z.string().optional(),
6959
- variant_key: import_zod.z.string().optional(),
6960
- campaign_id: import_zod.z.string().optional(),
6961
- // `closed`-specific
6962
- closed_at: import_zod.z.string().optional(),
6963
- duration_s: import_zod.z.number().nonnegative().optional(),
6964
- close_reason: import_zod.z.enum(["dismiss", "cta", "purchase", "error", "timeout"]).optional(),
6965
- scroll_depth: import_zod.z.number().min(0).max(1).optional(),
6966
- // `purchased`-specific (mínimo — transaction canônica leva detalhes)
6967
- product_id: import_zod.z.string().optional()
6968
- }).strict();
6969
- transactionEventSchema = import_zod.z.object({
6970
- type: import_zod.z.enum([
6971
- "completed",
6972
- "failed",
6973
- "refunded",
6974
- "renewed",
6975
- "canceled",
6976
- "expired"
6977
- ]),
6978
- tx_id: import_zod.z.string().min(1),
6979
- amount: import_zod.z.number().optional(),
6980
- currency: import_zod.z.string().length(3).optional(),
6981
- product_id: import_zod.z.string().optional(),
6982
- subscription_id: import_zod.z.string().optional(),
6983
- paywall_id: import_zod.z.string().optional(),
6984
- variant_id: import_zod.z.string().optional(),
6985
- reason: import_zod.z.string().optional()
6986
- }).strict();
6987
- onboardingEventSchema = import_zod.z.object({
6988
- type: import_zod.z.enum(["step", "complete", "drop"]),
6989
- step_index: import_zod.z.number().int().nonnegative().optional(),
6990
- step_name: import_zod.z.string().optional(),
6991
- variant_key: import_zod.z.string().optional(),
6992
- time_on_prev_s: import_zod.z.number().nonnegative().optional()
6993
- }).strict();
6994
- notificationEventSchema = import_zod.z.object({
6995
- type: import_zod.z.enum(["delivered", "displayed", "clicked", "dismissed"]),
6996
- notification_id: import_zod.z.string().optional(),
6997
- campaign_id: import_zod.z.string().optional(),
6998
- variant_key: import_zod.z.string().nullable().optional(),
6999
- message_id: import_zod.z.string().optional(),
7000
- device_id: import_zod.z.string().optional(),
7001
- push_platform: import_zod.z.enum(["ios", "android"]).optional(),
7002
- timezone: import_zod.z.string().optional(),
7003
- failure_reason: import_zod.z.string().optional(),
7004
- app_user_id: import_zod.z.string().optional()
7005
- }).strict();
7006
- customEventSchema = import_zod.z.object({
7007
- name: import_zod.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: import_zod.z.record(import_zod.z.string(), import_zod.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
  function buildV2Envelope(events, providerContext) {
7080
7128
  const context = { ...providerContext };
@@ -7102,6 +7150,9 @@ function buildV2Envelope(events, providerContext) {
7102
7150
  if (context.distinct_id === void 0 && events.length > 0) {
7103
7151
  context.distinct_id = events[0].distinctId;
7104
7152
  }
7153
+ if (!context.distinct_id || context.distinct_id.length === 0) {
7154
+ console.error("[Paywallo:ENVELOPE] distinct_id is empty after resolution \u2014 envelope will fail schema", { providerContext, eventCount: events.length, firstEventName: events[0]?.eventName });
7155
+ }
7105
7156
  return { context, events: v2Events };
7106
7157
  }
7107
7158
  function resolveProviderContext(provider) {
@@ -7155,12 +7206,18 @@ var init_eventEnvelopeV2 = __esm({
7155
7206
  session_id: import_zod2.z.string().optional(),
7156
7207
  device_id: import_zod2.z.string().optional(),
7157
7208
  app_version: import_zod2.z.string().optional(),
7209
+ app_build: import_zod2.z.string().optional(),
7210
+ bundle_id: import_zod2.z.string().optional(),
7158
7211
  sdk_version: import_zod2.z.string().optional(),
7159
7212
  platform: import_zod2.z.string().optional(),
7160
7213
  os_version: import_zod2.z.string().optional(),
7161
7214
  device_model: import_zod2.z.string().optional(),
7162
7215
  timezone: import_zod2.z.string().optional(),
7163
7216
  locale: import_zod2.z.string().optional(),
7217
+ carrier: import_zod2.z.string().optional(),
7218
+ screen_width: import_zod2.z.number().optional(),
7219
+ screen_height: import_zod2.z.number().optional(),
7220
+ screen_density: import_zod2.z.number().optional(),
7164
7221
  attribution: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).optional(),
7165
7222
  ids: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.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,6 +8069,36 @@ var init_PaywalloState = __esm({
8001
8069
  }
8002
8070
  });
8003
8071
 
8072
+ // src/utils/detectEnvironment.ts
8073
+ async function detectEnvironment() {
8074
+ if (cached !== null) return cached;
8075
+ if (typeof globalThis.__DEV__ !== "undefined" && globalThis.__DEV__) {
8076
+ cached = "sandbox";
8077
+ return cached;
8078
+ }
8079
+ if (!nativeModule) {
8080
+ cached = "production";
8081
+ return cached;
8082
+ }
8083
+ try {
8084
+ const result = await nativeModule.detect();
8085
+ cached = result === "sandbox" ? "sandbox" : "production";
8086
+ return cached;
8087
+ } catch {
8088
+ cached = "production";
8089
+ return cached;
8090
+ }
8091
+ }
8092
+ var import_react_native24, nativeModule, cached;
8093
+ var init_detectEnvironment = __esm({
8094
+ "src/utils/detectEnvironment.ts"() {
8095
+ "use strict";
8096
+ import_react_native24 = require("react-native");
8097
+ nativeModule = import_react_native24.NativeModules.PaywalloEnv ?? null;
8098
+ cached = null;
8099
+ }
8100
+ });
8101
+
8004
8102
  // src/core/PaywalloInitializer.ts
8005
8103
  async function initWithRetry(state, config) {
8006
8104
  while (state.initAttempts <= MAX_INIT_RETRIES) {
@@ -8080,7 +8178,13 @@ async function resolveSessionFlags(state, keys, apiClient, distinctId) {
8080
8178
  }
8081
8179
  }
8082
8180
  async function doInit(state, config) {
8083
- const environment = config.environment ?? (typeof __DEV__ !== "undefined" && __DEV__ ? "Sandbox" : "Production");
8181
+ let environment;
8182
+ if (config.environment) {
8183
+ environment = config.environment;
8184
+ } else {
8185
+ const detected = await detectEnvironment();
8186
+ environment = detected === "sandbox" ? "Sandbox" : "Production";
8187
+ }
8084
8188
  const offlineQueueEnabled = config.offlineQueueEnabled !== false;
8085
8189
  await Promise.all([
8086
8190
  networkMonitor.initialize({ debug: config.debug }),
@@ -8168,15 +8272,31 @@ async function doInit(state, config) {
8168
8272
  timezone = opts.timeZone || void 0;
8169
8273
  } catch {
8170
8274
  }
8275
+ let screenWidth;
8276
+ let screenHeight;
8277
+ let screenDensity;
8278
+ try {
8279
+ const screen = import_react_native25.Dimensions.get("screen");
8280
+ screenWidth = Math.round(screen.width) || void 0;
8281
+ screenHeight = Math.round(screen.height) || void 0;
8282
+ screenDensity = import_react_native25.PixelRatio.get() || void 0;
8283
+ } catch {
8284
+ }
8171
8285
  return {
8172
8286
  distinct_id: identityManager.getDistinctId() || void 0,
8173
8287
  device_id: identityManager.getDeviceId() ?? void 0,
8174
8288
  session_id: sessionManager.getSessionId() ?? void 0,
8175
8289
  app_version: device?.appVersion && device.appVersion !== "0.0.0" ? device.appVersion : void 0,
8290
+ app_build: device?.buildNumber && device.buildNumber !== "0" ? device.buildNumber : void 0,
8291
+ bundle_id: device?.bundleId || void 0,
8176
8292
  os_version: device?.systemVersion && device.systemVersion !== "0.0.0" ? device.systemVersion : void 0,
8177
8293
  device_model: device?.modelId || device?.model || void 0,
8178
8294
  timezone,
8179
8295
  locale,
8296
+ carrier: device?.carrier || void 0,
8297
+ screen_width: screenWidth,
8298
+ screen_height: screenHeight,
8299
+ screen_density: screenDensity,
8180
8300
  ...attribution && Object.keys(attribution).length > 0 && { attribution },
8181
8301
  ...ids && Object.keys(ids).length > 0 && { ids }
8182
8302
  };
@@ -8226,9 +8346,11 @@ async function doInit(state, config) {
8226
8346
  setupAutoPreload(state, apiClient, config, distinctId);
8227
8347
  startBatchPreload(apiClient, config.debug);
8228
8348
  }
8349
+ var import_react_native25;
8229
8350
  var init_PaywalloInitializer = __esm({
8230
8351
  "src/core/PaywalloInitializer.ts"() {
8231
8352
  "use strict";
8353
+ import_react_native25 = require("react-native");
8232
8354
  init_identity();
8233
8355
  init_AdvertisingIdManager();
8234
8356
  init_DeepLinkAttributionCapture();
@@ -8248,6 +8370,7 @@ var init_PaywalloInitializer = __esm({
8248
8370
  init_PaywalloState();
8249
8371
  init_queue();
8250
8372
  init_NativeStorage();
8373
+ init_detectEnvironment();
8251
8374
  }
8252
8375
  });
8253
8376
 
@@ -8652,7 +8775,7 @@ __export(OfferingService_exports, {
8652
8775
  offeringService: () => offeringService
8653
8776
  });
8654
8777
  function resolveProductId(raw) {
8655
- const isIos = import_react_native24.Platform.OS === "ios";
8778
+ const isIos = import_react_native26.Platform.OS === "ios";
8656
8779
  const productId = isIos ? raw.apple_product_id : raw.google_product_id;
8657
8780
  return productId ?? "";
8658
8781
  }
@@ -8671,7 +8794,7 @@ function mapProduct2(raw) {
8671
8794
  };
8672
8795
  }
8673
8796
  function pickPlatformProduct(products) {
8674
- const isIos = import_react_native24.Platform.OS === "ios";
8797
+ const isIos = import_react_native26.Platform.OS === "ios";
8675
8798
  const match = products.find((p) => isIos ? p.apple_product_id : p.google_product_id);
8676
8799
  return match ?? products[0] ?? null;
8677
8800
  }
@@ -8687,11 +8810,11 @@ function mapOffering(raw) {
8687
8810
  product: mapProduct2(rawProduct)
8688
8811
  };
8689
8812
  }
8690
- var import_react_native24, OFFERINGS_CACHE_KEY, DEFAULT_TTL3, OfferingService, offeringService;
8813
+ var import_react_native26, OFFERINGS_CACHE_KEY, DEFAULT_TTL3, OfferingService, offeringService;
8691
8814
  var init_OfferingService = __esm({
8692
8815
  "src/domains/offering/OfferingService.ts"() {
8693
8816
  "use strict";
8694
- import_react_native24 = require("react-native");
8817
+ import_react_native26 = require("react-native");
8695
8818
  init_SecureStorage();
8696
8819
  OFFERINGS_CACHE_KEY = "@paywallo:offerings_cache";
8697
8820
  DEFAULT_TTL3 = 5 * 60 * 1e3;
@@ -8705,8 +8828,8 @@ var init_OfferingService = __esm({
8705
8828
  this.config = config;
8706
8829
  }
8707
8830
  async getOfferings(ids) {
8708
- const cached = this.getMemCache();
8709
- if (cached && !cached.isStale) return this.filterByIds(cached.data, ids);
8831
+ const cached2 = this.getMemCache();
8832
+ if (cached2 && !cached2.isStale) return this.filterByIds(cached2.data, ids);
8710
8833
  if (!this.cache) {
8711
8834
  try {
8712
8835
  const stored = await secureStorage.get(OFFERINGS_CACHE_KEY);
@@ -9322,9 +9445,10 @@ function useSubscription() {
9322
9445
 
9323
9446
  // src/PaywalloProvider.tsx
9324
9447
  var import_react25 = require("react");
9325
- var import_react_native25 = require("react-native");
9448
+ var import_react_native27 = require("react-native");
9326
9449
  init_PaywalloClient();
9327
9450
  init_SecureStorage();
9451
+ init_NativeStorage();
9328
9452
  init_campaign();
9329
9453
  init_paywall();
9330
9454
  init_paywall();
@@ -9356,12 +9480,13 @@ async function detectOsVersion() {
9356
9480
  }
9357
9481
 
9358
9482
  // src/hooks/useAppAutoEvents.ts
9359
- var FIRST_SEEN_KEY = "@paywallo:firstSeen";
9483
+ var FIRST_SEEN_KEY = "@paywallo:firstSeen_v226";
9360
9484
  var DEBOUNCE_MS = 1e3;
9361
9485
  function useAppAutoEvents(config) {
9362
9486
  const { isInitialized, getAppVersion: getAppVersion2, getSessionId, emitLifecycle, storageGet, storageSet, debug } = config;
9363
9487
  const didRunRef = (0, import_react20.useRef)(false);
9364
9488
  (0, import_react20.useEffect)(() => {
9489
+ if (debug) console.log("[Paywallo EVENTS] useEffect fired", { isInitialized, didRunRefCurrent: didRunRef.current });
9365
9490
  if (!isInitialized || didRunRef.current) return;
9366
9491
  const timer = setTimeout(() => {
9367
9492
  if (didRunRef.current) return;
@@ -9371,17 +9496,23 @@ function useAppAutoEvents(config) {
9371
9496
  const appVersion = getAppVersion2();
9372
9497
  const deviceType = await detectPlatform3();
9373
9498
  const osVersion = await detectOsVersion();
9499
+ if (debug) console.log("[Paywallo EVENTS] computed device context", { deviceType, osVersion, appVersion, sessionId, hasSessionId: !!sessionId });
9374
9500
  try {
9501
+ if (debug) console.log("[Paywallo EVENTS] checking install \u2014 about to read FIRST_SEEN_KEY");
9375
9502
  const firstSeen = await storageGet(FIRST_SEEN_KEY);
9376
9503
  if (!firstSeen) {
9377
9504
  await emitLifecycle({ type: "install", device_type: deviceType, os_version: osVersion, app_version: appVersion, ...sessionId ? { session_id: sessionId } : {} }, "critical");
9378
9505
  await storageSet(FIRST_SEEN_KEY, (/* @__PURE__ */ new Date()).toISOString());
9506
+ if (debug) console.log("[Paywallo EVENTS] install event emitted (first time)");
9379
9507
  if (debug) console.log("[Paywallo EVENTS] lifecycle install emitted");
9508
+ } else {
9509
+ if (debug) console.log("[Paywallo EVENTS] install skipped \u2014 firstSeen already exists", { firstSeen });
9380
9510
  }
9381
9511
  } catch (err) {
9382
9512
  if (debug) console.log("[Paywallo EVENTS] lifecycle install check failed", err);
9383
9513
  }
9384
9514
  try {
9515
+ if (debug) console.log("[Paywallo EVENTS] emitting cold_start");
9385
9516
  await emitLifecycle({ type: "cold_start", app_version: appVersion, device_type: deviceType, os_version: osVersion, ...sessionId ? { session_id: sessionId } : {} }, "normal");
9386
9517
  if (debug) console.log("[Paywallo EVENTS] lifecycle cold_start emitted");
9387
9518
  } catch (err) {
@@ -9696,9 +9827,9 @@ function subscriptionInfoToSub(s) {
9696
9827
  }
9697
9828
  async function resolveOfflineFromCache(distinctId) {
9698
9829
  try {
9699
- const cached = await subscriptionCache.get(distinctId);
9700
- if (!cached) return false;
9701
- const sub = cached.data.subscription;
9830
+ const cached2 = await subscriptionCache.get(distinctId);
9831
+ if (!cached2) return false;
9832
+ const sub = cached2.data.subscription;
9702
9833
  return isSubscriptionStillActive(
9703
9834
  sub ? { status: sub.status, expiresAt: sub.expiresAt ?? null } : null
9704
9835
  );
@@ -9768,8 +9899,8 @@ function useSubscriptionSync() {
9768
9899
  if (!apiClient || !distinctId) return false;
9769
9900
  const storeKitResult = await checkStoreKitActive(PaywalloClient.getConfig()?.debug);
9770
9901
  if (storeKitResult !== null) return storeKitResult;
9771
- const cached = cacheRef.current;
9772
- if (cached && cached.expiresAt > Date.now()) return isActiveCacheEntry(cached);
9902
+ const cached2 = cacheRef.current;
9903
+ if (cached2 && cached2.expiresAt > Date.now()) return isActiveCacheEntry(cached2);
9773
9904
  const persistedResult = await checkPersistedCache(distinctId, (sub) => setCacheAndState(sub));
9774
9905
  if (persistedResult !== null) return persistedResult;
9775
9906
  try {
@@ -9786,8 +9917,8 @@ function useSubscriptionSync() {
9786
9917
  const apiClient = PaywalloClient.getApiClient();
9787
9918
  const distinctId = PaywalloClient.getDistinctId();
9788
9919
  if (!apiClient || !distinctId) return null;
9789
- const cached = cacheRef.current;
9790
- if (cached && cached.expiresAt > Date.now()) return cached.data;
9920
+ const cached2 = cacheRef.current;
9921
+ if (cached2 && cached2.expiresAt > Date.now()) return cached2.data;
9791
9922
  try {
9792
9923
  return await fetchAndCache();
9793
9924
  } catch {
@@ -9803,6 +9934,196 @@ function useSubscriptionSync() {
9803
9934
  return { subscription, hasActiveSubscription, getSubscription, invalidateCache };
9804
9935
  }
9805
9936
 
9937
+ // src/domains/paywall/SuperwallAutoBridge.ts
9938
+ init_PaywalloClient();
9939
+
9940
+ // src/core/utils/eventId.ts
9941
+ function hashString(input) {
9942
+ let h1 = 3735928559;
9943
+ let h2 = 1103547991;
9944
+ for (let i = 0; i < input.length; i++) {
9945
+ const ch = input.charCodeAt(i);
9946
+ h1 = Math.imul(h1 ^ ch, 2654435769);
9947
+ h2 = Math.imul(h2 ^ ch, 1598716949);
9948
+ }
9949
+ h1 = Math.imul(h1 ^ h1 >>> 16, 73244475);
9950
+ h1 ^= Math.imul(h2 ^ h2 >>> 13, 351452291);
9951
+ h2 = Math.imul(h2 ^ h2 >>> 16, 73244475);
9952
+ h2 ^= Math.imul(h1 ^ h1 >>> 13, 351452291);
9953
+ const toHex = (n) => (n >>> 0).toString(16).padStart(8, "0");
9954
+ return toHex(h1) + toHex(h2);
9955
+ }
9956
+ function buildDeterministicEventId(parts) {
9957
+ const raw = hashString(parts.join("|"));
9958
+ const p1 = raw.slice(0, 8);
9959
+ const p2 = raw.slice(8, 12);
9960
+ const p3 = "4" + raw.slice(13, 16);
9961
+ const p4 = (parseInt(raw[16], 16) & 3 | 8).toString(16) + raw.slice(17, 20);
9962
+ const p5 = raw.padEnd(32, "0").slice(20, 32);
9963
+ return `${p1}-${p2}-${p3}-${p4}-${p5}`;
9964
+ }
9965
+
9966
+ // src/domains/paywall/SuperwallAutoBridge.ts
9967
+ var bridgeStarted = false;
9968
+ var unsubscribe = null;
9969
+ var debugEnabled = false;
9970
+ function log(msg, data) {
9971
+ if (!debugEnabled) return;
9972
+ if (data !== void 0) {
9973
+ console.log(`[Paywallo:SuperwallBridge] ${msg}`, data);
9974
+ } else {
9975
+ console.log(`[Paywallo:SuperwallBridge] ${msg}`);
9976
+ }
9977
+ }
9978
+ function minuteBucket(ts) {
9979
+ return String(Math.floor(ts / 6e4));
9980
+ }
9981
+ function buildEventId(type, identifier, ts) {
9982
+ return buildDeterministicEventId(["sw", type, identifier, minuteBucket(ts)]);
9983
+ }
9984
+ async function trackPaywallEvent(type, identifier, ts, extra) {
9985
+ const _eventId = buildEventId(type, identifier, ts);
9986
+ try {
9987
+ const cleanExtra = {};
9988
+ if (extra) {
9989
+ for (const [k, v] of Object.entries(extra)) if (v !== void 0) cleanExtra[k] = v;
9990
+ }
9991
+ await PaywalloClient.track("paywall", {
9992
+ priority: "critical",
9993
+ properties: {
9994
+ type,
9995
+ paywall_id: identifier,
9996
+ ...cleanExtra
9997
+ }
9998
+ });
9999
+ } catch (err) {
10000
+ log("track error", { type, identifier, eventId: _eventId, err: String(err) });
10001
+ }
10002
+ }
10003
+ function mapDismissToCloseReason(resultType) {
10004
+ switch (resultType) {
10005
+ case "purchased":
10006
+ return "purchased";
10007
+ case "restored":
10008
+ return "purchased";
10009
+ // restore counts as a purchase outcome for funnel
10010
+ case "declined":
10011
+ return "dismissed";
10012
+ default:
10013
+ return void 0;
10014
+ }
10015
+ }
10016
+ function buildCallbacks() {
10017
+ return {
10018
+ onPaywallPresent: (info) => {
10019
+ const ts = Date.now();
10020
+ log("onPaywallPresent", { identifier: info.identifier });
10021
+ void trackPaywallEvent("viewed", info.identifier, ts, {
10022
+ placement: info.presentedByEventWithName,
10023
+ variant_id: info.experiment?.variant?.id,
10024
+ campaign_id: info.experiment?.id
10025
+ });
10026
+ },
10027
+ onPaywallDismiss: (info, result) => {
10028
+ const ts = Date.now();
10029
+ log("onPaywallDismiss", { identifier: info.identifier, result: result.type });
10030
+ void trackPaywallEvent("closed", info.identifier, ts, {
10031
+ placement: info.presentedByEventWithName,
10032
+ variant_id: info.experiment?.variant?.id,
10033
+ campaign_id: info.experiment?.id,
10034
+ close_reason: mapDismissToCloseReason(result.type),
10035
+ closed_at: new Date(ts).toISOString()
10036
+ });
10037
+ },
10038
+ onPurchase: (params) => {
10039
+ const ts = Date.now();
10040
+ const safe = params !== null && typeof params === "object" ? params : {};
10041
+ const productId = "productId" in safe && typeof safe.productId === "string" ? safe.productId : "unknown";
10042
+ const identifier = "paywallIdentifier" in safe && typeof safe.paywallIdentifier === "string" ? safe.paywallIdentifier : productId;
10043
+ log("onPurchase", { productId, identifier });
10044
+ void trackPaywallEvent("purchased", identifier, ts, { product_id: productId });
10045
+ },
10046
+ // Global Superwall event listener — used specifically for `transactionComplete`
10047
+ // which is the only event that exposes the full product (currency/price) +
10048
+ // transaction (storeTransactionId) shape needed to create a server-side
10049
+ // `transactions` row. Other events are skipped (paywallOpen/Dismiss already
10050
+ // handled above with their typed callbacks).
10051
+ onSuperwallEvent: (info) => {
10052
+ const ev = info?.event;
10053
+ if (!ev || ev.event !== "transactionComplete") return;
10054
+ const ts = Date.now();
10055
+ const product = ev.product;
10056
+ const transaction = ev.transaction;
10057
+ if (!product) {
10058
+ log("transactionComplete missing product \u2014 skipping transaction track");
10059
+ return;
10060
+ }
10061
+ const productId = product.productIdentifier ?? product.id ?? "unknown";
10062
+ const currency = product.currencyCode ?? "USD";
10063
+ const price = product.price ?? 0;
10064
+ const transactionId = transaction?.storeTransactionId ?? transaction?.originalTransactionIdentifier ?? `${productId}_${String(ts)}`;
10065
+ const isTrial = product.hasFreeTrial === true || (product.trialPeriodPrice ?? 0) > 0;
10066
+ const paywallIdentifier = ev.paywallInfo?.identifier;
10067
+ log("onSuperwallEvent: transactionComplete", { productId, currency, price, transactionId, isTrial });
10068
+ void (async () => {
10069
+ try {
10070
+ await PaywalloClient.track("transaction", {
10071
+ priority: "critical",
10072
+ properties: {
10073
+ type: isTrial ? "trial_started" : "completed",
10074
+ // Server's IngestTranslator looks for `transaction_id`/`product_id`
10075
+ // /`amount`/`currency`/`paywall_id` keys (not `tx_id`). PurchaseHandler
10076
+ // then resolves type=trial/new/renewal based on amount being 0 vs >0.
10077
+ transaction_id: transactionId,
10078
+ product_id: productId,
10079
+ amount: price,
10080
+ currency,
10081
+ ...paywallIdentifier ? { paywall_id: paywallIdentifier } : {}
10082
+ }
10083
+ });
10084
+ } catch (err) {
10085
+ log("transactionComplete track error", { err: String(err) });
10086
+ }
10087
+ })();
10088
+ }
10089
+ };
10090
+ }
10091
+ function startSuperwallAutoBridge(debug = false) {
10092
+ debugEnabled = debug;
10093
+ if (bridgeStarted) {
10094
+ log("already started, skipping");
10095
+ return;
10096
+ }
10097
+ void detectAndStart();
10098
+ }
10099
+ async function detectAndStart() {
10100
+ let subscribeFn;
10101
+ try {
10102
+ const mod = await import("expo-superwall");
10103
+ if (!mod || typeof mod.subscribeToSuperwallEvents !== "function") {
10104
+ const internal = await import("expo-superwall/build/src/internal/superwallEventBridge");
10105
+ if (typeof internal.subscribeToSuperwallEvents !== "function") {
10106
+ log("expo-superwall subscribeToSuperwallEvents not found \u2014 bridge inactive");
10107
+ return;
10108
+ }
10109
+ subscribeFn = internal.subscribeToSuperwallEvents;
10110
+ } else {
10111
+ subscribeFn = mod.subscribeToSuperwallEvents;
10112
+ }
10113
+ } catch {
10114
+ log("expo-superwall not installed \u2014 bridge inactive");
10115
+ return;
10116
+ }
10117
+ try {
10118
+ const callbacks = buildCallbacks();
10119
+ unsubscribe = subscribeFn(void 0, () => callbacks);
10120
+ bridgeStarted = true;
10121
+ log("bridge started");
10122
+ } catch (err) {
10123
+ log("subscribe failed", { err: String(err) });
10124
+ }
10125
+ }
10126
+
9806
10127
  // src/PaywalloProvider.tsx
9807
10128
  init_localization();
9808
10129
  var import_jsx_runtime3 = require("react/jsx-runtime");
@@ -9832,8 +10153,18 @@ function PaywalloProvider({ children, config }) {
9832
10153
  for (const [k, v] of Object.entries(props)) if (v !== void 0) flat[k] = v;
9833
10154
  return PaywalloClient.track("lifecycle", { properties: flat, priority });
9834
10155
  },
9835
- storageGet: (key) => secureStorageRef.current.get(key),
9836
- storageSet: (key, value) => secureStorageRef.current.set(key, value),
10156
+ storageGet: async (key) => {
10157
+ if (key === "@paywallo:firstSeen_v226") {
10158
+ return nativeStorage.get(key);
10159
+ }
10160
+ return secureStorageRef.current.get(key);
10161
+ },
10162
+ storageSet: async (key, value) => {
10163
+ if (key === "@paywallo:firstSeen_v226") {
10164
+ return nativeStorage.set(key, value);
10165
+ }
10166
+ return secureStorageRef.current.set(key, value);
10167
+ },
9837
10168
  debug: config.debug
9838
10169
  });
9839
10170
  const triggerRepreload = (0, import_react25.useCallback)((placement) => {
@@ -9841,8 +10172,8 @@ function PaywalloProvider({ children, config }) {
9841
10172
  });
9842
10173
  }, []);
9843
10174
  const { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady: baseHandlePreloadedWebViewReady, handlePreloadedClose: baseHandlePreloadedClose, setShowingPreloadedWebView, presentedAtRef } = usePaywallPresenter(preloadedWebView, setPreloadedWebView);
9844
- const SCREEN_HEIGHT = (0, import_react25.useMemo)(() => import_react_native25.Dimensions.get("window").height, []);
9845
- const slideAnim = (0, import_react25.useRef)(new import_react_native25.Animated.Value(SCREEN_HEIGHT)).current;
10175
+ const SCREEN_HEIGHT = (0, import_react25.useMemo)(() => import_react_native27.Dimensions.get("window").height, []);
10176
+ const slideAnim = (0, import_react25.useRef)(new import_react_native27.Animated.Value(SCREEN_HEIGHT)).current;
9846
10177
  const handlePreloadedWebViewReady = (0, import_react25.useCallback)(() => {
9847
10178
  if (PaywalloClient.getConfig()?.debug) {
9848
10179
  console.log(`[Paywallo WEBVIEW] loaded \u2014 placement: ${preloadedWebView?.placement}`);
@@ -9851,7 +10182,7 @@ function PaywalloProvider({ children, config }) {
9851
10182
  }, [preloadedWebView?.placement, baseHandlePreloadedWebViewReady]);
9852
10183
  const handlePreloadedClose = (0, import_react25.useCallback)(() => {
9853
10184
  const placement = preloadedWebView?.placement;
9854
- import_react_native25.Animated.timing(slideAnim, { toValue: SCREEN_HEIGHT, duration: 200, easing: import_react_native25.Easing.in(import_react_native25.Easing.cubic), useNativeDriver: true }).start(() => {
10185
+ import_react_native27.Animated.timing(slideAnim, { toValue: SCREEN_HEIGHT, duration: 200, easing: import_react_native27.Easing.in(import_react_native27.Easing.cubic), useNativeDriver: true }).start(() => {
9855
10186
  baseHandlePreloadedClose();
9856
10187
  if (placement) triggerRepreload(placement);
9857
10188
  });
@@ -9880,6 +10211,7 @@ function PaywalloProvider({ children, config }) {
9880
10211
  PaywalloClient.init(configRef.current).then(async () => {
9881
10212
  if (!mounted) return;
9882
10213
  setDistinctId(PaywalloClient.getDistinctId());
10214
+ startSuperwallAutoBridge(configRef.current.debug);
9883
10215
  setIsInitialized(true);
9884
10216
  if (!autoPreloadTriggeredRef.current) {
9885
10217
  autoPreloadTriggeredRef.current = true;
@@ -10100,7 +10432,7 @@ function PaywalloProvider({ children, config }) {
10100
10432
  (0, import_react25.useLayoutEffect)(() => {
10101
10433
  if (showingPreloadedWebView) {
10102
10434
  slideAnim.setValue(SCREEN_HEIGHT);
10103
- import_react_native25.Animated.timing(slideAnim, { toValue: 0, duration: 200, easing: import_react_native25.Easing.out(import_react_native25.Easing.cubic), useNativeDriver: true }).start();
10435
+ import_react_native27.Animated.timing(slideAnim, { toValue: 0, duration: 200, easing: import_react_native27.Easing.out(import_react_native27.Easing.cubic), useNativeDriver: true }).start();
10104
10436
  }
10105
10437
  }, [showingPreloadedWebView, slideAnim, SCREEN_HEIGHT]);
10106
10438
  const preloadStyle = [
@@ -10110,7 +10442,7 @@ function PaywalloProvider({ children, config }) {
10110
10442
  ];
10111
10443
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PaywalloContext.Provider, { value: contextValue, children: [
10112
10444
  children,
10113
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PaywallErrorBoundary, { children: !!preloadedWebView?.craftData && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native25.Animated.View, { style: preloadStyle, pointerEvents: showingPreloadedWebView ? "auto" : "none", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
10445
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PaywallErrorBoundary, { children: !!preloadedWebView?.craftData && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native27.Animated.View, { style: preloadStyle, pointerEvents: showingPreloadedWebView ? "auto" : "none", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
10114
10446
  PaywallWebView,
10115
10447
  {
10116
10448
  paywallId: preloadedWebView.paywallId,
@@ -10141,7 +10473,7 @@ function PaywalloProvider({ children, config }) {
10141
10473
  ) })
10142
10474
  ] });
10143
10475
  }
10144
- var styles3 = import_react_native25.StyleSheet.create({
10476
+ var styles3 = import_react_native27.StyleSheet.create({
10145
10477
  preloadOverlay: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "#ffffff" },
10146
10478
  hidden: { zIndex: -1, opacity: 0, width: 1, height: 1, overflow: "hidden" },
10147
10479
  visible: { zIndex: 9999, opacity: 1 }