capacitor-plugin-cdv-purchase 13.18.0 → 13.19.1

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: isTransientError(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: false }, '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.19.1';
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':
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-plugin-cdv-purchase",
3
- "version": "13.18.0",
3
+ "version": "13.19.1",
4
4
  "description": "In-App Purchases for Capacitor — iOS (StoreKit 2) and Android (Google Play Billing)",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -38,6 +38,7 @@
38
38
  "dist/",
39
39
  "types/",
40
40
  "www/",
41
+ "src/ts/platforms/iaptic-js/iaptic-js-types.d.ts",
41
42
  "android/",
42
43
  "!android/build",
43
44
  "ios/",
@@ -0,0 +1,326 @@
1
+ declare namespace ModuleIapticJS {
2
+ /**
3
+ * Type of product that can be purchased
4
+ */
5
+ export type ProductType = 'subscription' | 'consumable' | 'non consumable' | 'paid subscription';
6
+
7
+ /**
8
+ * Represents a pricing phase for an offer
9
+ */
10
+ export interface PricingPhase {
11
+ /** Price in micros (1/1,000,000 of the currency unit). For example, $1.99 = 1990000 micros */
12
+ priceMicros: number;
13
+ /** ISO 4217 currency code (e.g., 'USD', 'EUR', 'JPY') */
14
+ currency: string;
15
+ /**
16
+ * ISO 8601 duration string representing the billing period
17
+ * Examples: 'P1M' = 1 month, 'P1Y' = 1 year, 'P7D' = 7 days
18
+ */
19
+ billingPeriod: string;
20
+ /**
21
+ * Defines how the subscription recurs
22
+ */
23
+ recurrenceMode: 'INFINITE_RECURRING' | 'NON_RECURRING' | 'FINITE_RECURRING';
24
+ /**
25
+ * Defines when payment is collected
26
+ */
27
+ paymentMode: 'PayAsYouGo' | 'PayUpFront';
28
+ }
29
+
30
+ /**
31
+ * Represents a purchase offer for a product
32
+ */
33
+ export interface Offer {
34
+ /** Unique identifier for the offer in the format 'platform:id' */
35
+ id: string;
36
+ /** Payment platform identifier (e.g., 'stripe', 'google', 'apple') */
37
+ platform: string;
38
+ /** Type of the offer */
39
+ offerType: 'Subscription' | 'Default';
40
+ /** Array of pricing phases that make up this offer's payment schedule */
41
+ pricingPhases: PricingPhase[];
42
+ }
43
+
44
+ /**
45
+ * Represents a product that can be purchased
46
+ */
47
+ export interface Product {
48
+ /** Type of the product (subscription, consumable, etc.) */
49
+ type: ProductType;
50
+ /** Unique identifier for the product */
51
+ id: string;
52
+ /** Display name of the product shown to customers */
53
+ title: string;
54
+ /** Optional detailed description of the product */
55
+ description?: string;
56
+ /** Available purchase offers for this product */
57
+ offers: Offer[];
58
+ /**
59
+ * Optional additional data associated with the product
60
+ */
61
+ metadata?: Record<string, any>;
62
+ /** Optional platform identifier (e.g., 'stripe') */
63
+ platform?: string;
64
+ }
65
+
66
+ /**
67
+ * Represents a purchased product
68
+ */
69
+ export interface Purchase {
70
+ /** Unique identifier for the purchase */
71
+ purchaseId: string;
72
+ /** Unique identifier for the transaction */
73
+ transactionId: string;
74
+ /** Unique identifier for the product */
75
+ productId: string;
76
+ /** Platform identifier (e.g., 'stripe') */
77
+ platform: 'stripe';
78
+ /** Date of the purchase */
79
+ purchaseDate: string;
80
+ /** Date of the last renewal */
81
+ lastRenewalDate: string;
82
+ /** Date of the expiration */
83
+ expirationDate: string;
84
+ /** Intent of the renewal */
85
+ renewalIntent: 'Renew' | 'Cancel';
86
+ /** Whether the purchase is a trial period */
87
+ isTrialPeriod: boolean;
88
+ /** Price in micros (1/1,000,000 of the currency unit) */
89
+ amountMicros: number;
90
+ /** Currency code (e.g., 'USD', 'EUR') */
91
+ currency: string;
92
+ }
93
+
94
+ /**
95
+ * Represents an order for a product
96
+ */
97
+ export interface Order {
98
+ /** Unique identifier for the offer */
99
+ offerId: string;
100
+ /** Username in the application */
101
+ applicationUsername: string;
102
+ /** URL to redirect to on success */
103
+ successUrl: string;
104
+ /** URL to redirect to on cancel */
105
+ cancelUrl: string;
106
+ /** Optional access token for the user */
107
+ accessToken?: string;
108
+ }
109
+
110
+ /** Plan change request */
111
+ export interface PlanChange {
112
+ /** Unique identifier for the purchase to replace (optional) */
113
+ purchaseId: string;
114
+ /** New offer to subscribe to */
115
+ offerId: string;
116
+ /** Optional access token for the user */
117
+ accessToken?: string;
118
+ }
119
+
120
+ /**
121
+ * Configuration options for creating an Iaptic adapter
122
+ */
123
+ export interface Config {
124
+ /** Adapter type (currently only 'stripe' is supported) */
125
+ type: 'stripe';
126
+ /** Stripe publishable key */
127
+ stripePublicKey: string;
128
+ /** Your application name as configured on iaptic.com */
129
+ appName: string;
130
+ /** Your Iaptic public API key */
131
+ apiKey: string;
132
+ /** Optional custom Iaptic URL for private deployments */
133
+ customIapticUrl?: string;
134
+ }
135
+
136
+ /**
137
+ * Response from Iaptic's /stripe/products API
138
+ */
139
+ export interface GetProductsResponse {
140
+ /** Whether the request was successful */
141
+ ok: boolean;
142
+ /** Array of available products */
143
+ products: Product[];
144
+ }
145
+
146
+ /**
147
+ * Response from Iaptic's /stripe/checkout API
148
+ */
149
+ export interface PostCheckoutSessionResponse {
150
+ /** Whether the request was successful */
151
+ ok: boolean;
152
+ /** Stripe Checkout URL where the customer will be redirected */
153
+ url: string;
154
+ /** Access token for the customer */
155
+ accessToken: string;
156
+ }
157
+
158
+ /**
159
+ * Response from Iaptic's /stripe/purchases API
160
+ */
161
+ export interface GetPurchasesResponse {
162
+ /** Whether the request was successful */
163
+ ok: boolean;
164
+ /** Array of customer's purchases */
165
+ purchases: Purchase[];
166
+ /** Optional new access token */
167
+ newAccessToken?: string;
168
+ }
169
+
170
+ /**
171
+ * Response from Iaptic's /stripe/change-plan API
172
+ */
173
+ export interface ChangePlanResponse {
174
+ /** Whether the request was successful */
175
+ ok: boolean;
176
+ /** Updated purchase details */
177
+ purchase: Purchase;
178
+ /** Optional new access token */
179
+ newAccessToken?: string;
180
+ }
181
+
182
+ /**
183
+ * Represents a scheduled refresh for a subscription
184
+ */
185
+ export interface ScheduledRefresh {
186
+ /** Unique identifier for the scheduled refresh */
187
+ id: string;
188
+ /** Unique identifier for the subscription */
189
+ subscriptionId: string;
190
+ /** Date and time of the scheduled refresh */
191
+ scheduledAt: number;
192
+ /** Whether the refresh has been completed */
193
+ completed: boolean;
194
+ /** Whether the refresh is currently in progress */
195
+ inProgress: boolean;
196
+ /** Reason for the scheduled refresh */
197
+ reason: string;
198
+ }
199
+
200
+ /**
201
+ * RefreshScheduler class for managing subscription refreshes
202
+ */
203
+ export class RefreshScheduler {
204
+ schedules: ScheduledRefresh[];
205
+ constructor(iapticStripe: IapticStripe);
206
+ scheduleRefresh(subscriptionId: string, date: Date, reason: string): void;
207
+ schedulePurchaseRefreshes(purchase: Purchase): void;
208
+ clearSchedules(): void;
209
+ }
210
+
211
+ /**
212
+ * Main class for interacting with Iaptic's Stripe integration
213
+ */
214
+ export class IapticStripe {
215
+ /**
216
+ * Creates a new IapticStripe instance
217
+ * @param config - Configuration options for the Stripe integration
218
+ */
219
+ constructor(config: Config);
220
+
221
+ /**
222
+ * Gets a list of available products and their pricing
223
+ * @returns Promise resolving to an array of products
224
+ */
225
+ getProducts(): Promise<Product[]>;
226
+
227
+ /**
228
+ * Forces a refresh of the products list from the server
229
+ * @returns Promise resolving to an array of products
230
+ */
231
+ refreshProducts(): Promise<Product[]>;
232
+
233
+ /**
234
+ * Gets the current access token if one exists
235
+ * @returns The current access token or undefined if none exists
236
+ */
237
+ getAccessToken(): string | undefined;
238
+
239
+ /**
240
+ * Creates a new order and redirects to Stripe Checkout
241
+ * @param params - Order parameters including product and URLs
242
+ */
243
+ order(params: Order): Promise<void>;
244
+
245
+ /**
246
+ * Gets the customer's purchase history
247
+ * @param accessToken - Optional access token (uses stored token if not provided)
248
+ * @returns Promise resolving to an array of purchases
249
+ */
250
+ getPurchases(accessToken?: string): Promise<Purchase[]>;
251
+
252
+ /**
253
+ * Redirects to Stripe Customer Portal for subscription management
254
+ * @param params - Portal parameters including return URL
255
+ */
256
+ redirectToCustomerPortal(params: {
257
+ returnUrl?: string;
258
+ accessToken?: string;
259
+ }): Promise<void>;
260
+
261
+ /**
262
+ * Changes a subscription plan
263
+ * @param planChange - Plan change parameters
264
+ * @returns Promise resolving to the updated purchase details
265
+ */
266
+ changePlan(planChange: PlanChange): Promise<Purchase>;
267
+
268
+ /**
269
+ * Clears all stored data including access tokens and cached products
270
+ */
271
+ clearStoredData(): void;
272
+ }
273
+
274
+ /**
275
+ * Utility functions for the Iaptic library
276
+ */
277
+ export class Utils {
278
+ /**
279
+ * Format a currency amount from micros with proper localization
280
+ * @param amountMicros - Amount in micros (1/1,000,000 of currency unit)
281
+ * @param currency - ISO 4217 currency code (e.g., 'USD', 'EUR')
282
+ * @returns Formatted currency string
283
+ */
284
+ static formatCurrency(amountMicros: number, currency: string): string;
285
+
286
+ /**
287
+ * Format an ISO 8601 period string into human-readable English
288
+ * @param period - ISO 8601 duration string
289
+ * @returns Human-readable period string
290
+ */
291
+ static formatBillingPeriodEN(period: string): string;
292
+
293
+ /**
294
+ * Base64 encode a string with fallbacks for different environments
295
+ */
296
+ static base64Encode(str: string): string;
297
+
298
+ /**
299
+ * Build a URL with query parameters
300
+ */
301
+ static buildUrl(baseUrl: string, params: Record<string, string>): string;
302
+ }
303
+
304
+ /**
305
+ * Creates an Iaptic adapter for the specified payment platform
306
+ * @param config - Configuration options for the adapter
307
+ * @returns An initialized adapter instance
308
+ */
309
+ export function createAdapter(config: Config): IapticStripe;
310
+ }
311
+
312
+ /**
313
+ * Global IapticJS object available in browser environments
314
+ */
315
+ interface Window {
316
+ IapticJS: {
317
+ /** Current version of the IapticJS library */
318
+ version: string;
319
+ /** Function to create a new Iaptic adapter */
320
+ createAdapter: typeof ModuleIapticJS.createAdapter;
321
+ /** Stripe adapter class */
322
+ IapticStripe: typeof ModuleIapticJS.IapticStripe;
323
+ /** Utility functions */
324
+ Utils: typeof ModuleIapticJS.Utils;
325
+ };
326
+ }
package/www/store.d.ts CHANGED
@@ -366,6 +366,18 @@ declare namespace CdvPurchase {
366
366
  /** List of verified receipts */
367
367
  verifiedReceipts: VerifiedReceipt[];
368
368
  constructor(controller: ValidatorController, log: Logger);
369
+ /** Maximum number of validation attempts for a receipt when failures are temporary */
370
+ maxAttempts: number;
371
+ /** Number of failed attempts in a row, per receipt */
372
+ private failedAttempts;
373
+ /** Backoff for retrying temporary failures (created on first use, it listens to the "online" event) */
374
+ private retry?;
375
+ /**
376
+ * Schedule a new validation if the failure is temporary and attempts remain.
377
+ *
378
+ * @return true if a retry was scheduled
379
+ */
380
+ private scheduleRetry;
369
381
  numRequests: number;
370
382
  numResponses: number;
371
383
  incrRequestsCounter(): void;
@@ -393,6 +405,20 @@ declare namespace CdvPurchase {
393
405
  private removeExpiredCache;
394
406
  private runValidatorRequest;
395
407
  }
408
+ /**
409
+ * True if a validation error is temporary (validator or store unreachable,
410
+ * server error, timeout, no network) rather than a definitive answer about
411
+ * the receipt.
412
+ *
413
+ * Error responses from the validator come in two shapes:
414
+ * - HTTP 200 with `{ ok: false, status, code }` in the body (iaptic does this,
415
+ * so a store outage is `status: 502, code: 6778002` in the body);
416
+ * - a failed HTTP request, reported by the plugin as `code: COMMUNICATION`
417
+ * with the transport status: 5xx, 408 (timeout), 417 (unreadable response,
418
+ * e.g. a captive portal page), 0 or negative (no network, as reported by
419
+ * XMLHttpRequest or cordova-plugin-advanced-http).
420
+ */
421
+ function isTransientError(payload: Validator.Response.ErrorPayload): boolean;
396
422
  }
397
423
  }
398
424
  declare namespace CdvPurchase {
@@ -905,8 +931,8 @@ declare namespace CdvPurchase {
905
931
  }
906
932
  /**
907
933
  * Persist a subset of {@link VerifiedPurchase} to device storage so that
908
- * `store.owned()` works when the device is offline or has just restarted
909
- * without connectivity.
934
+ * `offline.isOwned()` can answer "is this product owned?" when the device
935
+ * is offline or has just restarted without connectivity.
910
936
  *
911
937
  * Phase 1 — unsigned cache. No JWT, no crypto, no server changes.
912
938
  *
@@ -1046,7 +1072,7 @@ declare namespace CdvPurchase {
1046
1072
  /**
1047
1073
  * Current release number of the plugin.
1048
1074
  */
1049
- const PLUGIN_VERSION = "13.18.0";
1075
+ const PLUGIN_VERSION = "13.19.1";
1050
1076
  /**
1051
1077
  * Entry class of the plugin.
1052
1078
  */
@@ -2181,7 +2207,23 @@ declare namespace CdvPurchase {
2181
2207
  lastRenewalDate?: Date;
2182
2208
  /** Time when the subscription is set to expire following this transaction */
2183
2209
  expirationDate?: Date;
2184
- /** True when the transaction has been acknowledged to the platform. */
2210
+ /**
2211
+ * True when the transaction has been acknowledged to the platform.
2212
+ *
2213
+ * - Google Play: the purchase was acknowledged (or consumed).
2214
+ * - Apple App Store (StoreKit 2, Capacitor build only): the transaction was finished. StoreKit 2 re-reports
2215
+ * finished non-consumables and non-renewing subscriptions as `approved` at every
2216
+ * app launch; check this flag in your `approved()` handler to skip delivering them again.
2217
+ *
2218
+ * Undefined when the platform doesn't report it (including StoreKit 1 and the Cordova
2219
+ * `cordova-plugin-purchase-storekit2` plugin).
2220
+ *
2221
+ * @example
2222
+ * store.when().approved(transaction => {
2223
+ * if (transaction.isAcknowledged) return transaction.finish(); // already delivered
2224
+ * deliver(transaction).then(() => transaction.finish());
2225
+ * });
2226
+ */
2185
2227
  isAcknowledged?: boolean;
2186
2228
  /** True when the transaction is still pending payment. */
2187
2229
  isPending?: boolean;
@@ -3015,7 +3057,7 @@ declare namespace CdvPurchase {
3015
3057
  /** Extended callbacks with SK2 fields (same as SK2BridgeCallbacks) */
3016
3058
  interface CapacitorBridgeCallbacks extends Bridge.BridgeCallbacks {
3017
3059
  purchased: (transactionIdentifier: string, productId: string, originalTransactionIdentifier?: string, transactionDate?: string, discountId?: string, expirationDate?: string, jwsRepresentation?: string, quantity?: number) => void;
3018
- restored: (transactionIdentifier: string, productId: string, originalTransactionIdentifier?: string, transactionDate?: string, discountId?: string, expirationDate?: string, jwsRepresentation?: string, quantity?: number) => void;
3060
+ restored: (transactionIdentifier: string, productId: string, originalTransactionIdentifier?: string, transactionDate?: string, discountId?: string, expirationDate?: string, jwsRepresentation?: string, quantity?: number, isFinished?: boolean) => void;
3019
3061
  }
3020
3062
  class CapacitorNativeBridge implements Bridge.BridgeInterface {
3021
3063
  appStoreReceipt: ApplicationReceipt | null;
@@ -3279,8 +3321,14 @@ declare namespace CdvPurchase {
3279
3321
  deferred: (productId: string) => void;
3280
3322
  /** Called when a transaction is in "finished" state */
3281
3323
  finished: (transactionIdentifier: string, productId: string) => void;
3282
- /** Called when a transaction is in "restored" state */
3283
- restored: (transactionIdentifier: string, productId: string, originalTransactionIdentifier?: string, transactionDate?: string, discountId?: string, expirationDate?: string, jwsRepresentation?: string, quantity?: number) => void;
3324
+ /**
3325
+ * Called when a transaction is in "restored" state.
3326
+ *
3327
+ * `isFinished` is set by StoreKit 2 bridges when known: true when the app
3328
+ * already finished the transaction (e.g. a finished entitlement re-emitted
3329
+ * at launch), false when it is still unfinished.
3330
+ */
3331
+ restored: (transactionIdentifier: string, productId: string, originalTransactionIdentifier?: string, transactionDate?: string, discountId?: string, expirationDate?: string, jwsRepresentation?: string, quantity?: number, isFinished?: boolean) => void;
3284
3332
  /** Called when the application receipt is refreshed */
3285
3333
  receiptsRefreshed: (receipt: ApplicationReceipt) => void;
3286
3334
  /** Called when a call to "restore" failed */
@@ -6714,6 +6762,31 @@ declare namespace CdvPurchase {
6714
6762
  interface UnverifiedReceipt {
6715
6763
  receipt: Receipt;
6716
6764
  payload: Validator.Response.ErrorPayload;
6765
+ /**
6766
+ * True when validation failed for a temporary reason: the validator
6767
+ * (or the store behind it) couldn't be reached, timed out, or answered
6768
+ * with a server error. The receipt may well be valid.
6769
+ *
6770
+ * Don't revoke access on a transient failure: keep what your app (or
6771
+ * your server) last knew, and validate again later.
6772
+ *
6773
+ * The plugin already retried a few times (with backoff) before firing
6774
+ * a transient failure.
6775
+ *
6776
+ * False when the validator gave a definitive answer (e.g. invalid
6777
+ * receipt, expired subscription, authentication error).
6778
+ *
6779
+ * Transient:
6780
+ * - HTTP 5xx, from the transport or as `payload.status` in the response body
6781
+ * - `CONNECTION_FAILED` (6778002) from the validator
6782
+ * - request timeout, no network, unreadable response
6783
+ *
6784
+ * Definitive: everything else (HTTP 400 `INVALID_PAYLOAD`, 401/403, 410,
6785
+ * 419 expired, ...).
6786
+ *
6787
+ * Set by the plugin on every `unverified` event since v13.19.0.
6788
+ */
6789
+ transient?: boolean;
6717
6790
  }
6718
6791
  /** Receipt data as validated by the receipt validation server */
6719
6792
  class VerifiedReceipt {
package/www/store.js CHANGED
@@ -521,6 +521,10 @@ var CdvPurchase;
521
521
  this.receiptsToValidate = new ReceiptsToValidate();
522
522
  /** List of verified receipts */
523
523
  this.verifiedReceipts = [];
524
+ /** Maximum number of validation attempts for a receipt when failures are temporary */
525
+ this.maxAttempts = 4;
526
+ /** Number of failed attempts in a row, per receipt */
527
+ this.failedAttempts = new Map();
524
528
  this.numRequests = 0;
525
529
  this.numResponses = 0;
526
530
  /**
@@ -534,6 +538,36 @@ var CdvPurchase;
534
538
  this.controller = controller;
535
539
  this.log = log.child('Validator');
536
540
  }
541
+ /**
542
+ * Schedule a new validation if the failure is temporary and attempts remain.
543
+ *
544
+ * @return true if a retry was scheduled
545
+ */
546
+ scheduleRetry(receipt, payload) {
547
+ var _a;
548
+ if (payload.ok || !isRetriableError(payload)) {
549
+ this.failedAttempts.delete(receipt);
550
+ if (payload.ok && this.retry)
551
+ this.retry.retryTimeout = this.retry.minTimeout;
552
+ return false;
553
+ }
554
+ const attempts = ((_a = this.failedAttempts.get(receipt)) !== null && _a !== void 0 ? _a : 0) + 1;
555
+ if (attempts >= this.maxAttempts) {
556
+ this.log.warn(`Validation failed ${attempts} times, giving up: ${payload.message}`);
557
+ this.failedAttempts.delete(receipt);
558
+ return false;
559
+ }
560
+ this.failedAttempts.set(receipt, attempts);
561
+ if (!this.retry)
562
+ this.retry = new Internal.Retry();
563
+ this.log.info(`Validation failed (${payload.message}), retry in ${this.retry.retryTimeout}ms (attempt ${attempts + 1}/${this.maxAttempts})`);
564
+ this.retry.retry(() => {
565
+ // the request is still counted as pending, so don't count it again
566
+ this.receiptsToValidate.add(receipt);
567
+ this.run();
568
+ });
569
+ return true;
570
+ }
537
571
  incrRequestsCounter() {
538
572
  this.numRequests = (this.numRequests + 1) | 0;
539
573
  this.log.debug(`Validation requests=${this.numRequests} responses=${this.numResponses}`);
@@ -574,6 +608,9 @@ var CdvPurchase;
574
608
  const onResponse = (r) => __awaiter(this, void 0, void 0, function* () {
575
609
  var _a;
576
610
  const { receipt, payload } = r;
611
+ // a retried request is still pending: only count final responses
612
+ if (this.scheduleRetry(receipt, payload))
613
+ return;
577
614
  this.incrResponsesCounter();
578
615
  try {
579
616
  const adapter = this.controller.adapters.find(receipt.platform);
@@ -595,11 +632,11 @@ var CdvPurchase;
595
632
  this.controller.verifiedCallbacks.trigger(vr, 'payload_expired');
596
633
  }
597
634
  else {
598
- this.controller.unverifiedCallbacks.trigger({ receipt, payload }, 'no_verified_receipt');
635
+ this.controller.unverifiedCallbacks.trigger({ receipt, payload, transient: false }, 'no_verified_receipt');
599
636
  }
600
637
  }
601
638
  else {
602
- this.controller.unverifiedCallbacks.trigger({ receipt, payload }, 'validator_error');
639
+ this.controller.unverifiedCallbacks.trigger({ receipt, payload, transient: isTransientError(payload) }, 'validator_error');
603
640
  }
604
641
  }
605
642
  catch (err) {
@@ -608,7 +645,7 @@ var CdvPurchase;
608
645
  ok: false,
609
646
  code: CdvPurchase.ErrorCode.VERIFICATION_FAILED,
610
647
  message: err.message,
611
- } }, 'validator_exception');
648
+ }, transient: false }, 'validator_exception');
612
649
  }
613
650
  });
614
651
  receipts.forEach(receipt => this.runOnReceipt(receipt, onResponse));
@@ -768,10 +805,13 @@ var CdvPurchase;
768
805
  data: { latest_receipt: (_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.latest_receipt },
769
806
  }
770
807
  });
771
- this.cache[bodyTransactionHash] = {
772
- payload: response,
773
- expires: (+new Date()) + 120000, // expires in 2 minutes
774
- };
808
+ // don't cache temporary errors, so a retry makes a new request
809
+ if (response.ok || !isTransientError(response)) {
810
+ this.cache[bodyTransactionHash] = {
811
+ payload: response,
812
+ expires: (+new Date()) + 120000, // expires in 2 minutes
813
+ };
814
+ }
775
815
  callback({ receipt, payload: response });
776
816
  },
777
817
  error: (status, message, data) => {
@@ -793,6 +833,44 @@ var CdvPurchase;
793
833
  }
794
834
  }
795
835
  Internal.Validator = Validator;
836
+ /** Validator error code: the validator couldn't reach the store (Apple, Google, ...). */
837
+ const VALIDATOR_CONNECTION_FAILED = 6778002;
838
+ /**
839
+ * True if a validation error is temporary (validator or store unreachable,
840
+ * server error, timeout, no network) rather than a definitive answer about
841
+ * the receipt.
842
+ *
843
+ * Error responses from the validator come in two shapes:
844
+ * - HTTP 200 with `{ ok: false, status, code }` in the body (iaptic does this,
845
+ * so a store outage is `status: 502, code: 6778002` in the body);
846
+ * - a failed HTTP request, reported by the plugin as `code: COMMUNICATION`
847
+ * with the transport status: 5xx, 408 (timeout), 417 (unreadable response,
848
+ * e.g. a captive portal page), 0 or negative (no network, as reported by
849
+ * XMLHttpRequest or cordova-plugin-advanced-http).
850
+ */
851
+ function isTransientError(payload) {
852
+ const status = payload.status;
853
+ if (typeof status === 'number' && status >= 500)
854
+ return true;
855
+ if (payload.code === VALIDATOR_CONNECTION_FAILED)
856
+ return true;
857
+ if (payload.code === CdvPurchase.ErrorCode.COMMUNICATION) {
858
+ if (typeof status !== 'number' || status <= 0)
859
+ return true;
860
+ if (status === CdvPurchase.Utils.Ajax.HTTP_REQUEST_TIMEOUT || status === 417)
861
+ return true;
862
+ }
863
+ return false;
864
+ }
865
+ Internal.isTransientError = isTransientError;
866
+ /**
867
+ * True if a validation failure is worth retrying: a temporary error
868
+ * (see isTransientError) or an invalid response from the validator.
869
+ * Validator verdicts (invalid receipt, expired, ...) are not retried.
870
+ */
871
+ function isRetriableError(payload) {
872
+ return isTransientError(payload) || payload.code === CdvPurchase.ErrorCode.BAD_RESPONSE;
873
+ }
796
874
  /**
797
875
  * Check if a payload looks like a valid validator response.
798
876
  *
@@ -1678,8 +1756,8 @@ var CdvPurchase;
1678
1756
  (function (CdvPurchase) {
1679
1757
  /**
1680
1758
  * Persist a subset of {@link VerifiedPurchase} to device storage so that
1681
- * `store.owned()` works when the device is offline or has just restarted
1682
- * without connectivity.
1759
+ * `offline.isOwned()` can answer "is this product owned?" when the device
1760
+ * is offline or has just restarted without connectivity.
1683
1761
  *
1684
1762
  * Phase 1 — unsigned cache. No JWT, no crypto, no server changes.
1685
1763
  *
@@ -1993,7 +2071,7 @@ var CdvPurchase;
1993
2071
  /**
1994
2072
  * Current release number of the plugin.
1995
2073
  */
1996
- CdvPurchase.PLUGIN_VERSION = '13.18.0';
2074
+ CdvPurchase.PLUGIN_VERSION = '13.19.1';
1997
2075
  /**
1998
2076
  * Entry class of the plugin.
1999
2077
  */
@@ -3714,13 +3792,19 @@ var CdvPurchase;
3714
3792
  yield this.receiptsUpdated.wait();
3715
3793
  this.log.info('finish: ' + transactionIdentifier + ' - ' + productId);
3716
3794
  this.removeTransactionInProgress(productId);
3717
- yield this.upsertTransaction(productId, transactionIdentifier, CdvPurchase.TransactionState.FINISHED);
3795
+ const finishedTransaction = yield this.upsertTransaction(productId, transactionIdentifier, CdvPurchase.TransactionState.FINISHED);
3796
+ finishedTransaction.isAcknowledged = true;
3718
3797
  this.receiptsUpdated.call();
3719
3798
  }),
3720
- restored: (transactionIdentifier, productId, originalTransactionIdentifier, transactionDate, discountId, expirationDate, jwsRepresentation, quantity) => __awaiter(this, void 0, void 0, function* () {
3799
+ restored: (transactionIdentifier, productId, originalTransactionIdentifier, transactionDate, discountId, expirationDate, jwsRepresentation, quantity, isFinished) => __awaiter(this, void 0, void 0, function* () {
3721
3800
  this.log.info('restore: ' + transactionIdentifier + ' - ' + productId);
3722
3801
  const transaction = yield this.upsertTransaction(productId, transactionIdentifier, CdvPurchase.TransactionState.APPROVED);
3723
3802
  transaction.refresh(productId, originalTransactionIdentifier, transactionDate, discountId, expirationDate, jwsRepresentation, quantity);
3803
+ // StoreKit 2 re-emits finished entitlements (non-consumables,
3804
+ // non-renewing subscriptions) at every launch. Expose the finished
3805
+ // state the same way Google Play does, so apps can skip them in approved().
3806
+ if (isFinished !== undefined)
3807
+ transaction.isAcknowledged = isFinished;
3724
3808
  this.receiptsUpdated.call();
3725
3809
  }),
3726
3810
  receiptsRefreshed: (receipt) => {
@@ -4354,7 +4438,7 @@ var CdvPurchase;
4354
4438
  }
4355
4439
  // Listen for transaction updates from native
4356
4440
  plugin.addListener('transactionUpdated', (data) => {
4357
- 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);
4441
+ 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);
4358
4442
  });
4359
4443
  plugin.addListener('restoreCompleted', () => {
4360
4444
  this.restoreCompletedTransactionsFinished();
@@ -4375,7 +4459,7 @@ var CdvPurchase;
4375
4459
  const pending = this.pendingTransactionUpdates;
4376
4460
  this.pendingTransactionUpdates = [];
4377
4461
  for (const args of pending) {
4378
- 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);
4462
+ 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);
4379
4463
  }
4380
4464
  if (this.options.ready)
4381
4465
  this.options.ready();
@@ -4459,13 +4543,13 @@ var CdvPurchase;
4459
4543
  .catch((err) => errorCb(CdvPurchase.ErrorCode.LOAD, (err === null || err === void 0 ? void 0 : err.message) || 'loadReceipts failed'));
4460
4544
  }
4461
4545
  // Called when the native side sends a transaction update
4462
- transactionUpdated(state, errorCode, errorText, transactionIdentifier, productId, transactionReceipt, originalTransactionIdentifier, transactionDate, discountId, expirationDate, jwsRepresentation, quantity) {
4546
+ transactionUpdated(state, errorCode, errorText, transactionIdentifier, productId, transactionReceipt, originalTransactionIdentifier, transactionDate, discountId, expirationDate, jwsRepresentation, quantity, isFinished) {
4463
4547
  if (!this.initialized) {
4464
4548
  this.pendingTransactionUpdates.push({
4465
4549
  state, errorCode, errorText, transactionIdentifier,
4466
4550
  productId, transactionReceipt, originalTransactionIdentifier,
4467
4551
  transactionDate, discountId, expirationDate, jwsRepresentation,
4468
- quantity,
4552
+ quantity, isFinished,
4469
4553
  });
4470
4554
  return;
4471
4555
  }
@@ -4501,7 +4585,7 @@ var CdvPurchase;
4501
4585
  // purchased, but is meaningless here: consumables cannot be
4502
4586
  // restored, so restored transactions are always quantity 1.
4503
4587
  if (this.options.restored) {
4504
- this.options.restored(transactionIdentifier, productId, originalTransactionIdentifier, transactionDate, discountId, expirationDate, jwsRepresentation, quantity);
4588
+ this.options.restored(transactionIdentifier, productId, originalTransactionIdentifier, transactionDate, discountId, expirationDate, jwsRepresentation, quantity, isFinished);
4505
4589
  }
4506
4590
  break;
4507
4591
  case 'PaymentTransactionStateDeferred':