capacitor-plugin-cdv-purchase 13.18.0 → 13.20.0

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/README.md CHANGED
@@ -42,6 +42,38 @@ If you're using `cordova-plugin-purchase` in a Capacitor app:
42
42
  3. `npx cap sync`
43
43
  4. Update your import to `import { store, ProductType, Platform } from 'capacitor-plugin-cdv-purchase'` — the API is identical
44
44
 
45
+ ### Behavior change: purchase events at app launch (iOS)
46
+
47
+ The Cordova build used StoreKit 1, which only re-delivers *unfinished* transactions at launch. The Capacitor build uses StoreKit 2, where `store.initialize()` surfaces the user's full set of **current entitlements** by firing `approved` for:
48
+
49
+ - every non-consumable purchase,
50
+ - the latest transaction of each auto-renewable subscription,
51
+ - each non-renewing subscription — **including transactions you already finished**.
52
+
53
+ `restorePurchases()` fires `approved` for the same set.
54
+
55
+ Consumables are **not** re-delivered this way: they never appear in StoreKit 2's current entitlements. Unfinished consumables are still re-delivered once per launch, exactly as under StoreKit 1.
56
+
57
+ **What this means for your app:** with the common pattern
58
+
59
+ ```typescript
60
+ store.when().approved((transaction) => {
61
+ deliverToServer(transaction); // your fulfillment endpoint
62
+ transaction.finish();
63
+ });
64
+ ```
65
+
66
+ `approved` fires for already-fulfilled non-consumables and non-renewing subscriptions on every app launch. Those transactions have `isAcknowledged === true` (you already finished them), so you can skip them without keeping your own list of processed ids:
67
+
68
+ ```typescript
69
+ store.when().approved((transaction) => {
70
+ if (transaction.isAcknowledged) return transaction.finish(); // already delivered
71
+ deliverToServer(transaction).then(() => transaction.finish());
72
+ });
73
+ ```
74
+
75
+ `isAcknowledged` is `false` for transactions you haven't finished yet, and becomes `true` once you call `finish()`. It's the same flag Google Play uses for acknowledged purchases. On iOS it is only set by this Capacitor plugin: Cordova apps (StoreKit 1, or the `cordova-plugin-purchase-storekit2` plugin) leave it `undefined`. Keeping server-side fulfillment idempotent on the transaction id is still good practice: an app killed between delivery and `finish()` gets the transaction again, unfinished.
76
+
45
77
  ## Device info for receipt validation
46
78
 
47
79
  When validating receipts, the plugin sends device information (OS, model, manufacturer) to the validator to support fraud detection and support requests. In Capacitor apps, this information is collected automatically via `@capacitor/device` through the Capacitor plugin proxy — no additional plugin installation is required.
package/dist/index.js CHANGED
@@ -516,6 +516,10 @@ var CdvPurchase;
516
516
  this.receiptsToValidate = new ReceiptsToValidate();
517
517
  /** List of verified receipts */
518
518
  this.verifiedReceipts = [];
519
+ /** Maximum number of validation attempts for a receipt when failures are temporary */
520
+ this.maxAttempts = 4;
521
+ /** Number of failed attempts in a row, per receipt */
522
+ this.failedAttempts = new Map();
519
523
  this.numRequests = 0;
520
524
  this.numResponses = 0;
521
525
  /**
@@ -529,6 +533,36 @@ var CdvPurchase;
529
533
  this.controller = controller;
530
534
  this.log = log.child('Validator');
531
535
  }
536
+ /**
537
+ * Schedule a new validation if the failure is temporary and attempts remain.
538
+ *
539
+ * @return true if a retry was scheduled
540
+ */
541
+ scheduleRetry(receipt, payload) {
542
+ var _a;
543
+ if (payload.ok || !isRetriableError(payload)) {
544
+ this.failedAttempts.delete(receipt);
545
+ if (payload.ok && this.retry)
546
+ this.retry.retryTimeout = this.retry.minTimeout;
547
+ return false;
548
+ }
549
+ const attempts = ((_a = this.failedAttempts.get(receipt)) !== null && _a !== void 0 ? _a : 0) + 1;
550
+ if (attempts >= this.maxAttempts) {
551
+ this.log.warn(`Validation failed ${attempts} times, giving up: ${payload.message}`);
552
+ this.failedAttempts.delete(receipt);
553
+ return false;
554
+ }
555
+ this.failedAttempts.set(receipt, attempts);
556
+ if (!this.retry)
557
+ this.retry = new Internal.Retry();
558
+ this.log.info(`Validation failed (${payload.message}), retry in ${this.retry.retryTimeout}ms (attempt ${attempts + 1}/${this.maxAttempts})`);
559
+ this.retry.retry(() => {
560
+ // the request is still counted as pending, so don't count it again
561
+ this.receiptsToValidate.add(receipt);
562
+ this.run();
563
+ });
564
+ return true;
565
+ }
532
566
  incrRequestsCounter() {
533
567
  this.numRequests = (this.numRequests + 1) | 0;
534
568
  this.log.debug(`Validation requests=${this.numRequests} responses=${this.numResponses}`);
@@ -569,6 +603,9 @@ var CdvPurchase;
569
603
  const onResponse = (r) => __awaiter(this, void 0, void 0, function* () {
570
604
  var _a;
571
605
  const { receipt, payload } = r;
606
+ // a retried request is still pending: only count final responses
607
+ if (this.scheduleRetry(receipt, payload))
608
+ return;
572
609
  this.incrResponsesCounter();
573
610
  try {
574
611
  const adapter = this.controller.adapters.find(receipt.platform);
@@ -590,11 +627,11 @@ var CdvPurchase;
590
627
  this.controller.verifiedCallbacks.trigger(vr, 'payload_expired');
591
628
  }
592
629
  else {
593
- this.controller.unverifiedCallbacks.trigger({ receipt, payload }, 'no_verified_receipt');
630
+ this.controller.unverifiedCallbacks.trigger({ receipt, payload, transient: false }, 'no_verified_receipt');
594
631
  }
595
632
  }
596
633
  else {
597
- this.controller.unverifiedCallbacks.trigger({ receipt, payload }, 'validator_error');
634
+ this.controller.unverifiedCallbacks.trigger({ receipt, payload, transient: isRetriableError(payload) }, 'validator_error');
598
635
  }
599
636
  }
600
637
  catch (err) {
@@ -603,7 +640,7 @@ var CdvPurchase;
603
640
  ok: false,
604
641
  code: CdvPurchase.ErrorCode.VERIFICATION_FAILED,
605
642
  message: err.message,
606
- } }, 'validator_exception');
643
+ }, transient: true }, 'validator_exception');
607
644
  }
608
645
  });
609
646
  receipts.forEach(receipt => this.runOnReceipt(receipt, onResponse));
@@ -763,10 +800,13 @@ var CdvPurchase;
763
800
  data: { latest_receipt: (_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.latest_receipt },
764
801
  }
765
802
  });
766
- this.cache[bodyTransactionHash] = {
767
- payload: response,
768
- expires: (+new Date()) + 120000, // expires in 2 minutes
769
- };
803
+ // don't cache temporary errors, so a retry makes a new request
804
+ if (response.ok || !isTransientError(response)) {
805
+ this.cache[bodyTransactionHash] = {
806
+ payload: response,
807
+ expires: (+new Date()) + 120000, // expires in 2 minutes
808
+ };
809
+ }
770
810
  callback({ receipt, payload: response });
771
811
  },
772
812
  error: (status, message, data) => {
@@ -788,6 +828,44 @@ var CdvPurchase;
788
828
  }
789
829
  }
790
830
  Internal.Validator = Validator;
831
+ /** Validator error code: the validator couldn't reach the store (Apple, Google, ...). */
832
+ const VALIDATOR_CONNECTION_FAILED = 6778002;
833
+ /**
834
+ * True if a validation error is temporary (validator or store unreachable,
835
+ * server error, timeout, no network) rather than a definitive answer about
836
+ * the receipt.
837
+ *
838
+ * Error responses from the validator come in two shapes:
839
+ * - HTTP 200 with `{ ok: false, status, code }` in the body (iaptic does this,
840
+ * so a store outage is `status: 502, code: 6778002` in the body);
841
+ * - a failed HTTP request, reported by the plugin as `code: COMMUNICATION`
842
+ * with the transport status: 5xx, 408 (timeout), 417 (unreadable response,
843
+ * e.g. a captive portal page), 0 or negative (no network, as reported by
844
+ * XMLHttpRequest or cordova-plugin-advanced-http).
845
+ */
846
+ function isTransientError(payload) {
847
+ const status = payload.status;
848
+ if (typeof status === 'number' && status >= 500)
849
+ return true;
850
+ if (payload.code === VALIDATOR_CONNECTION_FAILED)
851
+ return true;
852
+ if (payload.code === CdvPurchase.ErrorCode.COMMUNICATION) {
853
+ if (typeof status !== 'number' || status <= 0)
854
+ return true;
855
+ if (status === CdvPurchase.Utils.Ajax.HTTP_REQUEST_TIMEOUT || status === 417)
856
+ return true;
857
+ }
858
+ return false;
859
+ }
860
+ Internal.isTransientError = isTransientError;
861
+ /**
862
+ * True if a validation failure is worth retrying: a temporary error
863
+ * (see isTransientError) or an invalid response from the validator.
864
+ * Validator verdicts (invalid receipt, expired, ...) are not retried.
865
+ */
866
+ function isRetriableError(payload) {
867
+ return isTransientError(payload) || payload.code === CdvPurchase.ErrorCode.BAD_RESPONSE;
868
+ }
791
869
  /**
792
870
  * Check if a payload looks like a valid validator response.
793
871
  *
@@ -1660,8 +1738,8 @@ var CdvPurchase;
1660
1738
  (function (CdvPurchase) {
1661
1739
  /**
1662
1740
  * Persist a subset of {@link VerifiedPurchase} to device storage so that
1663
- * `store.owned()` works when the device is offline or has just restarted
1664
- * without connectivity.
1741
+ * `offline.isOwned()` can answer "is this product owned?" when the device
1742
+ * is offline or has just restarted without connectivity.
1665
1743
  *
1666
1744
  * Phase 1 — unsigned cache. No JWT, no crypto, no server changes.
1667
1745
  *
@@ -1975,7 +2053,7 @@ var CdvPurchase;
1975
2053
  /**
1976
2054
  * Current release number of the plugin.
1977
2055
  */
1978
- CdvPurchase.PLUGIN_VERSION = '13.18.0';
2056
+ CdvPurchase.PLUGIN_VERSION = '13.20.0';
1979
2057
  /**
1980
2058
  * Entry class of the plugin.
1981
2059
  */
@@ -3640,13 +3718,19 @@ var CdvPurchase;
3640
3718
  yield this.receiptsUpdated.wait();
3641
3719
  this.log.info('finish: ' + transactionIdentifier + ' - ' + productId);
3642
3720
  this.removeTransactionInProgress(productId);
3643
- yield this.upsertTransaction(productId, transactionIdentifier, CdvPurchase.TransactionState.FINISHED);
3721
+ const finishedTransaction = yield this.upsertTransaction(productId, transactionIdentifier, CdvPurchase.TransactionState.FINISHED);
3722
+ finishedTransaction.isAcknowledged = true;
3644
3723
  this.receiptsUpdated.call();
3645
3724
  }),
3646
- restored: (transactionIdentifier, productId, originalTransactionIdentifier, transactionDate, discountId, expirationDate, jwsRepresentation, quantity) => __awaiter(this, void 0, void 0, function* () {
3725
+ restored: (transactionIdentifier, productId, originalTransactionIdentifier, transactionDate, discountId, expirationDate, jwsRepresentation, quantity, isFinished) => __awaiter(this, void 0, void 0, function* () {
3647
3726
  this.log.info('restore: ' + transactionIdentifier + ' - ' + productId);
3648
3727
  const transaction = yield this.upsertTransaction(productId, transactionIdentifier, CdvPurchase.TransactionState.APPROVED);
3649
3728
  transaction.refresh(productId, originalTransactionIdentifier, transactionDate, discountId, expirationDate, jwsRepresentation, quantity);
3729
+ // StoreKit 2 re-emits finished entitlements (non-consumables,
3730
+ // non-renewing subscriptions) at every launch. Expose the finished
3731
+ // state the same way Google Play does, so apps can skip them in approved().
3732
+ if (isFinished !== undefined)
3733
+ transaction.isAcknowledged = isFinished;
3650
3734
  this.receiptsUpdated.call();
3651
3735
  }),
3652
3736
  receiptsRefreshed: (receipt) => {
@@ -4278,7 +4362,7 @@ var CdvPurchase;
4278
4362
  }
4279
4363
  // Listen for transaction updates from native
4280
4364
  plugin.addListener('transactionUpdated', (data) => {
4281
- this.transactionUpdated(data.state, data.errorCode, data.errorText, data.transactionIdentifier, data.productId, data.transactionReceipt, data.originalTransactionIdentifier, data.transactionDate, data.discountId, data.expirationDate, data.jwsRepresentation, data.quantity);
4365
+ this.transactionUpdated(data.state, data.errorCode, data.errorText, data.transactionIdentifier, data.productId, data.transactionReceipt, data.originalTransactionIdentifier, data.transactionDate, data.discountId, data.expirationDate, data.jwsRepresentation, data.quantity, data.isFinished);
4282
4366
  });
4283
4367
  plugin.addListener('restoreCompleted', () => {
4284
4368
  this.restoreCompletedTransactionsFinished();
@@ -4299,7 +4383,7 @@ var CdvPurchase;
4299
4383
  const pending = this.pendingTransactionUpdates;
4300
4384
  this.pendingTransactionUpdates = [];
4301
4385
  for (const args of pending) {
4302
- this.transactionUpdated(args.state, args.errorCode, args.errorText, args.transactionIdentifier, args.productId, args.transactionReceipt, args.originalTransactionIdentifier, args.transactionDate, args.discountId, args.expirationDate, args.jwsRepresentation, args.quantity);
4386
+ this.transactionUpdated(args.state, args.errorCode, args.errorText, args.transactionIdentifier, args.productId, args.transactionReceipt, args.originalTransactionIdentifier, args.transactionDate, args.discountId, args.expirationDate, args.jwsRepresentation, args.quantity, args.isFinished);
4303
4387
  }
4304
4388
  if (this.options.ready)
4305
4389
  this.options.ready();
@@ -4383,13 +4467,13 @@ var CdvPurchase;
4383
4467
  .catch((err) => errorCb(CdvPurchase.ErrorCode.LOAD, (err === null || err === void 0 ? void 0 : err.message) || 'loadReceipts failed'));
4384
4468
  }
4385
4469
  // Called when the native side sends a transaction update
4386
- transactionUpdated(state, errorCode, errorText, transactionIdentifier, productId, transactionReceipt, originalTransactionIdentifier, transactionDate, discountId, expirationDate, jwsRepresentation, quantity) {
4470
+ transactionUpdated(state, errorCode, errorText, transactionIdentifier, productId, transactionReceipt, originalTransactionIdentifier, transactionDate, discountId, expirationDate, jwsRepresentation, quantity, isFinished) {
4387
4471
  if (!this.initialized) {
4388
4472
  this.pendingTransactionUpdates.push({
4389
4473
  state, errorCode, errorText, transactionIdentifier,
4390
4474
  productId, transactionReceipt, originalTransactionIdentifier,
4391
4475
  transactionDate, discountId, expirationDate, jwsRepresentation,
4392
- quantity,
4476
+ quantity, isFinished,
4393
4477
  });
4394
4478
  return;
4395
4479
  }
@@ -4425,7 +4509,7 @@ var CdvPurchase;
4425
4509
  // purchased, but is meaningless here: consumables cannot be
4426
4510
  // restored, so restored transactions are always quantity 1.
4427
4511
  if (this.options.restored) {
4428
- this.options.restored(transactionIdentifier, productId, originalTransactionIdentifier, transactionDate, discountId, expirationDate, jwsRepresentation, quantity);
4512
+ this.options.restored(transactionIdentifier, productId, originalTransactionIdentifier, transactionDate, discountId, expirationDate, jwsRepresentation, quantity, isFinished);
4429
4513
  }
4430
4514
  break;
4431
4515
  case 'PaymentTransactionStateDeferred':
@@ -7767,7 +7851,7 @@ var CdvPurchase;
7767
7851
  this.products = [{ id: productId }];
7768
7852
  this.transactionId = purchase.transactionId;
7769
7853
  this.purchaseId = purchase.purchaseId;
7770
- this.purchaseDate = new Date(purchase.purchaseDate);
7854
+ this.purchaseDate = purchase.purchaseDate ? new Date(purchase.purchaseDate) : undefined;
7771
7855
  this.expirationDate = purchase.expirationDate ? new Date(purchase.expirationDate) : undefined;
7772
7856
  this.lastRenewalDate = purchase.lastRenewalDate ? new Date(purchase.lastRenewalDate) : undefined;
7773
7857
  this.renewalIntent = purchase.renewalIntent === 'Renew' ? CdvPurchase.RenewalIntent.RENEW : CdvPurchase.RenewalIntent.LAPSE;
@@ -7886,7 +7970,7 @@ var CdvPurchase;
7886
7970
  billingPeriod: pp.billingPeriod,
7887
7971
  paymentMode: pp.paymentMode,
7888
7972
  recurrenceMode: pp.recurrenceMode,
7889
- price: window.IapticJS.Utils.formatCurrency(pp.priceMicros, pp.currency) // Use Utils for formatting
7973
+ price: pp.currency ? window.IapticJS.Utils.formatCurrency(pp.priceMicros, pp.currency) : '' // Use Utils for formatting
7890
7974
  })),
7891
7975
  }, this.context.apiDecorators);
7892
7976
  product.addOffer(offer);
@@ -9358,6 +9442,7 @@ var CdvPurchase;
9358
9442
  this.latestReceipt = response.latest_receipt;
9359
9443
  this.nativeTransactions = [response.transaction];
9360
9444
  this.warning = response.warning;
9445
+ this.cache = response.cache;
9361
9446
  this.validationDate = response.date ? new Date(response.date) : new Date();
9362
9447
  Object.defineProperty(this, 'raw', { 'enumerable': false, get() { return response; } });
9363
9448
  Object.defineProperty(this, 'finish', { 'enumerable': false, get() { return () => decorator.finish(this); } });
@@ -9379,6 +9464,7 @@ var CdvPurchase;
9379
9464
  this.latestReceipt = response.latest_receipt;
9380
9465
  this.nativeTransactions = [response.transaction];
9381
9466
  this.warning = response.warning;
9467
+ this.cache = response.cache;
9382
9468
  }
9383
9469
  /** Finish all transactions in the receipt */
9384
9470
  finish() {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["esm/index.js","../build/entry.mjs"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\n// Set the marker so the shared store.js knows Capacitor native bridges are available\nif (typeof window !== 'undefined') {\n window.CdvPurchaseCapacitor = { installed: true };\n}\nconst PurchasePlugin = registerPlugin('PurchasePlugin');\nexport { PurchasePlugin };\n","// Capacitor bridge — sets CdvPurchaseCapacitor marker and registers native plugin\nexport { PurchasePlugin } from '../dist/esm/index.js';\n\n// Store runtime — executes the IIFE, creates window.CdvPurchase\nimport { CdvPurchase } from 'virtual:store-js';\n\n// Re-export the full namespace\nexport { CdvPurchase };\n\n// Convenience named exports\nexport const store = CdvPurchase.store;\nexport const Store = CdvPurchase.Store;\nexport const ProductType = CdvPurchase.ProductType;\nexport const Platform = CdvPurchase.Platform;\nexport const LogLevel = CdvPurchase.LogLevel;\nexport const ErrorCode = CdvPurchase.ErrorCode;\nexport const Logger = CdvPurchase.Logger;\nexport const Iaptic = CdvPurchase.Iaptic;\n"],"names":[],"mappings":";;AACA;AACA,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE;AACnC,IAAI,MAAM,CAAC,oBAAoB,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE;AACrD;AACK,MAAC,cAAc,GAAG,cAAc,CAAC,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACLtD;;AASA;AACY,MAAC,KAAK,GAAG,WAAW,CAAC;AACrB,MAAC,KAAK,GAAG,WAAW,CAAC;AACrB,MAAC,WAAW,GAAG,WAAW,CAAC;AAC3B,MAAC,QAAQ,GAAG,WAAW,CAAC;AACxB,MAAC,QAAQ,GAAG,WAAW,CAAC;AACxB,MAAC,SAAS,GAAG,WAAW,CAAC;AACzB,MAAC,MAAM,GAAG,WAAW,CAAC;AACtB,MAAC,MAAM,GAAG,WAAW,CAAC;;;;"}
1
+ {"version":3,"file":"index.js","sources":["esm/index.js","../build/entry.mjs"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\n// Set the marker so the shared store.js knows Capacitor native bridges are available\nif (typeof window !== 'undefined') {\n window.CdvPurchaseCapacitor = { installed: true };\n}\nconst PurchasePlugin = registerPlugin('PurchasePlugin');\nexport { PurchasePlugin };\n","// Capacitor bridge — sets CdvPurchaseCapacitor marker and registers native plugin\nexport { PurchasePlugin } from '../dist/esm/index.js';\n\n// Store runtime — executes the IIFE, creates window.CdvPurchase\nimport { CdvPurchase } from 'virtual:store-js';\n\n// Re-export the full namespace\nexport { CdvPurchase };\n\n// Convenience named exports\nexport const store = CdvPurchase.store;\nexport const Store = CdvPurchase.Store;\nexport const ProductType = CdvPurchase.ProductType;\nexport const Platform = CdvPurchase.Platform;\nexport const LogLevel = CdvPurchase.LogLevel;\nexport const ErrorCode = CdvPurchase.ErrorCode;\nexport const Logger = CdvPurchase.Logger;\nexport const Iaptic = CdvPurchase.Iaptic;\n"],"names":[],"mappings":";;AACA;AACA,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE;AACnC,IAAI,MAAM,CAAC,oBAAoB,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE;AACrD;AACK,MAAC,cAAc,GAAG,cAAc,CAAC,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACLtD;;AASA;AACY,MAAC,KAAK,GAAG,WAAW,CAAC;AACrB,MAAC,KAAK,GAAG,WAAW,CAAC;AACrB,MAAC,WAAW,GAAG,WAAW,CAAC;AAC3B,MAAC,QAAQ,GAAG,WAAW,CAAC;AACxB,MAAC,QAAQ,GAAG,WAAW,CAAC;AACxB,MAAC,SAAS,GAAG,WAAW,CAAC;AACzB,MAAC,MAAM,GAAG,WAAW,CAAC;AACtB,MAAC,MAAM,GAAG,WAAW,CAAC;;;;"}
@@ -5,6 +5,11 @@ import StoreKit
5
5
  @available(iOS 15.0, *)
6
6
  private class SK2State {
7
7
  var products: [String: Product] = [:]
8
+ /// Transactions emitted to JS and not yet finished. Access must be
9
+ /// serialized on the main thread (same as processedTransactionIds below):
10
+ /// the detached Transaction.updates observer, emitTransactionUpdate(),
11
+ /// finish(), and clearExpiredUnfinishedTransactions() all read or
12
+ /// mutate it from concurrent async contexts.
8
13
  var unfinishedTransactions: [String: Transaction] = [:]
9
14
  var transactionObserverTask: Task<Void, Never>?
10
15
  /// Transaction IDs already emitted to JS. Prevents duplicate delivery when
@@ -46,20 +51,21 @@ public class PurchasePlugin: CAPPlugin, CAPBridgedPlugin {
46
51
 
47
52
  // MARK: - Lifecycle
48
53
 
49
- override public func load() {
50
- if #available(iOS 15.0, *) {
51
- startTransactionObserver()
52
- }
53
- }
54
-
55
54
  deinit {
56
55
  if #available(iOS 15.0, *) {
57
56
  sk2.transactionObserverTask?.cancel()
58
57
  }
59
58
  }
60
59
 
60
+ /// Start listening to Transaction.updates. Called at the end of init(),
61
+ /// NOT at plugin load: Transaction.updates delivers unfinished
62
+ /// transactions as soon as iteration begins, and before JS called init()
63
+ /// no listener is attached — the events would be lost while their ids
64
+ /// still land in processedTransactionIds, hiding unfinished purchases
65
+ /// for the rest of the session.
61
66
  @available(iOS 15.0, *)
62
67
  private func startTransactionObserver() {
68
+ guard sk2.transactionObserverTask == nil else { return }
63
69
  sk2.transactionObserverTask = Task.detached { [weak self] in
64
70
  for await result in Transaction.updates {
65
71
  guard let self = self else { return }
@@ -73,22 +79,18 @@ public class PurchasePlugin: CAPPlugin, CAPBridgedPlugin {
73
79
  let jwsRepresentation = result.jwsRepresentation
74
80
  switch result {
75
81
  case .verified(let transaction):
76
- // Serialize access to processedTransactionIds on the main thread
77
- // since this method runs from a detached Task while init()/purchase()
78
- // also mutate the set.
79
- let shouldEmit: Bool = await MainActor.run {
80
- if sk2.processedTransactionIds.contains(transaction.id) {
81
- debugLog("Transaction.updates: skipping duplicate id=\(transaction.id)")
82
- return false
83
- }
84
- sk2.processedTransactionIds.insert(transaction.id)
85
- return true
82
+ guard await markEmittedToJs(transaction.id) else {
83
+ debugLog("Transaction.updates: skipping duplicate id=\(transaction.id)")
84
+ return
86
85
  }
87
- guard shouldEmit else { return }
88
86
  await emitTransactionUpdate(transaction, state: "PaymentTransactionStatePurchased",
89
87
  jwsRepresentation: jwsRepresentation)
90
88
  case .unverified(let transaction, _):
91
- // Emit as Purchased — let JS layer handle verification
89
+ // Emit as Purchased — let JS layer handle verification.
90
+ // Deliberately NOT deduped via markEmittedToJs: marking an
91
+ // unverified id would suppress the transaction for the rest of
92
+ // the session even if a later delivery verifies. A double-emit of
93
+ // the same JWS is harmless — JS re-validates it.
92
94
  await emitTransactionUpdate(transaction, state: "PaymentTransactionStatePurchased",
93
95
  jwsRepresentation: jwsRepresentation)
94
96
  }
@@ -104,29 +106,55 @@ public class PurchasePlugin: CAPPlugin, CAPBridgedPlugin {
104
106
  debugEnabled = call.getBool("debug", false)
105
107
  debugLog("init")
106
108
 
107
- // Load current entitlements and emit to JS so existing subscriptions
108
- // are visible immediately on app launch (without requiring a manual restore).
109
+ // Load current entitlements and unfinished transactions, and emit them
110
+ // to JS so existing purchases are visible immediately on app launch
111
+ // (without requiring a manual restore).
109
112
  Task {
110
- for await result in Transaction.currentEntitlements {
113
+ let (entitlements, unfinishedIds) = await self.entitlementsWithUnfinishedIds()
114
+ for result in entitlements {
111
115
  switch result {
112
116
  case .verified(let transaction):
113
117
  if transaction.isUpgraded {
114
118
  debugLog("init: skipping upgraded entitlement id=\(transaction.id) product=\(transaction.productID)")
115
119
  continue
116
120
  }
117
- if self.sk2.processedTransactionIds.contains(transaction.id) {
118
- debugLog("init: skipping already-processed entitlement id=\(transaction.id)")
121
+ guard await self.markEmittedToJs(transaction.id) else {
122
+ debugLog("init: skipping already-emitted entitlement id=\(transaction.id)")
119
123
  continue
120
124
  }
121
- self.sk2.processedTransactionIds.insert(transaction.id)
122
- self.sk2.unfinishedTransactions[String(transaction.id)] = transaction
123
125
  await self.emitTransactionUpdate(transaction,
124
126
  state: "PaymentTransactionStateRestored",
125
- jwsRepresentation: result.jwsRepresentation)
127
+ jwsRepresentation: result.jwsRepresentation,
128
+ isFinished: !unfinishedIds.contains(transaction.id))
126
129
  case .unverified(let transaction, let error):
127
130
  debugLog("init: unverified entitlement id=\(transaction.id) product=\(transaction.productID) error=\(error)")
128
131
  }
129
132
  }
133
+ // Emit every transaction the app hasn't finished yet. This is the
134
+ // only reliable source for unfinished consumables: they never
135
+ // appear in currentEntitlements, so without this pass an
136
+ // unfinished consumable would hang in limbo for the session.
137
+ for await result in Transaction.unfinished {
138
+ switch result {
139
+ case .verified(let transaction):
140
+ if transaction.isUpgraded {
141
+ debugLog("init: skipping upgraded unfinished transaction id=\(transaction.id) product=\(transaction.productID)")
142
+ continue
143
+ }
144
+ guard await self.markEmittedToJs(transaction.id) else { continue }
145
+ debugLog("init: emitting unfinished transaction id=\(transaction.id) product=\(transaction.productID)")
146
+ await self.emitTransactionUpdate(transaction,
147
+ state: "PaymentTransactionStateRestored",
148
+ jwsRepresentation: result.jwsRepresentation,
149
+ isFinished: false)
150
+ case .unverified(let transaction, let error):
151
+ debugLog("init: unverified unfinished transaction id=\(transaction.id) product=\(transaction.productID) error=\(error)")
152
+ }
153
+ }
154
+ // Only now start observing Transaction.updates — see the comment
155
+ // on startTransactionObserver() for why starting earlier loses
156
+ // unfinished transactions.
157
+ self.startTransactionObserver()
130
158
  call.resolve()
131
159
  }
132
160
  }
@@ -206,12 +234,13 @@ public class PurchasePlugin: CAPPlugin, CAPBridgedPlugin {
206
234
  let jwsRepresentation = verification.jwsRepresentation
207
235
  switch verification {
208
236
  case .verified(let transaction):
209
- sk2.processedTransactionIds.insert(transaction.id)
237
+ _ = await markEmittedToJs(transaction.id)
210
238
  await emitTransactionUpdate(transaction,
211
239
  state: "PaymentTransactionStatePurchased",
212
240
  jwsRepresentation: jwsRepresentation)
213
241
  case .unverified(let transaction, _):
214
- // Emit as Purchased — let JS handle verification
242
+ // Emit as Purchased — let JS handle verification.
243
+ // Deliberately not deduped — see handleTransactionUpdate.
215
244
  await emitTransactionUpdate(transaction,
216
245
  state: "PaymentTransactionStatePurchased",
217
246
  jwsRepresentation: jwsRepresentation)
@@ -254,9 +283,17 @@ public class PurchasePlugin: CAPPlugin, CAPBridgedPlugin {
254
283
  debugLog("finish: \(transactionId)")
255
284
 
256
285
  Task {
257
- if let transaction = sk2.unfinishedTransactions[transactionId] {
286
+ // Atomically claim the transaction on the main thread: read and
287
+ // removal must be one step, or two concurrent finish() calls for
288
+ // the same id could both proceed. Serialized because the detached
289
+ // Transaction.updates observer also mutates the dictionary.
290
+ let transaction: Transaction? = await MainActor.run {
291
+ guard let t = sk2.unfinishedTransactions[transactionId] else { return nil }
292
+ _ = sk2.unfinishedTransactions.removeValue(forKey: transactionId)
293
+ return t
294
+ }
295
+ if let transaction = transaction {
258
296
  await transaction.finish()
259
- sk2.unfinishedTransactions.removeValue(forKey: transactionId)
260
297
  // Note: do NOT remove from processedTransactionIds here.
261
298
  // Once a transaction ID has been emitted to JS, it must stay in the set
262
299
  // for the lifetime of the session to prevent Transaction.updates from
@@ -289,17 +326,21 @@ public class PurchasePlugin: CAPPlugin, CAPBridgedPlugin {
289
326
  Task {
290
327
  do {
291
328
  try await AppStore.sync()
292
- for await result in Transaction.currentEntitlements {
329
+ let (entitlements, unfinishedIds) = await entitlementsWithUnfinishedIds()
330
+ for result in entitlements {
293
331
  switch result {
294
332
  case .verified(let transaction):
295
333
  if transaction.isUpgraded {
296
334
  debugLog("restore: skipping upgraded entitlement id=\(transaction.id) product=\(transaction.productID)")
297
335
  continue
298
336
  }
299
- sk2.processedTransactionIds.insert(transaction.id)
337
+ // Restore deliberately re-emits transactions already
338
+ // delivered this session — mark, but ignore the result.
339
+ _ = await markEmittedToJs(transaction.id)
300
340
  await emitTransactionUpdate(transaction,
301
341
  state: "PaymentTransactionStateRestored",
302
- jwsRepresentation: result.jwsRepresentation)
342
+ jwsRepresentation: result.jwsRepresentation,
343
+ isFinished: !unfinishedIds.contains(transaction.id))
303
344
  case .unverified(let transaction, let error):
304
345
  debugLog("restore: unverified entitlement id=\(transaction.id) product=\(transaction.productID) error=\(error)")
305
346
  }
@@ -407,19 +448,63 @@ public class PurchasePlugin: CAPPlugin, CAPBridgedPlugin {
407
448
  if let expirationDate = transaction.expirationDate, expirationDate < Date() {
408
449
  debugLog("clearExpired: finishing expired transaction id=\(transaction.id) product=\(transaction.productID) expired=\(expirationDate)")
409
450
  await transaction.finish()
410
- sk2.unfinishedTransactions.removeValue(forKey: String(transaction.id))
451
+ await MainActor.run {
452
+ _ = sk2.unfinishedTransactions.removeValue(forKey: String(transaction.id))
453
+ }
411
454
  // Note: do NOT remove from processedTransactionIds.
412
455
  // The set must grow monotonically to prevent re-delivery via Transaction.updates.
413
456
  }
414
457
  }
415
458
  }
416
459
 
460
+ /// Current entitlements, plus the ids of every transaction the app has
461
+ /// not finished yet. Used to tell finished entitlements apart from
462
+ /// unfinished ones: finished non-consumables and non-renewing
463
+ /// subscriptions stay in Transaction.currentEntitlements and are
464
+ /// re-emitted at every launch.
465
+ ///
466
+ /// The unfinished snapshot is taken AFTER listing entitlements, so an
467
+ /// entitlement is never wrongly reported as finished: anything listed
468
+ /// that is still unfinished is in the snapshot. The only possible error
469
+ /// is the safe one (finished in between, reported as unfinished).
470
+ @available(iOS 15.0, *)
471
+ private func entitlementsWithUnfinishedIds() async -> ([VerificationResult<Transaction>], Set<UInt64>) {
472
+ var entitlements: [VerificationResult<Transaction>] = []
473
+ for await result in Transaction.currentEntitlements {
474
+ entitlements.append(result)
475
+ }
476
+ var ids = Set<UInt64>()
477
+ for await result in Transaction.unfinished {
478
+ ids.insert(result.unsafePayloadValue.id)
479
+ }
480
+ return (entitlements, ids)
481
+ }
482
+
483
+ /// Atomically check-and-mark a transaction id as emitted to the JS layer.
484
+ /// Returns false when it was already emitted this session. "Emitted" is
485
+ /// unrelated to Apple's finished state — it only prevents duplicate
486
+ /// delivery of the same transaction within one app session. Serialized on
487
+ /// the main thread because the Transaction.updates observer runs in a
488
+ /// detached Task while init()/restore()/purchase() run in their own Tasks.
489
+ @available(iOS 15.0, *)
490
+ private func markEmittedToJs(_ id: UInt64) async -> Bool {
491
+ return await MainActor.run {
492
+ if sk2.processedTransactionIds.contains(id) { return false }
493
+ sk2.processedTransactionIds.insert(id)
494
+ return true
495
+ }
496
+ }
497
+
417
498
  @available(iOS 15.0, *)
418
499
  private func emitTransactionUpdate(_ transaction: Transaction, state: String,
419
500
  errorCode: Int? = nil, errorText: String? = nil,
420
- jwsRepresentation: String? = nil) async {
501
+ jwsRepresentation: String? = nil,
502
+ isFinished: Bool? = nil) async {
421
503
  let transactionId = String(transaction.id)
422
- sk2.unfinishedTransactions[transactionId] = transaction
504
+ // Serialized on the main thread — see SK2State.unfinishedTransactions.
505
+ await MainActor.run {
506
+ sk2.unfinishedTransactions[transactionId] = transaction
507
+ }
423
508
 
424
509
  var data: [String: Any] = [
425
510
  "state": state,
@@ -443,6 +528,11 @@ public class PurchasePlugin: CAPPlugin, CAPBridgedPlugin {
443
528
  if let jws = jwsRepresentation {
444
529
  data["jwsRepresentation"] = jws
445
530
  }
531
+ // Whether the app already called finish() on this transaction. Only
532
+ // set when known (Restored emissions), so JS can skip re-delivery.
533
+ if let isFinished = isFinished {
534
+ data["isFinished"] = isFinished
535
+ }
446
536
 
447
537
  notifyListeners("transactionUpdated", data: data)
448
538
  }
@@ -524,7 +614,7 @@ public class PurchasePlugin: CAPPlugin, CAPBridgedPlugin {
524
614
  private func paymentModeToString(_ mode: Product.SubscriptionOffer.PaymentMode) -> String {
525
615
  switch mode {
526
616
  case .payAsYouGo: return "PayAsYouGo"
527
- case .payUpFront: return "PayUpFront"
617
+ case .payUpFront: return "UpFront"
528
618
  case .freeTrial: return "FreeTrial"
529
619
  default: return "PayAsYouGo"
530
620
  }