@virex-tech/paywallo-sdk 2.5.2 → 2.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2627,8 +2627,8 @@ var init_NotificationHandlerSetup = __esm({
2627
2627
  // src/core/version.ts
2628
2628
  function resolveVersion() {
2629
2629
  try {
2630
- if ("2.5.2") {
2631
- return "2.5.2";
2630
+ if ("2.5.3") {
2631
+ return "2.5.3";
2632
2632
  }
2633
2633
  } catch {
2634
2634
  }
@@ -4111,17 +4111,292 @@ var init_PaywallWebView = __esm({
4111
4111
  }
4112
4112
  });
4113
4113
 
4114
+ // src/domains/iap/PurchaseError.ts
4115
+ function createPurchaseError(code, message) {
4116
+ return new PurchaseError(
4117
+ PURCHASE_ERROR_CODES[code],
4118
+ message ?? DEFAULT_MESSAGES[code] ?? "Unknown purchase error",
4119
+ code === "USER_CANCELLED"
4120
+ );
4121
+ }
4122
+ var PurchaseError, PURCHASE_ERROR_CODES, DEFAULT_MESSAGES;
4123
+ var init_PurchaseError = __esm({
4124
+ "src/domains/iap/PurchaseError.ts"() {
4125
+ "use strict";
4126
+ init_PaywalloError();
4127
+ PurchaseError = class extends PaywalloError {
4128
+ constructor(code, message, userCancelled = false, httpStatus) {
4129
+ super("purchase", code, message);
4130
+ this.name = "PurchaseError";
4131
+ this.userCancelled = userCancelled;
4132
+ this.httpStatus = httpStatus;
4133
+ }
4134
+ };
4135
+ PURCHASE_ERROR_CODES = {
4136
+ NOT_INITIALIZED: "PURCHASE_NOT_INITIALIZED",
4137
+ PRODUCT_NOT_FOUND: "PRODUCT_NOT_FOUND",
4138
+ PURCHASE_FAILED: "PURCHASE_FAILED",
4139
+ RESTORE_FAILED: "RESTORE_FAILED",
4140
+ VALIDATION_FAILED: "VALIDATION_FAILED",
4141
+ USER_CANCELLED: "USER_CANCELLED",
4142
+ NETWORK_ERROR: "PURCHASE_NETWORK_ERROR",
4143
+ STORE_ERROR: "PURCHASE_STORE_ERROR",
4144
+ PENDING_PURCHASE: "PURCHASE_PENDING",
4145
+ DEFERRED_PURCHASE: "PURCHASE_DEFERRED",
4146
+ STORE_NOT_AVAILABLE: "PURCHASE_STORE_NOT_AVAILABLE"
4147
+ };
4148
+ DEFAULT_MESSAGES = {
4149
+ NOT_INITIALIZED: "Purchase controller not initialized. Call init() first.",
4150
+ PRODUCT_NOT_FOUND: "Product not found in store.",
4151
+ PURCHASE_FAILED: "Purchase failed. Please try again.",
4152
+ RESTORE_FAILED: "Failed to restore purchases. Please try again.",
4153
+ VALIDATION_FAILED: "Failed to validate purchase with server.",
4154
+ USER_CANCELLED: "Purchase was cancelled.",
4155
+ NETWORK_ERROR: "Network error. Please check your connection.",
4156
+ STORE_ERROR: "Store error. Please try again later.",
4157
+ PENDING_PURCHASE: "Purchase is pending approval.",
4158
+ DEFERRED_PURCHASE: "Purchase is deferred (e.g., parental approval required).",
4159
+ STORE_NOT_AVAILABLE: "In-app purchases are not available on this device."
4160
+ };
4161
+ }
4162
+ });
4163
+
4164
+ // src/domains/iap/NativeStoreKit.ts
4165
+ var NativeStoreKit_exports = {};
4166
+ __export(NativeStoreKit_exports, {
4167
+ NativeStoreKit: () => NativeStoreKit,
4168
+ nativeStoreKit: () => nativeStoreKit
4169
+ });
4170
+ function isAvailable3() {
4171
+ return PaywalloStoreKitNative != null;
4172
+ }
4173
+ function mapNativeProduct(native) {
4174
+ const product = {
4175
+ productId: native.productId,
4176
+ title: native.title,
4177
+ description: native.description,
4178
+ price: native.price,
4179
+ priceValue: native.priceValue,
4180
+ currency: native.currency ?? "USD",
4181
+ localizedPrice: native.localizedPrice,
4182
+ type: mapProductType(native.type),
4183
+ subscriptionPeriod: native.subscriptionPeriod,
4184
+ introductoryPrice: native.introductoryPrice,
4185
+ introductoryPriceValue: native.introductoryPriceValue,
4186
+ freeTrialPeriod: native.freeTrialPeriod
4187
+ };
4188
+ if (native.subscriptionPeriod) {
4189
+ const prices = calculatePerPeriodPrices(native.priceValue, native.subscriptionPeriod);
4190
+ product.pricePerMonth = prices.perMonth;
4191
+ product.pricePerWeek = prices.perWeek;
4192
+ product.pricePerDay = prices.perDay;
4193
+ }
4194
+ return product;
4195
+ }
4196
+ function mapProductType(type) {
4197
+ switch (type) {
4198
+ case "subscription":
4199
+ case "autoRenewable":
4200
+ return "subscription";
4201
+ case "consumable":
4202
+ return "consumable";
4203
+ default:
4204
+ return "non_consumable";
4205
+ }
4206
+ }
4207
+ function calculatePerPeriodPrices(priceValue, period) {
4208
+ const totalDays = periodToDays(period);
4209
+ if (totalDays <= 0) return {};
4210
+ const perDay = priceValue / totalDays;
4211
+ const perWeek = perDay * 7;
4212
+ const perMonth = perDay * 30;
4213
+ return {
4214
+ perDay: perDay.toFixed(2),
4215
+ perWeek: perWeek.toFixed(2),
4216
+ perMonth: perMonth.toFixed(2)
4217
+ };
4218
+ }
4219
+ function periodToDays(period) {
4220
+ const match = period.match(/^P(\d+)([DWMY])$/);
4221
+ if (!match) return 0;
4222
+ const value = parseInt(match[1], 10);
4223
+ switch (match[2]) {
4224
+ case "D":
4225
+ return value;
4226
+ case "W":
4227
+ return value * 7;
4228
+ case "M":
4229
+ return value * 30;
4230
+ case "Y":
4231
+ return value * 365;
4232
+ default:
4233
+ return 0;
4234
+ }
4235
+ }
4236
+ function mapNativePurchase(native) {
4237
+ return {
4238
+ productId: native.productId,
4239
+ transactionId: native.transactionId,
4240
+ transactionDate: native.transactionDate,
4241
+ receipt: native.receipt,
4242
+ platform: import_react_native16.Platform.OS
4243
+ };
4244
+ }
4245
+ function normalizeTransactionUpdate(raw) {
4246
+ if (!raw || typeof raw !== "object") return null;
4247
+ const r = raw;
4248
+ const type = r.type;
4249
+ if (type !== "renewed" && type !== "refunded" && type !== "canceled") return null;
4250
+ const transactionId = typeof r.transactionId === "string" ? r.transactionId : null;
4251
+ const productId = typeof r.productId === "string" ? r.productId : null;
4252
+ if (!transactionId) return null;
4253
+ const update = {
4254
+ type,
4255
+ transactionId,
4256
+ productId: productId ?? ""
4257
+ };
4258
+ if (typeof r.amount === "number") update.amount = r.amount;
4259
+ if (typeof r.currency === "string") update.currency = r.currency;
4260
+ return update;
4261
+ }
4262
+ var import_react_native16, TRANSACTION_UPDATE_EVENT, PaywalloStoreKitNative, nativeStoreKit, NativeStoreKit;
4263
+ var init_NativeStoreKit = __esm({
4264
+ "src/domains/iap/NativeStoreKit.ts"() {
4265
+ "use strict";
4266
+ import_react_native16 = require("react-native");
4267
+ init_PaywalloClient();
4268
+ init_uuid();
4269
+ init_IdentityManager();
4270
+ init_PurchaseError();
4271
+ TRANSACTION_UPDATE_EVENT = "PaywalloTransactionUpdate";
4272
+ PaywalloStoreKitNative = import_react_native16.NativeModules.PaywalloStoreKit ?? null;
4273
+ nativeStoreKit = {
4274
+ isAvailable: isAvailable3,
4275
+ async getProducts(productIds) {
4276
+ if (!PaywalloStoreKitNative) {
4277
+ return [];
4278
+ }
4279
+ const nativeProducts = await PaywalloStoreKitNative.getProducts(productIds);
4280
+ return nativeProducts.map(mapNativeProduct);
4281
+ },
4282
+ async purchase(productId) {
4283
+ if (!PaywalloStoreKitNative) {
4284
+ throw new PurchaseError(
4285
+ PURCHASE_ERROR_CODES.STORE_NOT_AVAILABLE,
4286
+ "StoreKit native module not available"
4287
+ );
4288
+ }
4289
+ const distinctId = identityManager.getDistinctId();
4290
+ const rawUuid = distinctId?.startsWith("$paywallo_anon:") ? distinctId.slice("$paywallo_anon:".length) : distinctId;
4291
+ const appAccountToken = isValidUUIDv4(rawUuid) ? rawUuid : null;
4292
+ if (distinctId && appAccountToken === null) {
4293
+ if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] distinctId is not a valid UUID v4 \u2014 passing null to appAccountToken:", distinctId);
4294
+ }
4295
+ try {
4296
+ const result = await PaywalloStoreKitNative.purchase(productId, appAccountToken);
4297
+ if (!result || result.pending) {
4298
+ return null;
4299
+ }
4300
+ return mapNativePurchase(result);
4301
+ } catch (err) {
4302
+ const message = err instanceof Error ? err.message : String(err);
4303
+ if (message.toLowerCase().includes("cancel")) {
4304
+ throw new PurchaseError(PURCHASE_ERROR_CODES.USER_CANCELLED, message, true);
4305
+ }
4306
+ throw new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, message);
4307
+ }
4308
+ },
4309
+ async finishTransaction(transactionId) {
4310
+ if (!PaywalloStoreKitNative) return;
4311
+ try {
4312
+ await PaywalloStoreKitNative.finishTransaction(transactionId);
4313
+ } catch (err) {
4314
+ console.error("[Paywallo] finishTransaction native error", err);
4315
+ }
4316
+ },
4317
+ async getActiveTransactions() {
4318
+ if (!PaywalloStoreKitNative) return [];
4319
+ try {
4320
+ const results = await PaywalloStoreKitNative.getActiveTransactions();
4321
+ return results.map(mapNativePurchase);
4322
+ } catch (err) {
4323
+ console.error("[Paywallo] getActiveTransactions failed", err);
4324
+ return [];
4325
+ }
4326
+ },
4327
+ /**
4328
+ * Subscribes to native transaction update events (renewals, refunds,
4329
+ * cancellations) from StoreKit 2 (`Transaction.updates`) on iOS or
4330
+ * `PurchasesUpdatedListener` + refund diff on Android.
4331
+ *
4332
+ * Returns a subscription with a `remove()` method. The caller is
4333
+ * responsible for cleanup (typically on `Paywallo.reset()`).
4334
+ *
4335
+ * Returns `null` when the native module isn't available (non-mobile env,
4336
+ * native module not linked) — callers should treat null as a no-op.
4337
+ */
4338
+ subscribeToTransactionUpdates(listener) {
4339
+ if (!PaywalloStoreKitNative) return null;
4340
+ try {
4341
+ const EmitterCtor = import_react_native16.NativeEventEmitter;
4342
+ const emitter = new EmitterCtor(PaywalloStoreKitNative);
4343
+ const sub = emitter.addListener(TRANSACTION_UPDATE_EVENT, (payload) => {
4344
+ const update = normalizeTransactionUpdate(payload);
4345
+ if (update) listener(update);
4346
+ });
4347
+ return { remove: () => sub.remove() };
4348
+ } catch (err) {
4349
+ if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] subscribeToTransactionUpdates failed", err);
4350
+ return null;
4351
+ }
4352
+ }
4353
+ };
4354
+ NativeStoreKit = nativeStoreKit;
4355
+ }
4356
+ });
4357
+
4114
4358
  // src/domains/iap/IAPTransactionEmitter.ts
4115
4359
  var IAPTransactionEmitter;
4116
4360
  var init_IAPTransactionEmitter = __esm({
4117
4361
  "src/domains/iap/IAPTransactionEmitter.ts"() {
4118
4362
  "use strict";
4119
4363
  init_PaywalloClient();
4364
+ init_NativeStoreKit();
4120
4365
  IAPTransactionEmitter = class {
4121
4366
  constructor(getProductFromCache, getApiClientFn, getDebug) {
4122
4367
  this.getProductFromCache = getProductFromCache;
4123
4368
  this.getApiClientFn = getApiClientFn;
4124
4369
  this.getDebug = getDebug;
4370
+ this.transactionUpdateSubscription = null;
4371
+ }
4372
+ /**
4373
+ * Emits `transaction {type: "completed"}` after a server-validated purchase.
4374
+ * Fire-and-forget — a tracking failure must never poison the purchase flow.
4375
+ */
4376
+ async emitTransactionCompleted(purchase, productId, options) {
4377
+ try {
4378
+ const apiClient = this.getApiClientFn();
4379
+ if (!apiClient) return;
4380
+ const distinctId = PaywalloClient.getDistinctId();
4381
+ if (!distinctId) return;
4382
+ const product = this.getProductFromCache(productId);
4383
+ const properties = {
4384
+ type: "completed",
4385
+ tx_id: purchase.transactionId,
4386
+ product_id: purchase.productId
4387
+ };
4388
+ if (product?.priceValue !== void 0 && product.priceValue > 0) {
4389
+ properties.amount = product.priceValue;
4390
+ }
4391
+ if (product?.currency) {
4392
+ properties.currency = product.currency;
4393
+ }
4394
+ if (options?.paywallId) properties.paywall_id = options.paywallId;
4395
+ if (options?.variantId) properties.variant_id = options.variantId;
4396
+ await apiClient.trackEvent("transaction", distinctId, properties, void 0, { priority: "critical" });
4397
+ } catch (err) {
4398
+ if (this.getDebug()) console.log("[Paywallo PURCHASE] transaction event emission failed", err);
4399
+ }
4125
4400
  }
4126
4401
  /**
4127
4402
  * Emits `checkout_started` quando o usuário inicia a compra (toca em comprar),
@@ -4150,15 +4425,65 @@ var init_IAPTransactionEmitter = __esm({
4150
4425
  }
4151
4426
  }
4152
4427
  /**
4153
- * No-op. Native transaction update listener removed — sale events are sourced
4154
- * exclusively from server-side webhooks (Apple/Google/Superwall/RevenueCat).
4428
+ * Forwards a native transaction update to the EventBatcher.
4429
+ * Public so tests can exercise the wiring without a fake NativeEventEmitter.
4430
+ */
4431
+ async emitTransactionUpdate(update) {
4432
+ try {
4433
+ const apiClient = this.getApiClientFn() ?? PaywalloClient.getApiClient();
4434
+ if (!apiClient) {
4435
+ if (this.getDebug()) console.log("[Paywallo IAP] transaction update dropped (no apiClient)", update.type);
4436
+ return;
4437
+ }
4438
+ const distinctId = PaywalloClient.getDistinctId();
4439
+ if (!distinctId) {
4440
+ if (this.getDebug()) console.log("[Paywallo IAP] transaction update dropped (no distinctId)", update.type);
4441
+ return;
4442
+ }
4443
+ const properties = {
4444
+ type: update.type,
4445
+ tx_id: update.transactionId,
4446
+ product_id: update.productId
4447
+ };
4448
+ if (update.type !== "canceled") {
4449
+ const cached2 = this.getProductFromCache(update.productId);
4450
+ const amount = update.amount ?? cached2?.priceValue;
4451
+ const currency = update.currency ?? cached2?.currency;
4452
+ if (typeof amount === "number" && amount > 0) properties.amount = amount;
4453
+ if (currency) properties.currency = currency;
4454
+ }
4455
+ await apiClient.trackEvent("transaction", distinctId, properties, void 0, { priority: "critical" });
4456
+ if (this.getDebug()) console.log(`[Paywallo IAP] emitted transaction {${update.type}}`, update.transactionId);
4457
+ } catch (err) {
4458
+ if (this.getDebug()) console.log("[Paywallo IAP] transaction update emission failed", err);
4459
+ }
4460
+ }
4461
+ /**
4462
+ * Starts listening to native Transaction.updates. Idempotent.
4155
4463
  */
4156
4464
  startListener() {
4465
+ if (this.transactionUpdateSubscription) return;
4466
+ this.transactionUpdateSubscription = nativeStoreKit.subscribeToTransactionUpdates(
4467
+ (update) => {
4468
+ void this.emitTransactionUpdate(update);
4469
+ }
4470
+ );
4471
+ if (this.getDebug() && this.transactionUpdateSubscription) {
4472
+ console.log("[Paywallo IAP] transaction update listener started");
4473
+ }
4157
4474
  }
4158
4475
  /**
4159
- * No-op. See startListener.
4476
+ * Removes the native listener. Safe to call multiple times.
4160
4477
  */
4161
4478
  stopListener() {
4479
+ if (!this.transactionUpdateSubscription) return;
4480
+ try {
4481
+ this.transactionUpdateSubscription.remove();
4482
+ } catch (err) {
4483
+ if (this.getDebug()) console.log("[Paywallo IAP] transaction listener removal failed", err);
4484
+ }
4485
+ this.transactionUpdateSubscription = null;
4486
+ if (this.getDebug()) console.log("[Paywallo IAP] transaction update listener stopped");
4162
4487
  }
4163
4488
  };
4164
4489
  }
@@ -4646,11 +4971,11 @@ var init_types2 = __esm({
4646
4971
  });
4647
4972
 
4648
4973
  // src/core/network/NetworkMonitor.ts
4649
- var import_react_native16, NetworkMonitorClass, networkMonitor;
4974
+ var import_react_native17, NetworkMonitorClass, networkMonitor;
4650
4975
  var init_NetworkMonitor = __esm({
4651
4976
  "src/core/network/NetworkMonitor.ts"() {
4652
4977
  "use strict";
4653
- import_react_native16 = require("react-native");
4978
+ import_react_native17 = require("react-native");
4654
4979
  init_types2();
4655
4980
  NetworkMonitorClass = class {
4656
4981
  constructor() {
@@ -4679,7 +5004,7 @@ var init_NetworkMonitor = __esm({
4679
5004
  }, this.config.checkInterval);
4680
5005
  }
4681
5006
  setupAppStateListener() {
4682
- this.appStateSubscription = import_react_native16.AppState.addEventListener("change", (state) => {
5007
+ this.appStateSubscription = import_react_native17.AppState.addEventListener("change", (state) => {
4683
5008
  if (state === "active") {
4684
5009
  if (!this.checkIntervalId) {
4685
5010
  this.checkIntervalId = setInterval(() => {
@@ -5047,62 +5372,12 @@ var init_queue = __esm({
5047
5372
  }
5048
5373
  });
5049
5374
 
5050
- // src/domains/iap/PurchaseError.ts
5051
- function createPurchaseError(code, message) {
5052
- return new PurchaseError(
5053
- PURCHASE_ERROR_CODES[code],
5054
- message ?? DEFAULT_MESSAGES[code] ?? "Unknown purchase error",
5055
- code === "USER_CANCELLED"
5056
- );
5057
- }
5058
- var PurchaseError, PURCHASE_ERROR_CODES, DEFAULT_MESSAGES;
5059
- var init_PurchaseError = __esm({
5060
- "src/domains/iap/PurchaseError.ts"() {
5061
- "use strict";
5062
- init_PaywalloError();
5063
- PurchaseError = class extends PaywalloError {
5064
- constructor(code, message, userCancelled = false, httpStatus) {
5065
- super("purchase", code, message);
5066
- this.name = "PurchaseError";
5067
- this.userCancelled = userCancelled;
5068
- this.httpStatus = httpStatus;
5069
- }
5070
- };
5071
- PURCHASE_ERROR_CODES = {
5072
- NOT_INITIALIZED: "PURCHASE_NOT_INITIALIZED",
5073
- PRODUCT_NOT_FOUND: "PRODUCT_NOT_FOUND",
5074
- PURCHASE_FAILED: "PURCHASE_FAILED",
5075
- RESTORE_FAILED: "RESTORE_FAILED",
5076
- VALIDATION_FAILED: "VALIDATION_FAILED",
5077
- USER_CANCELLED: "USER_CANCELLED",
5078
- NETWORK_ERROR: "PURCHASE_NETWORK_ERROR",
5079
- STORE_ERROR: "PURCHASE_STORE_ERROR",
5080
- PENDING_PURCHASE: "PURCHASE_PENDING",
5081
- DEFERRED_PURCHASE: "PURCHASE_DEFERRED",
5082
- STORE_NOT_AVAILABLE: "PURCHASE_STORE_NOT_AVAILABLE"
5083
- };
5084
- DEFAULT_MESSAGES = {
5085
- NOT_INITIALIZED: "Purchase controller not initialized. Call init() first.",
5086
- PRODUCT_NOT_FOUND: "Product not found in store.",
5087
- PURCHASE_FAILED: "Purchase failed. Please try again.",
5088
- RESTORE_FAILED: "Failed to restore purchases. Please try again.",
5089
- VALIDATION_FAILED: "Failed to validate purchase with server.",
5090
- USER_CANCELLED: "Purchase was cancelled.",
5091
- NETWORK_ERROR: "Network error. Please check your connection.",
5092
- STORE_ERROR: "Store error. Please try again later.",
5093
- PENDING_PURCHASE: "Purchase is pending approval.",
5094
- DEFERRED_PURCHASE: "Purchase is deferred (e.g., parental approval required).",
5095
- STORE_NOT_AVAILABLE: "In-app purchases are not available on this device."
5096
- };
5097
- }
5098
- });
5099
-
5100
5375
  // src/domains/iap/IAPValidator.ts
5101
- var import_react_native17, IAPValidator;
5376
+ var import_react_native18, IAPValidator;
5102
5377
  var init_IAPValidator = __esm({
5103
5378
  "src/domains/iap/IAPValidator.ts"() {
5104
5379
  "use strict";
5105
- import_react_native17 = require("react-native");
5380
+ import_react_native18 = require("react-native");
5106
5381
  init_PaywalloClient();
5107
5382
  init_queue();
5108
5383
  init_PurchaseError();
@@ -5128,7 +5403,7 @@ var init_IAPValidator = __esm({
5128
5403
  async validateWithServer(purchase, productId, options) {
5129
5404
  const apiClient = this.getApiClientFn();
5130
5405
  const product = this.getProductFromCache(productId);
5131
- const platform = import_react_native17.Platform.OS;
5406
+ const platform = import_react_native18.Platform.OS;
5132
5407
  const distinctId = PaywalloClient.getDistinctId();
5133
5408
  return this.validateWithRetry(
5134
5409
  () => apiClient.validatePurchase(
@@ -5163,7 +5438,7 @@ var init_IAPValidator = __esm({
5163
5438
  try {
5164
5439
  const apiClient = this.getApiClientFn();
5165
5440
  const product = this.getProductFromCache(productId);
5166
- const platform = import_react_native17.Platform.OS;
5441
+ const platform = import_react_native18.Platform.OS;
5167
5442
  const distinctId = PaywalloClient.getDistinctId();
5168
5443
  const appKey = apiClient.getAppKey();
5169
5444
  const body = {
@@ -5195,200 +5470,6 @@ var init_IAPValidator = __esm({
5195
5470
  }
5196
5471
  });
5197
5472
 
5198
- // src/domains/iap/NativeStoreKit.ts
5199
- var NativeStoreKit_exports = {};
5200
- __export(NativeStoreKit_exports, {
5201
- NativeStoreKit: () => NativeStoreKit,
5202
- nativeStoreKit: () => nativeStoreKit
5203
- });
5204
- function isAvailable3() {
5205
- return PaywalloStoreKitNative != null;
5206
- }
5207
- function mapNativeProduct(native) {
5208
- const product = {
5209
- productId: native.productId,
5210
- title: native.title,
5211
- description: native.description,
5212
- price: native.price,
5213
- priceValue: native.priceValue,
5214
- currency: native.currency ?? "USD",
5215
- localizedPrice: native.localizedPrice,
5216
- type: mapProductType(native.type),
5217
- subscriptionPeriod: native.subscriptionPeriod,
5218
- introductoryPrice: native.introductoryPrice,
5219
- introductoryPriceValue: native.introductoryPriceValue,
5220
- freeTrialPeriod: native.freeTrialPeriod
5221
- };
5222
- if (native.subscriptionPeriod) {
5223
- const prices = calculatePerPeriodPrices(native.priceValue, native.subscriptionPeriod);
5224
- product.pricePerMonth = prices.perMonth;
5225
- product.pricePerWeek = prices.perWeek;
5226
- product.pricePerDay = prices.perDay;
5227
- }
5228
- return product;
5229
- }
5230
- function mapProductType(type) {
5231
- switch (type) {
5232
- case "subscription":
5233
- case "autoRenewable":
5234
- return "subscription";
5235
- case "consumable":
5236
- return "consumable";
5237
- default:
5238
- return "non_consumable";
5239
- }
5240
- }
5241
- function calculatePerPeriodPrices(priceValue, period) {
5242
- const totalDays = periodToDays(period);
5243
- if (totalDays <= 0) return {};
5244
- const perDay = priceValue / totalDays;
5245
- const perWeek = perDay * 7;
5246
- const perMonth = perDay * 30;
5247
- return {
5248
- perDay: perDay.toFixed(2),
5249
- perWeek: perWeek.toFixed(2),
5250
- perMonth: perMonth.toFixed(2)
5251
- };
5252
- }
5253
- function periodToDays(period) {
5254
- const match = period.match(/^P(\d+)([DWMY])$/);
5255
- if (!match) return 0;
5256
- const value = parseInt(match[1], 10);
5257
- switch (match[2]) {
5258
- case "D":
5259
- return value;
5260
- case "W":
5261
- return value * 7;
5262
- case "M":
5263
- return value * 30;
5264
- case "Y":
5265
- return value * 365;
5266
- default:
5267
- return 0;
5268
- }
5269
- }
5270
- function mapNativePurchase(native) {
5271
- return {
5272
- productId: native.productId,
5273
- transactionId: native.transactionId,
5274
- transactionDate: native.transactionDate,
5275
- receipt: native.receipt,
5276
- platform: import_react_native18.Platform.OS
5277
- };
5278
- }
5279
- function normalizeTransactionUpdate(raw) {
5280
- if (!raw || typeof raw !== "object") return null;
5281
- const r = raw;
5282
- const type = r.type;
5283
- if (type !== "renewed" && type !== "refunded" && type !== "canceled") return null;
5284
- const transactionId = typeof r.transactionId === "string" ? r.transactionId : null;
5285
- const productId = typeof r.productId === "string" ? r.productId : null;
5286
- if (!transactionId) return null;
5287
- const update = {
5288
- type,
5289
- transactionId,
5290
- productId: productId ?? ""
5291
- };
5292
- if (typeof r.amount === "number") update.amount = r.amount;
5293
- if (typeof r.currency === "string") update.currency = r.currency;
5294
- return update;
5295
- }
5296
- var import_react_native18, TRANSACTION_UPDATE_EVENT, PaywalloStoreKitNative, nativeStoreKit, NativeStoreKit;
5297
- var init_NativeStoreKit = __esm({
5298
- "src/domains/iap/NativeStoreKit.ts"() {
5299
- "use strict";
5300
- import_react_native18 = require("react-native");
5301
- init_PaywalloClient();
5302
- init_uuid();
5303
- init_IdentityManager();
5304
- init_PurchaseError();
5305
- TRANSACTION_UPDATE_EVENT = "PaywalloTransactionUpdate";
5306
- PaywalloStoreKitNative = import_react_native18.NativeModules.PaywalloStoreKit ?? null;
5307
- nativeStoreKit = {
5308
- isAvailable: isAvailable3,
5309
- async getProducts(productIds) {
5310
- if (!PaywalloStoreKitNative) {
5311
- return [];
5312
- }
5313
- const nativeProducts = await PaywalloStoreKitNative.getProducts(productIds);
5314
- return nativeProducts.map(mapNativeProduct);
5315
- },
5316
- async purchase(productId) {
5317
- if (!PaywalloStoreKitNative) {
5318
- throw new PurchaseError(
5319
- PURCHASE_ERROR_CODES.STORE_NOT_AVAILABLE,
5320
- "StoreKit native module not available"
5321
- );
5322
- }
5323
- const distinctId = identityManager.getDistinctId();
5324
- const rawUuid = distinctId?.startsWith("$paywallo_anon:") ? distinctId.slice("$paywallo_anon:".length) : distinctId;
5325
- const appAccountToken = isValidUUIDv4(rawUuid) ? rawUuid : null;
5326
- if (distinctId && appAccountToken === null) {
5327
- if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] distinctId is not a valid UUID v4 \u2014 passing null to appAccountToken:", distinctId);
5328
- }
5329
- try {
5330
- const result = await PaywalloStoreKitNative.purchase(productId, appAccountToken);
5331
- if (!result || result.pending) {
5332
- return null;
5333
- }
5334
- return mapNativePurchase(result);
5335
- } catch (err) {
5336
- const message = err instanceof Error ? err.message : String(err);
5337
- if (message.toLowerCase().includes("cancel")) {
5338
- throw new PurchaseError(PURCHASE_ERROR_CODES.USER_CANCELLED, message, true);
5339
- }
5340
- throw new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, message);
5341
- }
5342
- },
5343
- async finishTransaction(transactionId) {
5344
- if (!PaywalloStoreKitNative) return;
5345
- try {
5346
- await PaywalloStoreKitNative.finishTransaction(transactionId);
5347
- } catch (err) {
5348
- console.error("[Paywallo] finishTransaction native error", err);
5349
- }
5350
- },
5351
- async getActiveTransactions() {
5352
- if (!PaywalloStoreKitNative) return [];
5353
- try {
5354
- const results = await PaywalloStoreKitNative.getActiveTransactions();
5355
- return results.map(mapNativePurchase);
5356
- } catch (err) {
5357
- console.error("[Paywallo] getActiveTransactions failed", err);
5358
- return [];
5359
- }
5360
- },
5361
- /**
5362
- * Subscribes to native transaction update events (renewals, refunds,
5363
- * cancellations) from StoreKit 2 (`Transaction.updates`) on iOS or
5364
- * `PurchasesUpdatedListener` + refund diff on Android.
5365
- *
5366
- * Returns a subscription with a `remove()` method. The caller is
5367
- * responsible for cleanup (typically on `Paywallo.reset()`).
5368
- *
5369
- * Returns `null` when the native module isn't available (non-mobile env,
5370
- * native module not linked) — callers should treat null as a no-op.
5371
- */
5372
- subscribeToTransactionUpdates(listener) {
5373
- if (!PaywalloStoreKitNative) return null;
5374
- try {
5375
- const EmitterCtor = import_react_native18.NativeEventEmitter;
5376
- const emitter = new EmitterCtor(PaywalloStoreKitNative);
5377
- const sub = emitter.addListener(TRANSACTION_UPDATE_EVENT, (payload) => {
5378
- const update = normalizeTransactionUpdate(payload);
5379
- if (update) listener(update);
5380
- });
5381
- return { remove: () => sub.remove() };
5382
- } catch (err) {
5383
- if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] subscribeToTransactionUpdates failed", err);
5384
- return null;
5385
- }
5386
- }
5387
- };
5388
- NativeStoreKit = nativeStoreKit;
5389
- }
5390
- });
5391
-
5392
5473
  // src/domains/iap/IAPService.ts
5393
5474
  function getIAPService() {
5394
5475
  if (!_instance) {
@@ -5489,8 +5570,8 @@ var init_IAPService = __esm({
5489
5570
  stopTransactionListener() {
5490
5571
  this.emitter.stopListener();
5491
5572
  }
5492
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
5493
- async emitTransactionUpdate(_update) {
5573
+ async emitTransactionUpdate(update) {
5574
+ return this.emitter.emitTransactionUpdate(update);
5494
5575
  }
5495
5576
  async purchase(productId, options) {
5496
5577
  if (!nativeStoreKit.isAvailable()) {
@@ -5519,6 +5600,10 @@ var init_IAPService = __esm({
5519
5600
  if (this.debug) console.log(`[Paywallo PURCHASE] ${productId} server validated`);
5520
5601
  await this.finishTransaction(purchase.transactionId);
5521
5602
  if (this.debug) console.log(`[Paywallo PURCHASE] ${productId} finished`);
5603
+ await this.emitter.emitTransactionCompleted(purchase, productId, {
5604
+ paywallId: options?.paywallId,
5605
+ variantId: options?.variantId
5606
+ });
5522
5607
  }).catch(async (err) => {
5523
5608
  const kind = this.validator.classifyValidationError(err);
5524
5609
  if (kind === "server4xx") {
@@ -10265,10 +10350,57 @@ function buildCallbacks() {
10265
10350
  const identifier = "paywallIdentifier" in safe && typeof safe.paywallIdentifier === "string" ? safe.paywallIdentifier : productId;
10266
10351
  log2("onPurchase", { productId, identifier });
10267
10352
  void trackPaywallEvent("purchased", identifier, ts, { product_id: productId });
10353
+ },
10354
+ // Global Superwall event listener — used specifically for `transactionComplete`
10355
+ // which is the only event that exposes the full product (currency/price) +
10356
+ // transaction (storeTransactionId) shape needed to create a server-side
10357
+ // `transactions` row. Other events are skipped (paywallOpen/Dismiss already
10358
+ // handled above with their typed callbacks).
10359
+ onSuperwallEvent: (info) => {
10360
+ const ev = info?.event;
10361
+ if (!ev || ev.event !== "transactionComplete") return;
10362
+ const ts = Date.now();
10363
+ const product = ev.product;
10364
+ const transaction = ev.transaction;
10365
+ if (!product) {
10366
+ log2("transactionComplete missing product \u2014 skipping transaction track");
10367
+ return;
10368
+ }
10369
+ const productId = product.productIdentifier ?? product.id ?? "unknown";
10370
+ const currency = product.currencyCode ?? "USD";
10371
+ const price = product.price ?? 0;
10372
+ const transactionId = transaction?.purchaseToken ?? transaction?.storeTransactionId ?? transaction?.originalTransactionIdentifier ?? `${productId}_${String(ts)}`;
10373
+ const introPrice = product.trialPeriodPrice ?? 0;
10374
+ const isTrial = product.hasFreeTrial === true && introPrice <= 0;
10375
+ const isPaidIntro = introPrice > 0;
10376
+ const paywallIdentifier = ev.paywallInfo?.identifier;
10377
+ log2("onSuperwallEvent: transactionComplete", { productId, currency, price, introPrice, transactionId, isTrial });
10378
+ void (async () => {
10379
+ try {
10380
+ await PaywalloClient.track("transaction", {
10381
+ priority: "critical",
10382
+ properties: {
10383
+ type: isTrial ? "trial_started" : "completed",
10384
+ // Server's IngestTranslator looks for `transaction_id`/`product_id`
10385
+ // /`amount`/`currency`/`paywall_id` keys. A free trial MUST carry
10386
+ // amount=0 (no revenue at start); a paid intro offer carries its
10387
+ // intro amount (real revenue); a normal purchase carries `price`.
10388
+ // `full_price` carries the recurring full price so the server can
10389
+ // value the eventual conversion (free trial) or keep the headline
10390
+ // price alongside the discounted intro amount (paid intro).
10391
+ transaction_id: transactionId,
10392
+ product_id: productId,
10393
+ amount: isTrial ? 0 : isPaidIntro ? introPrice : price,
10394
+ currency,
10395
+ ...isTrial || isPaidIntro ? { full_price: price } : {},
10396
+ ...paywallIdentifier ? { paywall_id: paywallIdentifier } : {}
10397
+ }
10398
+ });
10399
+ } catch (err) {
10400
+ log2("transactionComplete track error", { err: String(err) });
10401
+ }
10402
+ })();
10268
10403
  }
10269
- // onSuperwallEvent (transactionComplete → transaction track) removed —
10270
- // sale/transaction events are sourced exclusively from server-side webhooks
10271
- // (Apple/Google/Superwall/RevenueCat), the single source of truth for revenue.
10272
10404
  };
10273
10405
  }
10274
10406
  function startSuperwallAutoBridge(debug = false) {