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/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: isRetriableError(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: true }, '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.20.0';
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':
@@ -7981,7 +8065,7 @@ var CdvPurchase;
7981
8065
  this.products = [{ id: productId }];
7982
8066
  this.transactionId = purchase.transactionId;
7983
8067
  this.purchaseId = purchase.purchaseId;
7984
- this.purchaseDate = new Date(purchase.purchaseDate);
8068
+ this.purchaseDate = purchase.purchaseDate ? new Date(purchase.purchaseDate) : undefined;
7985
8069
  this.expirationDate = purchase.expirationDate ? new Date(purchase.expirationDate) : undefined;
7986
8070
  this.lastRenewalDate = purchase.lastRenewalDate ? new Date(purchase.lastRenewalDate) : undefined;
7987
8071
  this.renewalIntent = purchase.renewalIntent === 'Renew' ? CdvPurchase.RenewalIntent.RENEW : CdvPurchase.RenewalIntent.LAPSE;
@@ -8100,7 +8184,7 @@ var CdvPurchase;
8100
8184
  billingPeriod: pp.billingPeriod,
8101
8185
  paymentMode: pp.paymentMode,
8102
8186
  recurrenceMode: pp.recurrenceMode,
8103
- price: window.IapticJS.Utils.formatCurrency(pp.priceMicros, pp.currency) // Use Utils for formatting
8187
+ price: pp.currency ? window.IapticJS.Utils.formatCurrency(pp.priceMicros, pp.currency) : '' // Use Utils for formatting
8104
8188
  })),
8105
8189
  }, this.context.apiDecorators);
8106
8190
  product.addOffer(offer);
@@ -9594,6 +9678,7 @@ var CdvPurchase;
9594
9678
  this.latestReceipt = response.latest_receipt;
9595
9679
  this.nativeTransactions = [response.transaction];
9596
9680
  this.warning = response.warning;
9681
+ this.cache = response.cache;
9597
9682
  this.validationDate = response.date ? new Date(response.date) : new Date();
9598
9683
  Object.defineProperty(this, 'raw', { 'enumerable': false, get() { return response; } });
9599
9684
  Object.defineProperty(this, 'finish', { 'enumerable': false, get() { return () => decorator.finish(this); } });
@@ -9615,6 +9700,7 @@ var CdvPurchase;
9615
9700
  this.latestReceipt = response.latest_receipt;
9616
9701
  this.nativeTransactions = [response.transaction];
9617
9702
  this.warning = response.warning;
9703
+ this.cache = response.cache;
9618
9704
  }
9619
9705
  /** Finish all transactions in the receipt */
9620
9706
  finish() {