capacitor-plugin-cdv-purchase 13.17.2 → 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 +370 -15
- 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 +213 -5
- package/www/store.js +370 -15
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
|
*
|
|
@@ -1674,6 +1752,276 @@ var CdvPurchase;
|
|
|
1674
1752
|
Utils.md5toUUID = md5toUUID;
|
|
1675
1753
|
})(Utils = CdvPurchase.Utils || (CdvPurchase.Utils = {}));
|
|
1676
1754
|
})(CdvPurchase || (CdvPurchase = {}));
|
|
1755
|
+
var CdvPurchase;
|
|
1756
|
+
(function (CdvPurchase) {
|
|
1757
|
+
/**
|
|
1758
|
+
* Persist a subset of {@link VerifiedPurchase} to device storage so that
|
|
1759
|
+
* `offline.isOwned()` can answer "is this product owned?" when the device
|
|
1760
|
+
* is offline or has just restarted without connectivity.
|
|
1761
|
+
*
|
|
1762
|
+
* Phase 1 — unsigned cache. No JWT, no crypto, no server changes.
|
|
1763
|
+
*
|
|
1764
|
+
* @example
|
|
1765
|
+
* ```typescript
|
|
1766
|
+
* const offline = new CdvPurchase.OfflineEntitlements(store, {
|
|
1767
|
+
* gracePeriodMs: 30 * 24 * 60 * 60 * 1000, // 30 days
|
|
1768
|
+
* onExpiredOffline: 'readonly',
|
|
1769
|
+
* detectClockRollback: true,
|
|
1770
|
+
* });
|
|
1771
|
+
*
|
|
1772
|
+
* await offline.ready();
|
|
1773
|
+
*
|
|
1774
|
+
* function isUserPremium(): boolean {
|
|
1775
|
+
* return store.owned('premium') || offline.isOwned('premium');
|
|
1776
|
+
* }
|
|
1777
|
+
* ```
|
|
1778
|
+
*
|
|
1779
|
+
* **Durability warning:** for long-offline deployments (weeks without
|
|
1780
|
+
* connectivity), pass a file-based or secure-storage adapter. `localStorage`
|
|
1781
|
+
* (the default) can be evicted by the WebView under storage pressure, which
|
|
1782
|
+
* would silently lose the cached entitlements.
|
|
1783
|
+
*/
|
|
1784
|
+
class OfflineEntitlements {
|
|
1785
|
+
constructor(store, options = {}) {
|
|
1786
|
+
var _a, _b, _c, _d;
|
|
1787
|
+
/** In-memory cache of persisted entitlements, keyed by `platform:productId`. */
|
|
1788
|
+
this.cache = {};
|
|
1789
|
+
/** Last seen timestamp persisted with the data, used for clock rollback detection. */
|
|
1790
|
+
this.lastSeenTimestamp = 0;
|
|
1791
|
+
/** True once `ready()` has resolved. */
|
|
1792
|
+
this.isReady = false;
|
|
1793
|
+
/** Last event fired per productId, to deduplicate events on repeated isOwned() calls. */
|
|
1794
|
+
this.lastEventPerProduct = {};
|
|
1795
|
+
this.store = store;
|
|
1796
|
+
this.storage = (_a = options.storage) !== null && _a !== void 0 ? _a : OfflineEntitlements.createLocalStorageAdapter();
|
|
1797
|
+
this.gracePeriodMs = (_b = options.gracePeriodMs) !== null && _b !== void 0 ? _b : OfflineEntitlements.DEFAULT_GRACE_PERIOD_MS;
|
|
1798
|
+
this.onExpiredOffline = (_c = options.onExpiredOffline) !== null && _c !== void 0 ? _c : 'readonly';
|
|
1799
|
+
this.detectClockRollback = (_d = options.detectClockRollback) !== null && _d !== void 0 ? _d : false;
|
|
1800
|
+
this.eventCallbacks = new CdvPurchase.Internal.Callbacks(store.log, 'OfflineEntitlements');
|
|
1801
|
+
this.store.when().verified((receipt) => { void this.onVerified(receipt); });
|
|
1802
|
+
}
|
|
1803
|
+
/** Wrap the global `localStorage` as an async `OfflineStorageAdapter`. */
|
|
1804
|
+
static createLocalStorageAdapter() {
|
|
1805
|
+
return {
|
|
1806
|
+
getItem: (key) => Promise.resolve(localStorage.getItem(key)),
|
|
1807
|
+
setItem: (key, value) => { localStorage.setItem(key, value); return Promise.resolve(); },
|
|
1808
|
+
removeItem: (key) => { localStorage.removeItem(key); return Promise.resolve(); },
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
/** Load persisted entitlements from storage into the in-memory cache. Idempotent. */
|
|
1812
|
+
ready() {
|
|
1813
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1814
|
+
if (this.isReady)
|
|
1815
|
+
return;
|
|
1816
|
+
yield this.loadFromStorage();
|
|
1817
|
+
this.isReady = true;
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
/** Reload from storage and re-evaluate. Resolves when the reload is complete. Call after reconnecting or manually. */
|
|
1821
|
+
refresh() {
|
|
1822
|
+
return this.loadFromStorage();
|
|
1823
|
+
}
|
|
1824
|
+
/** Register a callback for {@link OfflineEntitlementEvent}s. */
|
|
1825
|
+
onEvent(callback) {
|
|
1826
|
+
this.eventCallbacks.push(callback);
|
|
1827
|
+
}
|
|
1828
|
+
/** Remove all persisted entitlements from storage and clear the in-memory cache. For user logout. */
|
|
1829
|
+
clear() {
|
|
1830
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1831
|
+
this.cache = {};
|
|
1832
|
+
this.lastSeenTimestamp = 0;
|
|
1833
|
+
this.isReady = false;
|
|
1834
|
+
this.lastEventPerProduct = {};
|
|
1835
|
+
yield this.storage.removeItem(OfflineEntitlements.STORAGE_KEY);
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
/**
|
|
1839
|
+
* Return `true` if the product is owned, using the persisted cache when
|
|
1840
|
+
* the in-memory `verifiedReceipts` don't grant ownership.
|
|
1841
|
+
*
|
|
1842
|
+
* Returns `false` for all products until `ready()` has resolved.
|
|
1843
|
+
*/
|
|
1844
|
+
isOwned(productId) {
|
|
1845
|
+
if (!this.isReady) {
|
|
1846
|
+
this.store.log.warn('OfflineEntitlements.isOwned("' + productId + '") called before ready() — returning false. Call await offline.ready() at startup.');
|
|
1847
|
+
return false;
|
|
1848
|
+
}
|
|
1849
|
+
// 1. If store.verifiedReceipts has a valid (non-expired) entry, return true.
|
|
1850
|
+
const online = CdvPurchase.Internal.VerifiedReceipts.isOwned(this.store.verifiedReceipts, { id: productId });
|
|
1851
|
+
if (online)
|
|
1852
|
+
return true;
|
|
1853
|
+
const now = +new Date();
|
|
1854
|
+
// 2. Find persisted purchase for this productId across all platforms.
|
|
1855
|
+
const persisted = this.findPersisted(productId);
|
|
1856
|
+
if (!persisted) {
|
|
1857
|
+
// No persisted entitlement.
|
|
1858
|
+
this.fireEvent('entitlement_missing', productId, 'No persisted entitlement for this product.');
|
|
1859
|
+
return false;
|
|
1860
|
+
}
|
|
1861
|
+
// 3. Branch by product type.
|
|
1862
|
+
// Subscriptions have an expiryDate; non-consumables don't.
|
|
1863
|
+
if (persisted.expiryDate !== undefined && persisted.expiryDate !== null) {
|
|
1864
|
+
// Subscription
|
|
1865
|
+
// Clock rollback detection — scoped to subscriptions (non-consumables have no time component).
|
|
1866
|
+
if (this.detectClockRollback && this.lastSeenTimestamp > now) {
|
|
1867
|
+
this.fireEvent('clock_rollback', persisted.id, 'Clock appears to have rolled back; denying offline entitlement.');
|
|
1868
|
+
return false;
|
|
1869
|
+
}
|
|
1870
|
+
if (persisted.isExpired) {
|
|
1871
|
+
this.fireEvent('expired', persisted.id, 'Subscription is marked as expired.');
|
|
1872
|
+
return false;
|
|
1873
|
+
}
|
|
1874
|
+
if (now < persisted.expiryDate) {
|
|
1875
|
+
return true;
|
|
1876
|
+
}
|
|
1877
|
+
const renewalIntent = persisted.renewalIntent;
|
|
1878
|
+
if (renewalIntent === CdvPurchase.RenewalIntent.RENEW && now < persisted.expiryDate + this.gracePeriodMs) {
|
|
1879
|
+
this.fireEvent('grace', persisted.id, 'Subscription expired but within grace period.');
|
|
1880
|
+
return true;
|
|
1881
|
+
}
|
|
1882
|
+
if (renewalIntent === CdvPurchase.RenewalIntent.LAPSE) {
|
|
1883
|
+
// Lapsed subscriptions deny at expiryDate.
|
|
1884
|
+
return false;
|
|
1885
|
+
}
|
|
1886
|
+
// Unknown/undefined renewalIntent: fall through to onExpiredOffline.
|
|
1887
|
+
if (this.onExpiredOffline === 'readonly') {
|
|
1888
|
+
this.fireEvent('readonly', persisted.id, 'Subscription expired and grace period elapsed; granting readonly access.');
|
|
1889
|
+
return true;
|
|
1890
|
+
}
|
|
1891
|
+
return false;
|
|
1892
|
+
}
|
|
1893
|
+
else {
|
|
1894
|
+
// Non-consumable: never hard-expires, no clock rollback check.
|
|
1895
|
+
return true;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Retrieve the persisted entitlement for a product, analogous to
|
|
1900
|
+
* `store.findInVerifiedReceipts()`.
|
|
1901
|
+
*
|
|
1902
|
+
* Returns the {@link PersistedPurchase} (with `expiryDate`,
|
|
1903
|
+
* `renewalIntent`, `lastRenewalDate`, etc.) if one has been cached,
|
|
1904
|
+
* or `undefined` if no offline entitlement exists for this product.
|
|
1905
|
+
*
|
|
1906
|
+
* Returns `undefined` for all products until `ready()` has resolved.
|
|
1907
|
+
*
|
|
1908
|
+
* @example
|
|
1909
|
+
* ```typescript
|
|
1910
|
+
* const entitlement = offline.find('premium');
|
|
1911
|
+
* if (entitlement?.expiryDate) {
|
|
1912
|
+
* const daysLeft = Math.ceil((entitlement.expiryDate - Date.now()) / 86400000);
|
|
1913
|
+
* showRenewalBanner(daysLeft);
|
|
1914
|
+
* }
|
|
1915
|
+
* ```
|
|
1916
|
+
*/
|
|
1917
|
+
find(productId) {
|
|
1918
|
+
if (!this.isReady)
|
|
1919
|
+
return undefined;
|
|
1920
|
+
return this.findPersisted(productId);
|
|
1921
|
+
}
|
|
1922
|
+
/** Persist all `VerifiedPurchase`s in a verified receipt to storage. */
|
|
1923
|
+
onVerified(receipt) {
|
|
1924
|
+
var _a;
|
|
1925
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1926
|
+
for (const purchase of receipt.collection) {
|
|
1927
|
+
// Skip consumables — they don't need offline entitlement.
|
|
1928
|
+
if (purchase.isConsumed)
|
|
1929
|
+
continue;
|
|
1930
|
+
const platform = (_a = purchase.platform) !== null && _a !== void 0 ? _a : receipt.platform;
|
|
1931
|
+
const product = this.store.get(purchase.id, platform);
|
|
1932
|
+
if (product && product.type === CdvPurchase.ProductType.CONSUMABLE)
|
|
1933
|
+
continue;
|
|
1934
|
+
const key = platform + ':' + purchase.id;
|
|
1935
|
+
this.cache[key] = {
|
|
1936
|
+
id: purchase.id,
|
|
1937
|
+
platform,
|
|
1938
|
+
expiryDate: purchase.expiryDate,
|
|
1939
|
+
isExpired: purchase.isExpired,
|
|
1940
|
+
renewalIntent: purchase.renewalIntent,
|
|
1941
|
+
lastRenewalDate: purchase.lastRenewalDate,
|
|
1942
|
+
purchaseDate: purchase.purchaseDate,
|
|
1943
|
+
cancelationReason: purchase.cancelationReason,
|
|
1944
|
+
isBillingRetryPeriod: purchase.isBillingRetryPeriod,
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
this.lastSeenTimestamp = +new Date();
|
|
1948
|
+
yield this.saveToStorage();
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
/** Find the persisted purchase for a productId across all platforms. */
|
|
1952
|
+
findPersisted(productId) {
|
|
1953
|
+
var _a, _b, _c, _d;
|
|
1954
|
+
let found;
|
|
1955
|
+
for (const key of Object.keys(this.cache)) {
|
|
1956
|
+
const entry = this.cache[key];
|
|
1957
|
+
if (entry.id === productId) {
|
|
1958
|
+
if (!found || ((_b = (_a = found.lastRenewalDate) !== null && _a !== void 0 ? _a : found.purchaseDate) !== null && _b !== void 0 ? _b : 0) < ((_d = (_c = entry.lastRenewalDate) !== null && _c !== void 0 ? _c : entry.purchaseDate) !== null && _d !== void 0 ? _d : 0)) {
|
|
1959
|
+
found = entry;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
return found;
|
|
1964
|
+
}
|
|
1965
|
+
/** Load the persisted payload from storage into the in-memory cache. */
|
|
1966
|
+
loadFromStorage() {
|
|
1967
|
+
var _a, _b;
|
|
1968
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1969
|
+
try {
|
|
1970
|
+
const raw = yield this.storage.getItem(OfflineEntitlements.STORAGE_KEY);
|
|
1971
|
+
if (!raw) {
|
|
1972
|
+
this.cache = {};
|
|
1973
|
+
this.lastSeenTimestamp = 0;
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
const parsed = JSON.parse(raw);
|
|
1977
|
+
if (parsed.schemaVersion !== OfflineEntitlements.SCHEMA_VERSION) {
|
|
1978
|
+
this.store.log.warn('OfflineEntitlements: schema version mismatch, ignoring cached data.');
|
|
1979
|
+
this.cache = {};
|
|
1980
|
+
this.lastSeenTimestamp = 0;
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
this.cache = (_a = parsed.receipts) !== null && _a !== void 0 ? _a : {};
|
|
1984
|
+
this.lastSeenTimestamp = (_b = parsed.lastSeenTimestamp) !== null && _b !== void 0 ? _b : 0;
|
|
1985
|
+
}
|
|
1986
|
+
catch (err) {
|
|
1987
|
+
this.store.log.warn('OfflineEntitlements: failed to load from storage: ' + err.message);
|
|
1988
|
+
this.cache = {};
|
|
1989
|
+
this.lastSeenTimestamp = 0;
|
|
1990
|
+
}
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
/** Serialize the in-memory cache to storage. */
|
|
1994
|
+
saveToStorage() {
|
|
1995
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1996
|
+
const payload = {
|
|
1997
|
+
receipts: this.cache,
|
|
1998
|
+
lastSeenTimestamp: this.lastSeenTimestamp,
|
|
1999
|
+
schemaVersion: OfflineEntitlements.SCHEMA_VERSION,
|
|
2000
|
+
};
|
|
2001
|
+
try {
|
|
2002
|
+
yield this.storage.setItem(OfflineEntitlements.STORAGE_KEY, JSON.stringify(payload));
|
|
2003
|
+
}
|
|
2004
|
+
catch (err) {
|
|
2005
|
+
this.store.log.warn('OfflineEntitlements: failed to save to storage: ' + err.message);
|
|
2006
|
+
}
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
/** Fire an event to all registered callbacks, deduplicating per productId. */
|
|
2010
|
+
fireEvent(type, productId, message) {
|
|
2011
|
+
if (this.lastEventPerProduct[productId] === type)
|
|
2012
|
+
return;
|
|
2013
|
+
this.lastEventPerProduct[productId] = type;
|
|
2014
|
+
this.eventCallbacks.trigger({ type, productId, message }, 'offline_entitlements');
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
/** Storage key used in the storage adapter. */
|
|
2018
|
+
OfflineEntitlements.STORAGE_KEY = 'cdvpurchase.offline_entitlements';
|
|
2019
|
+
/** Schema version for the persisted payload. */
|
|
2020
|
+
OfflineEntitlements.SCHEMA_VERSION = 1;
|
|
2021
|
+
/** Default grace period: 30 days. */
|
|
2022
|
+
OfflineEntitlements.DEFAULT_GRACE_PERIOD_MS = 30 * 24 * 60 * 60 * 1000;
|
|
2023
|
+
CdvPurchase.OfflineEntitlements = OfflineEntitlements;
|
|
2024
|
+
})(CdvPurchase || (CdvPurchase = {}));
|
|
1677
2025
|
/// <reference path="types.ts" />
|
|
1678
2026
|
/// <reference path="utils/compatibility.ts" />
|
|
1679
2027
|
/// <reference path="utils/platform-name.ts" />
|
|
@@ -1689,6 +2037,7 @@ var CdvPurchase;
|
|
|
1689
2037
|
/// <reference path="internal/receipts-monitor.ts" />
|
|
1690
2038
|
/// <reference path="internal/expiry-monitor.ts" />
|
|
1691
2039
|
/// <reference path="utils/to-uuid.ts" />
|
|
2040
|
+
/// <reference path="offline-entitlements.ts" />
|
|
1692
2041
|
/**
|
|
1693
2042
|
* Namespace for the cordova-plugin-purchase plugin.
|
|
1694
2043
|
*
|
|
@@ -1722,7 +2071,7 @@ var CdvPurchase;
|
|
|
1722
2071
|
/**
|
|
1723
2072
|
* Current release number of the plugin.
|
|
1724
2073
|
*/
|
|
1725
|
-
CdvPurchase.PLUGIN_VERSION = '13.
|
|
2074
|
+
CdvPurchase.PLUGIN_VERSION = '13.19.1';
|
|
1726
2075
|
/**
|
|
1727
2076
|
* Entry class of the plugin.
|
|
1728
2077
|
*/
|
|
@@ -3443,13 +3792,19 @@ var CdvPurchase;
|
|
|
3443
3792
|
yield this.receiptsUpdated.wait();
|
|
3444
3793
|
this.log.info('finish: ' + transactionIdentifier + ' - ' + productId);
|
|
3445
3794
|
this.removeTransactionInProgress(productId);
|
|
3446
|
-
yield this.upsertTransaction(productId, transactionIdentifier, CdvPurchase.TransactionState.FINISHED);
|
|
3795
|
+
const finishedTransaction = yield this.upsertTransaction(productId, transactionIdentifier, CdvPurchase.TransactionState.FINISHED);
|
|
3796
|
+
finishedTransaction.isAcknowledged = true;
|
|
3447
3797
|
this.receiptsUpdated.call();
|
|
3448
3798
|
}),
|
|
3449
|
-
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* () {
|
|
3450
3800
|
this.log.info('restore: ' + transactionIdentifier + ' - ' + productId);
|
|
3451
3801
|
const transaction = yield this.upsertTransaction(productId, transactionIdentifier, CdvPurchase.TransactionState.APPROVED);
|
|
3452
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;
|
|
3453
3808
|
this.receiptsUpdated.call();
|
|
3454
3809
|
}),
|
|
3455
3810
|
receiptsRefreshed: (receipt) => {
|
|
@@ -4083,7 +4438,7 @@ var CdvPurchase;
|
|
|
4083
4438
|
}
|
|
4084
4439
|
// Listen for transaction updates from native
|
|
4085
4440
|
plugin.addListener('transactionUpdated', (data) => {
|
|
4086
|
-
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);
|
|
4087
4442
|
});
|
|
4088
4443
|
plugin.addListener('restoreCompleted', () => {
|
|
4089
4444
|
this.restoreCompletedTransactionsFinished();
|
|
@@ -4104,7 +4459,7 @@ var CdvPurchase;
|
|
|
4104
4459
|
const pending = this.pendingTransactionUpdates;
|
|
4105
4460
|
this.pendingTransactionUpdates = [];
|
|
4106
4461
|
for (const args of pending) {
|
|
4107
|
-
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);
|
|
4108
4463
|
}
|
|
4109
4464
|
if (this.options.ready)
|
|
4110
4465
|
this.options.ready();
|
|
@@ -4188,13 +4543,13 @@ var CdvPurchase;
|
|
|
4188
4543
|
.catch((err) => errorCb(CdvPurchase.ErrorCode.LOAD, (err === null || err === void 0 ? void 0 : err.message) || 'loadReceipts failed'));
|
|
4189
4544
|
}
|
|
4190
4545
|
// Called when the native side sends a transaction update
|
|
4191
|
-
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) {
|
|
4192
4547
|
if (!this.initialized) {
|
|
4193
4548
|
this.pendingTransactionUpdates.push({
|
|
4194
4549
|
state, errorCode, errorText, transactionIdentifier,
|
|
4195
4550
|
productId, transactionReceipt, originalTransactionIdentifier,
|
|
4196
4551
|
transactionDate, discountId, expirationDate, jwsRepresentation,
|
|
4197
|
-
quantity,
|
|
4552
|
+
quantity, isFinished,
|
|
4198
4553
|
});
|
|
4199
4554
|
return;
|
|
4200
4555
|
}
|
|
@@ -4230,7 +4585,7 @@ var CdvPurchase;
|
|
|
4230
4585
|
// purchased, but is meaningless here: consumables cannot be
|
|
4231
4586
|
// restored, so restored transactions are always quantity 1.
|
|
4232
4587
|
if (this.options.restored) {
|
|
4233
|
-
this.options.restored(transactionIdentifier, productId, originalTransactionIdentifier, transactionDate, discountId, expirationDate, jwsRepresentation, quantity);
|
|
4588
|
+
this.options.restored(transactionIdentifier, productId, originalTransactionIdentifier, transactionDate, discountId, expirationDate, jwsRepresentation, quantity, isFinished);
|
|
4234
4589
|
}
|
|
4235
4590
|
break;
|
|
4236
4591
|
case 'PaymentTransactionStateDeferred':
|