capacitor-plugin-cdv-purchase 13.18.0 → 13.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/dist/index.js +105 -19
- package/dist/index.js.map +1 -1
- package/ios/Sources/PurchasePlugin/PurchasePlugin.swift +127 -37
- package/package.json +2 -1
- package/src/ts/platforms/iaptic-js/iaptic-js-types.d.ts +451 -0
- package/www/store.d.ts +111 -7
- package/www/store.js +105 -19
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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
|
-
* `
|
|
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.
|
|
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() {
|