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 +32 -0
- package/dist/index.js +101 -17
- package/dist/index.js.map +1 -1
- package/ios/Sources/PurchasePlugin/PurchasePlugin.swift +126 -36
- package/package.json +2 -1
- package/src/ts/platforms/iaptic-js/iaptic-js-types.d.ts +326 -0
- package/www/store.d.ts +80 -7
- package/www/store.js +101 -17
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
* `
|
|
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.
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
108
|
-
// are visible immediately on app launch
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
debugLog("init: skipping already-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
501
|
+
jwsRepresentation: String? = nil,
|
|
502
|
+
isFinished: Bool? = nil) async {
|
|
421
503
|
let transactionId = String(transaction.id)
|
|
422
|
-
|
|
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.
|
|
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
|
-
* `
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
3283
|
-
|
|
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
|
-
|
|
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.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':
|