@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.mjs CHANGED
@@ -2611,8 +2611,8 @@ var init_NotificationHandlerSetup = __esm({
2611
2611
  // src/core/version.ts
2612
2612
  function resolveVersion() {
2613
2613
  try {
2614
- if ("2.5.2") {
2615
- return "2.5.2";
2614
+ if ("2.5.3") {
2615
+ return "2.5.3";
2616
2616
  }
2617
2617
  } catch {
2618
2618
  }
@@ -4106,17 +4106,292 @@ var init_PaywallWebView = __esm({
4106
4106
  }
4107
4107
  });
4108
4108
 
4109
+ // src/domains/iap/PurchaseError.ts
4110
+ function createPurchaseError(code, message) {
4111
+ return new PurchaseError(
4112
+ PURCHASE_ERROR_CODES[code],
4113
+ message ?? DEFAULT_MESSAGES[code] ?? "Unknown purchase error",
4114
+ code === "USER_CANCELLED"
4115
+ );
4116
+ }
4117
+ var PurchaseError, PURCHASE_ERROR_CODES, DEFAULT_MESSAGES;
4118
+ var init_PurchaseError = __esm({
4119
+ "src/domains/iap/PurchaseError.ts"() {
4120
+ "use strict";
4121
+ init_PaywalloError();
4122
+ PurchaseError = class extends PaywalloError {
4123
+ constructor(code, message, userCancelled = false, httpStatus) {
4124
+ super("purchase", code, message);
4125
+ this.name = "PurchaseError";
4126
+ this.userCancelled = userCancelled;
4127
+ this.httpStatus = httpStatus;
4128
+ }
4129
+ };
4130
+ PURCHASE_ERROR_CODES = {
4131
+ NOT_INITIALIZED: "PURCHASE_NOT_INITIALIZED",
4132
+ PRODUCT_NOT_FOUND: "PRODUCT_NOT_FOUND",
4133
+ PURCHASE_FAILED: "PURCHASE_FAILED",
4134
+ RESTORE_FAILED: "RESTORE_FAILED",
4135
+ VALIDATION_FAILED: "VALIDATION_FAILED",
4136
+ USER_CANCELLED: "USER_CANCELLED",
4137
+ NETWORK_ERROR: "PURCHASE_NETWORK_ERROR",
4138
+ STORE_ERROR: "PURCHASE_STORE_ERROR",
4139
+ PENDING_PURCHASE: "PURCHASE_PENDING",
4140
+ DEFERRED_PURCHASE: "PURCHASE_DEFERRED",
4141
+ STORE_NOT_AVAILABLE: "PURCHASE_STORE_NOT_AVAILABLE"
4142
+ };
4143
+ DEFAULT_MESSAGES = {
4144
+ NOT_INITIALIZED: "Purchase controller not initialized. Call init() first.",
4145
+ PRODUCT_NOT_FOUND: "Product not found in store.",
4146
+ PURCHASE_FAILED: "Purchase failed. Please try again.",
4147
+ RESTORE_FAILED: "Failed to restore purchases. Please try again.",
4148
+ VALIDATION_FAILED: "Failed to validate purchase with server.",
4149
+ USER_CANCELLED: "Purchase was cancelled.",
4150
+ NETWORK_ERROR: "Network error. Please check your connection.",
4151
+ STORE_ERROR: "Store error. Please try again later.",
4152
+ PENDING_PURCHASE: "Purchase is pending approval.",
4153
+ DEFERRED_PURCHASE: "Purchase is deferred (e.g., parental approval required).",
4154
+ STORE_NOT_AVAILABLE: "In-app purchases are not available on this device."
4155
+ };
4156
+ }
4157
+ });
4158
+
4159
+ // src/domains/iap/NativeStoreKit.ts
4160
+ var NativeStoreKit_exports = {};
4161
+ __export(NativeStoreKit_exports, {
4162
+ NativeStoreKit: () => NativeStoreKit,
4163
+ nativeStoreKit: () => nativeStoreKit
4164
+ });
4165
+ import { NativeEventEmitter as NativeEventEmitter3, NativeModules as NativeModules6, Platform as Platform11 } from "react-native";
4166
+ function isAvailable3() {
4167
+ return PaywalloStoreKitNative != null;
4168
+ }
4169
+ function mapNativeProduct(native) {
4170
+ const product = {
4171
+ productId: native.productId,
4172
+ title: native.title,
4173
+ description: native.description,
4174
+ price: native.price,
4175
+ priceValue: native.priceValue,
4176
+ currency: native.currency ?? "USD",
4177
+ localizedPrice: native.localizedPrice,
4178
+ type: mapProductType(native.type),
4179
+ subscriptionPeriod: native.subscriptionPeriod,
4180
+ introductoryPrice: native.introductoryPrice,
4181
+ introductoryPriceValue: native.introductoryPriceValue,
4182
+ freeTrialPeriod: native.freeTrialPeriod
4183
+ };
4184
+ if (native.subscriptionPeriod) {
4185
+ const prices = calculatePerPeriodPrices(native.priceValue, native.subscriptionPeriod);
4186
+ product.pricePerMonth = prices.perMonth;
4187
+ product.pricePerWeek = prices.perWeek;
4188
+ product.pricePerDay = prices.perDay;
4189
+ }
4190
+ return product;
4191
+ }
4192
+ function mapProductType(type) {
4193
+ switch (type) {
4194
+ case "subscription":
4195
+ case "autoRenewable":
4196
+ return "subscription";
4197
+ case "consumable":
4198
+ return "consumable";
4199
+ default:
4200
+ return "non_consumable";
4201
+ }
4202
+ }
4203
+ function calculatePerPeriodPrices(priceValue, period) {
4204
+ const totalDays = periodToDays(period);
4205
+ if (totalDays <= 0) return {};
4206
+ const perDay = priceValue / totalDays;
4207
+ const perWeek = perDay * 7;
4208
+ const perMonth = perDay * 30;
4209
+ return {
4210
+ perDay: perDay.toFixed(2),
4211
+ perWeek: perWeek.toFixed(2),
4212
+ perMonth: perMonth.toFixed(2)
4213
+ };
4214
+ }
4215
+ function periodToDays(period) {
4216
+ const match = period.match(/^P(\d+)([DWMY])$/);
4217
+ if (!match) return 0;
4218
+ const value = parseInt(match[1], 10);
4219
+ switch (match[2]) {
4220
+ case "D":
4221
+ return value;
4222
+ case "W":
4223
+ return value * 7;
4224
+ case "M":
4225
+ return value * 30;
4226
+ case "Y":
4227
+ return value * 365;
4228
+ default:
4229
+ return 0;
4230
+ }
4231
+ }
4232
+ function mapNativePurchase(native) {
4233
+ return {
4234
+ productId: native.productId,
4235
+ transactionId: native.transactionId,
4236
+ transactionDate: native.transactionDate,
4237
+ receipt: native.receipt,
4238
+ platform: Platform11.OS
4239
+ };
4240
+ }
4241
+ function normalizeTransactionUpdate(raw) {
4242
+ if (!raw || typeof raw !== "object") return null;
4243
+ const r = raw;
4244
+ const type = r.type;
4245
+ if (type !== "renewed" && type !== "refunded" && type !== "canceled") return null;
4246
+ const transactionId = typeof r.transactionId === "string" ? r.transactionId : null;
4247
+ const productId = typeof r.productId === "string" ? r.productId : null;
4248
+ if (!transactionId) return null;
4249
+ const update = {
4250
+ type,
4251
+ transactionId,
4252
+ productId: productId ?? ""
4253
+ };
4254
+ if (typeof r.amount === "number") update.amount = r.amount;
4255
+ if (typeof r.currency === "string") update.currency = r.currency;
4256
+ return update;
4257
+ }
4258
+ var TRANSACTION_UPDATE_EVENT, PaywalloStoreKitNative, nativeStoreKit, NativeStoreKit;
4259
+ var init_NativeStoreKit = __esm({
4260
+ "src/domains/iap/NativeStoreKit.ts"() {
4261
+ "use strict";
4262
+ init_PaywalloClient();
4263
+ init_uuid();
4264
+ init_IdentityManager();
4265
+ init_PurchaseError();
4266
+ TRANSACTION_UPDATE_EVENT = "PaywalloTransactionUpdate";
4267
+ PaywalloStoreKitNative = NativeModules6.PaywalloStoreKit ?? null;
4268
+ nativeStoreKit = {
4269
+ isAvailable: isAvailable3,
4270
+ async getProducts(productIds) {
4271
+ if (!PaywalloStoreKitNative) {
4272
+ return [];
4273
+ }
4274
+ const nativeProducts = await PaywalloStoreKitNative.getProducts(productIds);
4275
+ return nativeProducts.map(mapNativeProduct);
4276
+ },
4277
+ async purchase(productId) {
4278
+ if (!PaywalloStoreKitNative) {
4279
+ throw new PurchaseError(
4280
+ PURCHASE_ERROR_CODES.STORE_NOT_AVAILABLE,
4281
+ "StoreKit native module not available"
4282
+ );
4283
+ }
4284
+ const distinctId = identityManager.getDistinctId();
4285
+ const rawUuid = distinctId?.startsWith("$paywallo_anon:") ? distinctId.slice("$paywallo_anon:".length) : distinctId;
4286
+ const appAccountToken = isValidUUIDv4(rawUuid) ? rawUuid : null;
4287
+ if (distinctId && appAccountToken === null) {
4288
+ if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] distinctId is not a valid UUID v4 \u2014 passing null to appAccountToken:", distinctId);
4289
+ }
4290
+ try {
4291
+ const result = await PaywalloStoreKitNative.purchase(productId, appAccountToken);
4292
+ if (!result || result.pending) {
4293
+ return null;
4294
+ }
4295
+ return mapNativePurchase(result);
4296
+ } catch (err) {
4297
+ const message = err instanceof Error ? err.message : String(err);
4298
+ if (message.toLowerCase().includes("cancel")) {
4299
+ throw new PurchaseError(PURCHASE_ERROR_CODES.USER_CANCELLED, message, true);
4300
+ }
4301
+ throw new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, message);
4302
+ }
4303
+ },
4304
+ async finishTransaction(transactionId) {
4305
+ if (!PaywalloStoreKitNative) return;
4306
+ try {
4307
+ await PaywalloStoreKitNative.finishTransaction(transactionId);
4308
+ } catch (err) {
4309
+ console.error("[Paywallo] finishTransaction native error", err);
4310
+ }
4311
+ },
4312
+ async getActiveTransactions() {
4313
+ if (!PaywalloStoreKitNative) return [];
4314
+ try {
4315
+ const results = await PaywalloStoreKitNative.getActiveTransactions();
4316
+ return results.map(mapNativePurchase);
4317
+ } catch (err) {
4318
+ console.error("[Paywallo] getActiveTransactions failed", err);
4319
+ return [];
4320
+ }
4321
+ },
4322
+ /**
4323
+ * Subscribes to native transaction update events (renewals, refunds,
4324
+ * cancellations) from StoreKit 2 (`Transaction.updates`) on iOS or
4325
+ * `PurchasesUpdatedListener` + refund diff on Android.
4326
+ *
4327
+ * Returns a subscription with a `remove()` method. The caller is
4328
+ * responsible for cleanup (typically on `Paywallo.reset()`).
4329
+ *
4330
+ * Returns `null` when the native module isn't available (non-mobile env,
4331
+ * native module not linked) — callers should treat null as a no-op.
4332
+ */
4333
+ subscribeToTransactionUpdates(listener) {
4334
+ if (!PaywalloStoreKitNative) return null;
4335
+ try {
4336
+ const EmitterCtor = NativeEventEmitter3;
4337
+ const emitter = new EmitterCtor(PaywalloStoreKitNative);
4338
+ const sub = emitter.addListener(TRANSACTION_UPDATE_EVENT, (payload) => {
4339
+ const update = normalizeTransactionUpdate(payload);
4340
+ if (update) listener(update);
4341
+ });
4342
+ return { remove: () => sub.remove() };
4343
+ } catch (err) {
4344
+ if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] subscribeToTransactionUpdates failed", err);
4345
+ return null;
4346
+ }
4347
+ }
4348
+ };
4349
+ NativeStoreKit = nativeStoreKit;
4350
+ }
4351
+ });
4352
+
4109
4353
  // src/domains/iap/IAPTransactionEmitter.ts
4110
4354
  var IAPTransactionEmitter;
4111
4355
  var init_IAPTransactionEmitter = __esm({
4112
4356
  "src/domains/iap/IAPTransactionEmitter.ts"() {
4113
4357
  "use strict";
4114
4358
  init_PaywalloClient();
4359
+ init_NativeStoreKit();
4115
4360
  IAPTransactionEmitter = class {
4116
4361
  constructor(getProductFromCache, getApiClientFn, getDebug) {
4117
4362
  this.getProductFromCache = getProductFromCache;
4118
4363
  this.getApiClientFn = getApiClientFn;
4119
4364
  this.getDebug = getDebug;
4365
+ this.transactionUpdateSubscription = null;
4366
+ }
4367
+ /**
4368
+ * Emits `transaction {type: "completed"}` after a server-validated purchase.
4369
+ * Fire-and-forget — a tracking failure must never poison the purchase flow.
4370
+ */
4371
+ async emitTransactionCompleted(purchase, productId, options) {
4372
+ try {
4373
+ const apiClient = this.getApiClientFn();
4374
+ if (!apiClient) return;
4375
+ const distinctId = PaywalloClient.getDistinctId();
4376
+ if (!distinctId) return;
4377
+ const product = this.getProductFromCache(productId);
4378
+ const properties = {
4379
+ type: "completed",
4380
+ tx_id: purchase.transactionId,
4381
+ product_id: purchase.productId
4382
+ };
4383
+ if (product?.priceValue !== void 0 && product.priceValue > 0) {
4384
+ properties.amount = product.priceValue;
4385
+ }
4386
+ if (product?.currency) {
4387
+ properties.currency = product.currency;
4388
+ }
4389
+ if (options?.paywallId) properties.paywall_id = options.paywallId;
4390
+ if (options?.variantId) properties.variant_id = options.variantId;
4391
+ await apiClient.trackEvent("transaction", distinctId, properties, void 0, { priority: "critical" });
4392
+ } catch (err) {
4393
+ if (this.getDebug()) console.log("[Paywallo PURCHASE] transaction event emission failed", err);
4394
+ }
4120
4395
  }
4121
4396
  /**
4122
4397
  * Emits `checkout_started` quando o usuário inicia a compra (toca em comprar),
@@ -4145,15 +4420,65 @@ var init_IAPTransactionEmitter = __esm({
4145
4420
  }
4146
4421
  }
4147
4422
  /**
4148
- * No-op. Native transaction update listener removed — sale events are sourced
4149
- * exclusively from server-side webhooks (Apple/Google/Superwall/RevenueCat).
4423
+ * Forwards a native transaction update to the EventBatcher.
4424
+ * Public so tests can exercise the wiring without a fake NativeEventEmitter.
4425
+ */
4426
+ async emitTransactionUpdate(update) {
4427
+ try {
4428
+ const apiClient = this.getApiClientFn() ?? PaywalloClient.getApiClient();
4429
+ if (!apiClient) {
4430
+ if (this.getDebug()) console.log("[Paywallo IAP] transaction update dropped (no apiClient)", update.type);
4431
+ return;
4432
+ }
4433
+ const distinctId = PaywalloClient.getDistinctId();
4434
+ if (!distinctId) {
4435
+ if (this.getDebug()) console.log("[Paywallo IAP] transaction update dropped (no distinctId)", update.type);
4436
+ return;
4437
+ }
4438
+ const properties = {
4439
+ type: update.type,
4440
+ tx_id: update.transactionId,
4441
+ product_id: update.productId
4442
+ };
4443
+ if (update.type !== "canceled") {
4444
+ const cached2 = this.getProductFromCache(update.productId);
4445
+ const amount = update.amount ?? cached2?.priceValue;
4446
+ const currency = update.currency ?? cached2?.currency;
4447
+ if (typeof amount === "number" && amount > 0) properties.amount = amount;
4448
+ if (currency) properties.currency = currency;
4449
+ }
4450
+ await apiClient.trackEvent("transaction", distinctId, properties, void 0, { priority: "critical" });
4451
+ if (this.getDebug()) console.log(`[Paywallo IAP] emitted transaction {${update.type}}`, update.transactionId);
4452
+ } catch (err) {
4453
+ if (this.getDebug()) console.log("[Paywallo IAP] transaction update emission failed", err);
4454
+ }
4455
+ }
4456
+ /**
4457
+ * Starts listening to native Transaction.updates. Idempotent.
4150
4458
  */
4151
4459
  startListener() {
4460
+ if (this.transactionUpdateSubscription) return;
4461
+ this.transactionUpdateSubscription = nativeStoreKit.subscribeToTransactionUpdates(
4462
+ (update) => {
4463
+ void this.emitTransactionUpdate(update);
4464
+ }
4465
+ );
4466
+ if (this.getDebug() && this.transactionUpdateSubscription) {
4467
+ console.log("[Paywallo IAP] transaction update listener started");
4468
+ }
4152
4469
  }
4153
4470
  /**
4154
- * No-op. See startListener.
4471
+ * Removes the native listener. Safe to call multiple times.
4155
4472
  */
4156
4473
  stopListener() {
4474
+ if (!this.transactionUpdateSubscription) return;
4475
+ try {
4476
+ this.transactionUpdateSubscription.remove();
4477
+ } catch (err) {
4478
+ if (this.getDebug()) console.log("[Paywallo IAP] transaction listener removal failed", err);
4479
+ }
4480
+ this.transactionUpdateSubscription = null;
4481
+ if (this.getDebug()) console.log("[Paywallo IAP] transaction update listener stopped");
4157
4482
  }
4158
4483
  };
4159
4484
  }
@@ -5042,58 +5367,8 @@ var init_queue = __esm({
5042
5367
  }
5043
5368
  });
5044
5369
 
5045
- // src/domains/iap/PurchaseError.ts
5046
- function createPurchaseError(code, message) {
5047
- return new PurchaseError(
5048
- PURCHASE_ERROR_CODES[code],
5049
- message ?? DEFAULT_MESSAGES[code] ?? "Unknown purchase error",
5050
- code === "USER_CANCELLED"
5051
- );
5052
- }
5053
- var PurchaseError, PURCHASE_ERROR_CODES, DEFAULT_MESSAGES;
5054
- var init_PurchaseError = __esm({
5055
- "src/domains/iap/PurchaseError.ts"() {
5056
- "use strict";
5057
- init_PaywalloError();
5058
- PurchaseError = class extends PaywalloError {
5059
- constructor(code, message, userCancelled = false, httpStatus) {
5060
- super("purchase", code, message);
5061
- this.name = "PurchaseError";
5062
- this.userCancelled = userCancelled;
5063
- this.httpStatus = httpStatus;
5064
- }
5065
- };
5066
- PURCHASE_ERROR_CODES = {
5067
- NOT_INITIALIZED: "PURCHASE_NOT_INITIALIZED",
5068
- PRODUCT_NOT_FOUND: "PRODUCT_NOT_FOUND",
5069
- PURCHASE_FAILED: "PURCHASE_FAILED",
5070
- RESTORE_FAILED: "RESTORE_FAILED",
5071
- VALIDATION_FAILED: "VALIDATION_FAILED",
5072
- USER_CANCELLED: "USER_CANCELLED",
5073
- NETWORK_ERROR: "PURCHASE_NETWORK_ERROR",
5074
- STORE_ERROR: "PURCHASE_STORE_ERROR",
5075
- PENDING_PURCHASE: "PURCHASE_PENDING",
5076
- DEFERRED_PURCHASE: "PURCHASE_DEFERRED",
5077
- STORE_NOT_AVAILABLE: "PURCHASE_STORE_NOT_AVAILABLE"
5078
- };
5079
- DEFAULT_MESSAGES = {
5080
- NOT_INITIALIZED: "Purchase controller not initialized. Call init() first.",
5081
- PRODUCT_NOT_FOUND: "Product not found in store.",
5082
- PURCHASE_FAILED: "Purchase failed. Please try again.",
5083
- RESTORE_FAILED: "Failed to restore purchases. Please try again.",
5084
- VALIDATION_FAILED: "Failed to validate purchase with server.",
5085
- USER_CANCELLED: "Purchase was cancelled.",
5086
- NETWORK_ERROR: "Network error. Please check your connection.",
5087
- STORE_ERROR: "Store error. Please try again later.",
5088
- PENDING_PURCHASE: "Purchase is pending approval.",
5089
- DEFERRED_PURCHASE: "Purchase is deferred (e.g., parental approval required).",
5090
- STORE_NOT_AVAILABLE: "In-app purchases are not available on this device."
5091
- };
5092
- }
5093
- });
5094
-
5095
5370
  // src/domains/iap/IAPValidator.ts
5096
- import { Platform as Platform11 } from "react-native";
5371
+ import { Platform as Platform12 } from "react-native";
5097
5372
  var IAPValidator;
5098
5373
  var init_IAPValidator = __esm({
5099
5374
  "src/domains/iap/IAPValidator.ts"() {
@@ -5123,7 +5398,7 @@ var init_IAPValidator = __esm({
5123
5398
  async validateWithServer(purchase, productId, options) {
5124
5399
  const apiClient = this.getApiClientFn();
5125
5400
  const product = this.getProductFromCache(productId);
5126
- const platform = Platform11.OS;
5401
+ const platform = Platform12.OS;
5127
5402
  const distinctId = PaywalloClient.getDistinctId();
5128
5403
  return this.validateWithRetry(
5129
5404
  () => apiClient.validatePurchase(
@@ -5158,7 +5433,7 @@ var init_IAPValidator = __esm({
5158
5433
  try {
5159
5434
  const apiClient = this.getApiClientFn();
5160
5435
  const product = this.getProductFromCache(productId);
5161
- const platform = Platform11.OS;
5436
+ const platform = Platform12.OS;
5162
5437
  const distinctId = PaywalloClient.getDistinctId();
5163
5438
  const appKey = apiClient.getAppKey();
5164
5439
  const body = {
@@ -5190,200 +5465,6 @@ var init_IAPValidator = __esm({
5190
5465
  }
5191
5466
  });
5192
5467
 
5193
- // src/domains/iap/NativeStoreKit.ts
5194
- var NativeStoreKit_exports = {};
5195
- __export(NativeStoreKit_exports, {
5196
- NativeStoreKit: () => NativeStoreKit,
5197
- nativeStoreKit: () => nativeStoreKit
5198
- });
5199
- import { NativeEventEmitter as NativeEventEmitter3, NativeModules as NativeModules6, Platform as Platform12 } from "react-native";
5200
- function isAvailable3() {
5201
- return PaywalloStoreKitNative != null;
5202
- }
5203
- function mapNativeProduct(native) {
5204
- const product = {
5205
- productId: native.productId,
5206
- title: native.title,
5207
- description: native.description,
5208
- price: native.price,
5209
- priceValue: native.priceValue,
5210
- currency: native.currency ?? "USD",
5211
- localizedPrice: native.localizedPrice,
5212
- type: mapProductType(native.type),
5213
- subscriptionPeriod: native.subscriptionPeriod,
5214
- introductoryPrice: native.introductoryPrice,
5215
- introductoryPriceValue: native.introductoryPriceValue,
5216
- freeTrialPeriod: native.freeTrialPeriod
5217
- };
5218
- if (native.subscriptionPeriod) {
5219
- const prices = calculatePerPeriodPrices(native.priceValue, native.subscriptionPeriod);
5220
- product.pricePerMonth = prices.perMonth;
5221
- product.pricePerWeek = prices.perWeek;
5222
- product.pricePerDay = prices.perDay;
5223
- }
5224
- return product;
5225
- }
5226
- function mapProductType(type) {
5227
- switch (type) {
5228
- case "subscription":
5229
- case "autoRenewable":
5230
- return "subscription";
5231
- case "consumable":
5232
- return "consumable";
5233
- default:
5234
- return "non_consumable";
5235
- }
5236
- }
5237
- function calculatePerPeriodPrices(priceValue, period) {
5238
- const totalDays = periodToDays(period);
5239
- if (totalDays <= 0) return {};
5240
- const perDay = priceValue / totalDays;
5241
- const perWeek = perDay * 7;
5242
- const perMonth = perDay * 30;
5243
- return {
5244
- perDay: perDay.toFixed(2),
5245
- perWeek: perWeek.toFixed(2),
5246
- perMonth: perMonth.toFixed(2)
5247
- };
5248
- }
5249
- function periodToDays(period) {
5250
- const match = period.match(/^P(\d+)([DWMY])$/);
5251
- if (!match) return 0;
5252
- const value = parseInt(match[1], 10);
5253
- switch (match[2]) {
5254
- case "D":
5255
- return value;
5256
- case "W":
5257
- return value * 7;
5258
- case "M":
5259
- return value * 30;
5260
- case "Y":
5261
- return value * 365;
5262
- default:
5263
- return 0;
5264
- }
5265
- }
5266
- function mapNativePurchase(native) {
5267
- return {
5268
- productId: native.productId,
5269
- transactionId: native.transactionId,
5270
- transactionDate: native.transactionDate,
5271
- receipt: native.receipt,
5272
- platform: Platform12.OS
5273
- };
5274
- }
5275
- function normalizeTransactionUpdate(raw) {
5276
- if (!raw || typeof raw !== "object") return null;
5277
- const r = raw;
5278
- const type = r.type;
5279
- if (type !== "renewed" && type !== "refunded" && type !== "canceled") return null;
5280
- const transactionId = typeof r.transactionId === "string" ? r.transactionId : null;
5281
- const productId = typeof r.productId === "string" ? r.productId : null;
5282
- if (!transactionId) return null;
5283
- const update = {
5284
- type,
5285
- transactionId,
5286
- productId: productId ?? ""
5287
- };
5288
- if (typeof r.amount === "number") update.amount = r.amount;
5289
- if (typeof r.currency === "string") update.currency = r.currency;
5290
- return update;
5291
- }
5292
- var TRANSACTION_UPDATE_EVENT, PaywalloStoreKitNative, nativeStoreKit, NativeStoreKit;
5293
- var init_NativeStoreKit = __esm({
5294
- "src/domains/iap/NativeStoreKit.ts"() {
5295
- "use strict";
5296
- init_PaywalloClient();
5297
- init_uuid();
5298
- init_IdentityManager();
5299
- init_PurchaseError();
5300
- TRANSACTION_UPDATE_EVENT = "PaywalloTransactionUpdate";
5301
- PaywalloStoreKitNative = NativeModules6.PaywalloStoreKit ?? null;
5302
- nativeStoreKit = {
5303
- isAvailable: isAvailable3,
5304
- async getProducts(productIds) {
5305
- if (!PaywalloStoreKitNative) {
5306
- return [];
5307
- }
5308
- const nativeProducts = await PaywalloStoreKitNative.getProducts(productIds);
5309
- return nativeProducts.map(mapNativeProduct);
5310
- },
5311
- async purchase(productId) {
5312
- if (!PaywalloStoreKitNative) {
5313
- throw new PurchaseError(
5314
- PURCHASE_ERROR_CODES.STORE_NOT_AVAILABLE,
5315
- "StoreKit native module not available"
5316
- );
5317
- }
5318
- const distinctId = identityManager.getDistinctId();
5319
- const rawUuid = distinctId?.startsWith("$paywallo_anon:") ? distinctId.slice("$paywallo_anon:".length) : distinctId;
5320
- const appAccountToken = isValidUUIDv4(rawUuid) ? rawUuid : null;
5321
- if (distinctId && appAccountToken === null) {
5322
- if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] distinctId is not a valid UUID v4 \u2014 passing null to appAccountToken:", distinctId);
5323
- }
5324
- try {
5325
- const result = await PaywalloStoreKitNative.purchase(productId, appAccountToken);
5326
- if (!result || result.pending) {
5327
- return null;
5328
- }
5329
- return mapNativePurchase(result);
5330
- } catch (err) {
5331
- const message = err instanceof Error ? err.message : String(err);
5332
- if (message.toLowerCase().includes("cancel")) {
5333
- throw new PurchaseError(PURCHASE_ERROR_CODES.USER_CANCELLED, message, true);
5334
- }
5335
- throw new PurchaseError(PURCHASE_ERROR_CODES.PURCHASE_FAILED, message);
5336
- }
5337
- },
5338
- async finishTransaction(transactionId) {
5339
- if (!PaywalloStoreKitNative) return;
5340
- try {
5341
- await PaywalloStoreKitNative.finishTransaction(transactionId);
5342
- } catch (err) {
5343
- console.error("[Paywallo] finishTransaction native error", err);
5344
- }
5345
- },
5346
- async getActiveTransactions() {
5347
- if (!PaywalloStoreKitNative) return [];
5348
- try {
5349
- const results = await PaywalloStoreKitNative.getActiveTransactions();
5350
- return results.map(mapNativePurchase);
5351
- } catch (err) {
5352
- console.error("[Paywallo] getActiveTransactions failed", err);
5353
- return [];
5354
- }
5355
- },
5356
- /**
5357
- * Subscribes to native transaction update events (renewals, refunds,
5358
- * cancellations) from StoreKit 2 (`Transaction.updates`) on iOS or
5359
- * `PurchasesUpdatedListener` + refund diff on Android.
5360
- *
5361
- * Returns a subscription with a `remove()` method. The caller is
5362
- * responsible for cleanup (typically on `Paywallo.reset()`).
5363
- *
5364
- * Returns `null` when the native module isn't available (non-mobile env,
5365
- * native module not linked) — callers should treat null as a no-op.
5366
- */
5367
- subscribeToTransactionUpdates(listener) {
5368
- if (!PaywalloStoreKitNative) return null;
5369
- try {
5370
- const EmitterCtor = NativeEventEmitter3;
5371
- const emitter = new EmitterCtor(PaywalloStoreKitNative);
5372
- const sub = emitter.addListener(TRANSACTION_UPDATE_EVENT, (payload) => {
5373
- const update = normalizeTransactionUpdate(payload);
5374
- if (update) listener(update);
5375
- });
5376
- return { remove: () => sub.remove() };
5377
- } catch (err) {
5378
- if (PaywalloClient.getConfig()?.debug) console.log("[Paywallo] subscribeToTransactionUpdates failed", err);
5379
- return null;
5380
- }
5381
- }
5382
- };
5383
- NativeStoreKit = nativeStoreKit;
5384
- }
5385
- });
5386
-
5387
5468
  // src/domains/iap/IAPService.ts
5388
5469
  import { Platform as Platform13 } from "react-native";
5389
5470
  function getIAPService() {
@@ -5484,8 +5565,8 @@ var init_IAPService = __esm({
5484
5565
  stopTransactionListener() {
5485
5566
  this.emitter.stopListener();
5486
5567
  }
5487
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
5488
- async emitTransactionUpdate(_update) {
5568
+ async emitTransactionUpdate(update) {
5569
+ return this.emitter.emitTransactionUpdate(update);
5489
5570
  }
5490
5571
  async purchase(productId, options) {
5491
5572
  if (!nativeStoreKit.isAvailable()) {
@@ -5514,6 +5595,10 @@ var init_IAPService = __esm({
5514
5595
  if (this.debug) console.log(`[Paywallo PURCHASE] ${productId} server validated`);
5515
5596
  await this.finishTransaction(purchase.transactionId);
5516
5597
  if (this.debug) console.log(`[Paywallo PURCHASE] ${productId} finished`);
5598
+ await this.emitter.emitTransactionCompleted(purchase, productId, {
5599
+ paywallId: options?.paywallId,
5600
+ variantId: options?.variantId
5601
+ });
5517
5602
  }).catch(async (err) => {
5518
5603
  const kind = this.validator.classifyValidationError(err);
5519
5604
  if (kind === "server4xx") {
@@ -10185,10 +10270,57 @@ function buildCallbacks() {
10185
10270
  const identifier = "paywallIdentifier" in safe && typeof safe.paywallIdentifier === "string" ? safe.paywallIdentifier : productId;
10186
10271
  log2("onPurchase", { productId, identifier });
10187
10272
  void trackPaywallEvent("purchased", identifier, ts, { product_id: productId });
10273
+ },
10274
+ // Global Superwall event listener — used specifically for `transactionComplete`
10275
+ // which is the only event that exposes the full product (currency/price) +
10276
+ // transaction (storeTransactionId) shape needed to create a server-side
10277
+ // `transactions` row. Other events are skipped (paywallOpen/Dismiss already
10278
+ // handled above with their typed callbacks).
10279
+ onSuperwallEvent: (info) => {
10280
+ const ev = info?.event;
10281
+ if (!ev || ev.event !== "transactionComplete") return;
10282
+ const ts = Date.now();
10283
+ const product = ev.product;
10284
+ const transaction = ev.transaction;
10285
+ if (!product) {
10286
+ log2("transactionComplete missing product \u2014 skipping transaction track");
10287
+ return;
10288
+ }
10289
+ const productId = product.productIdentifier ?? product.id ?? "unknown";
10290
+ const currency = product.currencyCode ?? "USD";
10291
+ const price = product.price ?? 0;
10292
+ const transactionId = transaction?.purchaseToken ?? transaction?.storeTransactionId ?? transaction?.originalTransactionIdentifier ?? `${productId}_${String(ts)}`;
10293
+ const introPrice = product.trialPeriodPrice ?? 0;
10294
+ const isTrial = product.hasFreeTrial === true && introPrice <= 0;
10295
+ const isPaidIntro = introPrice > 0;
10296
+ const paywallIdentifier = ev.paywallInfo?.identifier;
10297
+ log2("onSuperwallEvent: transactionComplete", { productId, currency, price, introPrice, transactionId, isTrial });
10298
+ void (async () => {
10299
+ try {
10300
+ await PaywalloClient.track("transaction", {
10301
+ priority: "critical",
10302
+ properties: {
10303
+ type: isTrial ? "trial_started" : "completed",
10304
+ // Server's IngestTranslator looks for `transaction_id`/`product_id`
10305
+ // /`amount`/`currency`/`paywall_id` keys. A free trial MUST carry
10306
+ // amount=0 (no revenue at start); a paid intro offer carries its
10307
+ // intro amount (real revenue); a normal purchase carries `price`.
10308
+ // `full_price` carries the recurring full price so the server can
10309
+ // value the eventual conversion (free trial) or keep the headline
10310
+ // price alongside the discounted intro amount (paid intro).
10311
+ transaction_id: transactionId,
10312
+ product_id: productId,
10313
+ amount: isTrial ? 0 : isPaidIntro ? introPrice : price,
10314
+ currency,
10315
+ ...isTrial || isPaidIntro ? { full_price: price } : {},
10316
+ ...paywallIdentifier ? { paywall_id: paywallIdentifier } : {}
10317
+ }
10318
+ });
10319
+ } catch (err) {
10320
+ log2("transactionComplete track error", { err: String(err) });
10321
+ }
10322
+ })();
10188
10323
  }
10189
- // onSuperwallEvent (transactionComplete → transaction track) removed —
10190
- // sale/transaction events are sourced exclusively from server-side webhooks
10191
- // (Apple/Google/Superwall/RevenueCat), the single source of truth for revenue.
10192
10324
  };
10193
10325
  }
10194
10326
  function startSuperwallAutoBridge(debug = false) {