capacitor-plugin-cdv-purchase 13.15.1 → 13.15.3

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.
@@ -1,6 +1,6 @@
1
1
  require 'json'
2
2
 
3
- package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
3
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
4
 
5
5
  Pod::Spec.new do |s|
6
6
  s.name = 'CapacitorPluginCdvPurchase'
@@ -10,7 +10,7 @@ Pod::Spec.new do |s|
10
10
  s.homepage = package['repository']['url']
11
11
  s.author = 'Jean-Christophe Hoelt'
12
12
  s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
13
- s.source_files = 'Sources/PurchasePlugin/**/*.{swift,h,m}'
13
+ s.source_files = 'ios/Sources/PurchasePlugin/**/*.{swift,h,m}'
14
14
  s.ios.deployment_target = '13.0'
15
15
  s.swift_version = '5.9'
16
16
  s.dependency 'Capacitor'
package/Package.swift ADDED
@@ -0,0 +1,27 @@
1
+ // swift-tools-version: 5.9
2
+ import PackageDescription
3
+
4
+ let package = Package(
5
+ name: "CapacitorPluginCdvPurchase",
6
+ platforms: [.iOS(.v15)],
7
+ products: [
8
+ .library(
9
+ name: "CapacitorPluginCdvPurchase",
10
+ targets: ["PurchasePlugin"])
11
+ ],
12
+ dependencies: [
13
+ .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", "6.0.0"..<"10.0.0")
14
+ ],
15
+ targets: [
16
+ .target(
17
+ name: "PurchasePlugin",
18
+ dependencies: [
19
+ .product(name: "Capacitor", package: "capacitor-swift-pm"),
20
+ .product(name: "Cordova", package: "capacitor-swift-pm")
21
+ ],
22
+ path: "ios/Sources/PurchasePlugin",
23
+ swiftSettings: [
24
+ .enableExperimentalFeature("NonescapableTypes")
25
+ ])
26
+ ]
27
+ )
@@ -499,6 +499,7 @@ public class PurchasePlugin extends Plugin implements
499
499
  .enableOneTimeProducts()
500
500
  .enablePrepaidPlans()
501
501
  .build())
502
+ .enableAutoServiceReconnection()
502
503
  .setListener(this)
503
504
  .build();
504
505
 
@@ -795,8 +796,8 @@ public class PurchasePlugin extends Plugin implements
795
796
  mPendingBuyCall = null;
796
797
  }
797
798
  }
798
- } catch (JSONException e) {
799
- Log.w(TAG, "onPurchasesUpdated() -> JSONException "
799
+ } catch (Exception e) {
800
+ Log.w(TAG, "onPurchasesUpdated() -> Exception "
800
801
  + e.getMessage());
801
802
  }
802
803
  }
package/dist/index.js CHANGED
@@ -683,9 +683,39 @@ var CdvPurchase;
683
683
  body.introPriceMicros = intro.priceMicros;
684
684
  }
685
685
  }
686
+ // Only include the products array once per day to reduce request size.
687
+ // The products array is used by the validator for analytics (price history,
688
+ // transaction amounts) but not for the validation flow itself.
689
+ // The top-level price/currency fields serve as fallback when products is omitted.
690
+ if (!this.shouldSendProducts()) {
691
+ delete body.products;
692
+ }
686
693
  return body;
687
694
  });
688
695
  }
696
+ /** Check if the products array should be included in the validation request.
697
+ * Returns true at most once per day, tracked via localStorage. */
698
+ shouldSendProducts() {
699
+ var _a, _b;
700
+ const STORAGE_KEY = 'cdvpurchase_has_sent_products_in_validation';
701
+ const ONE_DAY_MS = 86400000;
702
+ try {
703
+ const stored = (_a = window.localStorage) === null || _a === void 0 ? void 0 : _a.getItem(STORAGE_KEY);
704
+ if (stored) {
705
+ const lastSent = parseInt(stored, 10);
706
+ if (!isNaN(lastSent) && (Date.now() - lastSent) < ONE_DAY_MS) {
707
+ return false;
708
+ }
709
+ }
710
+ // Send products this time and record the date
711
+ (_b = window.localStorage) === null || _b === void 0 ? void 0 : _b.setItem(STORAGE_KEY, String(Date.now()));
712
+ return true;
713
+ }
714
+ catch (_e) {
715
+ // localStorage not available (e.g. private browsing)
716
+ return true;
717
+ }
718
+ }
689
719
  removeExpiredCache() {
690
720
  const now = +new Date();
691
721
  const deleteList = [];
@@ -1038,6 +1068,8 @@ var CdvPurchase;
1038
1068
  /** Those platforms have reported that their receipts are ready */
1039
1069
  this.platformWithReceiptsReady = [];
1040
1070
  this.lastTransactionState = {};
1071
+ /** Remember the first transactionId for each subscription dedup key */
1072
+ this.subscriptionFirstTransactionId = {};
1041
1073
  /** Store the listener's latest calling time (in ms) for a given transaction at a given state */
1042
1074
  this.lastCallTimeForState = {};
1043
1075
  this.updatedReceiptsToProcess = [];
@@ -1047,6 +1079,26 @@ var CdvPurchase;
1047
1079
  static makeTransactionToken(transaction) {
1048
1080
  return transaction.platform + '|' + transaction.transactionId;
1049
1081
  }
1082
+ /**
1083
+ * Create a subscription dedup key from a transaction.
1084
+ *
1085
+ * StoreKit 2 can deliver the same subscription purchase event twice with
1086
+ * different `transactionId` but identical `originalTransactionId` and
1087
+ * `purchaseDate`. This key groups those duplicates together so only one
1088
+ * `approved`/`finished` event is surfaced per billing period.
1089
+ *
1090
+ * Returns `undefined` for non-subscription transactions (no `originalTransactionId`).
1091
+ */
1092
+ static makeSubscriptionKey(transaction) {
1093
+ if (transaction.platform !== CdvPurchase.Platform.APPLE_APPSTORE)
1094
+ return undefined;
1095
+ const skTransaction = transaction;
1096
+ if (!skTransaction.originalTransactionId)
1097
+ return undefined;
1098
+ if (!transaction.purchaseDate)
1099
+ return undefined;
1100
+ return transaction.platform + '|' + skTransaction.originalTransactionId + '|' + transaction.purchaseDate.getTime();
1101
+ }
1050
1102
  /**
1051
1103
  * Set the list of supported platforms.
1052
1104
  *
@@ -1126,16 +1178,32 @@ var CdvPurchase;
1126
1178
  const transactionToken = StoreAdapterListener.makeTransactionToken(transaction);
1127
1179
  const tokenWithState = transactionToken + '@' + transaction.state;
1128
1180
  const lastState = this.lastTransactionState[transactionToken];
1181
+ const subscriptionKey = StoreAdapterListener.makeSubscriptionKey(transaction);
1182
+ const isSubscriptionDuplicate = !!subscriptionKey
1183
+ && !!this.subscriptionFirstTransactionId[subscriptionKey]
1184
+ && this.subscriptionFirstTransactionId[subscriptionKey] !== transaction.transactionId;
1185
+ // Remember the first transactionId for this subscription key
1186
+ if (subscriptionKey && !this.subscriptionFirstTransactionId[subscriptionKey]) {
1187
+ this.subscriptionFirstTransactionId[subscriptionKey] = transaction.transactionId;
1188
+ }
1129
1189
  // Retrigger "approved", so validation is rerun on potential update.
1130
1190
  if (transaction.state === CdvPurchase.TransactionState.APPROVED) {
1131
- // prevent calling approved twice in a very short period (60 seconds).
1132
- const lastCalled = (_a = this.lastCallTimeForState[tokenWithState]) !== null && _a !== void 0 ? _a : 0;
1133
- if (now - lastCalled > 60000) {
1134
- this.lastCallTimeForState[tokenWithState] = now;
1135
- this.delegate.approvedCallbacks.trigger(transaction, 'adapterListener_receiptsUpdated_approved');
1191
+ if (isSubscriptionDuplicate) {
1192
+ // Finish the duplicate at the native level so StoreKit won't re-deliver it
1193
+ this.log.debug(`Auto-finishing subscription duplicate ${transactionToken}, already processed as ${this.subscriptionFirstTransactionId[subscriptionKey]}`);
1194
+ this.delegate.finishDuplicate(transaction);
1136
1195
  }
1137
1196
  else {
1138
- this.log.debug(`Skipping ${tokenWithState}, because it has been last called ${lastCalled > 0 ? Math.round(now - lastCalled) + 'ms ago (' + now + '-' + lastCalled + ')' : 'never'}`);
1197
+ // Use subscription key for dedup when available, otherwise fall back to transactionToken
1198
+ const dedupKey = subscriptionKey ? subscriptionKey + '@' + transaction.state : tokenWithState;
1199
+ const lastCalled = (_a = this.lastCallTimeForState[dedupKey]) !== null && _a !== void 0 ? _a : 0;
1200
+ if (now - lastCalled > 60000) {
1201
+ this.lastCallTimeForState[dedupKey] = now;
1202
+ this.delegate.approvedCallbacks.trigger(transaction, 'adapterListener_receiptsUpdated_approved');
1203
+ }
1204
+ else {
1205
+ this.log.debug(`Skipping ${tokenWithState}, because it has been last called ${lastCalled > 0 ? Math.round(now - lastCalled) + 'ms ago (' + now + '-' + lastCalled + ')' : 'never'}`);
1206
+ }
1139
1207
  }
1140
1208
  }
1141
1209
  else if (lastState !== transaction.state) {
@@ -1144,15 +1212,31 @@ var CdvPurchase;
1144
1212
  this.delegate.initiatedCallbacks.trigger(transaction, 'adapterListener_receiptsUpdated_initiated');
1145
1213
  }
1146
1214
  else if (transaction.state === CdvPurchase.TransactionState.FINISHED) {
1147
- this.lastCallTimeForState[tokenWithState] = now;
1148
- this.delegate.finishedCallbacks.trigger(transaction, 'adapterListener_receiptsUpdated_finished');
1215
+ if (isSubscriptionDuplicate) {
1216
+ // Finish the duplicate at the native level so StoreKit won't re-deliver it
1217
+ this.log.debug(`Auto-finishing subscription duplicate ${transactionToken}, already processed as ${this.subscriptionFirstTransactionId[subscriptionKey]}`);
1218
+ this.delegate.finishDuplicate(transaction);
1219
+ }
1220
+ else {
1221
+ this.lastCallTimeForState[tokenWithState] = now;
1222
+ this.delegate.finishedCallbacks.trigger(transaction, 'adapterListener_receiptsUpdated_finished');
1223
+ }
1149
1224
  }
1150
1225
  else if (transaction.state === CdvPurchase.TransactionState.PENDING) {
1151
1226
  this.lastCallTimeForState[tokenWithState] = now;
1152
1227
  this.delegate.pendingCallbacks.trigger(transaction, 'adapterListener_receiptsUpdated_pending');
1153
1228
  }
1154
1229
  }
1155
- this.lastTransactionState[transactionToken] = transaction.state;
1230
+ // Mark subscription duplicates as FINISHED so the re-processing triggered
1231
+ // by finishDuplicate()'s receiptsUpdated.call() won't enter the FINISHED
1232
+ // handler and call finishDuplicate() a second time on an already-finished
1233
+ // native transaction.
1234
+ if (isSubscriptionDuplicate) {
1235
+ this.lastTransactionState[transactionToken] = CdvPurchase.TransactionState.FINISHED;
1236
+ }
1237
+ else {
1238
+ this.lastTransactionState[transactionToken] = transaction.state;
1239
+ }
1156
1240
  });
1157
1241
  });
1158
1242
  }
@@ -1561,7 +1645,7 @@ var CdvPurchase;
1561
1645
  /**
1562
1646
  * Current release number of the plugin.
1563
1647
  */
1564
- CdvPurchase.PLUGIN_VERSION = '13.15.1';
1648
+ CdvPurchase.PLUGIN_VERSION = '13.15.3';
1565
1649
  /**
1566
1650
  * Entry class of the plugin.
1567
1651
  */
@@ -1641,6 +1725,12 @@ var CdvPurchase;
1641
1725
  finishedCallbacks: this.finishedCallbacks,
1642
1726
  pendingCallbacks: this.pendingCallbacks,
1643
1727
  receiptsReadyCallbacks: this.receiptsReadyCallbacks,
1728
+ finishDuplicate: (transaction) => {
1729
+ const adapter = this.adapters.findReady(transaction.platform);
1730
+ if (adapter) {
1731
+ adapter.finish(transaction);
1732
+ }
1733
+ },
1644
1734
  }, this.log);
1645
1735
  this.transactionStateMonitors = new CdvPurchase.Internal.TransactionStateMonitors(this.when());
1646
1736
  this._validator = new CdvPurchase.Internal.Validator({
@@ -3031,9 +3121,14 @@ var CdvPurchase;
3031
3121
  this.initializeAppReceipt(() => {
3032
3122
  var _a;
3033
3123
  if (!this._receipt) {
3034
- // this should not happen
3035
- this.log.warn('Failed to load the application receipt, cannot proceed with handling the purchase');
3036
- return;
3124
+ // Receipt failed to load — create a minimal receipt so the
3125
+ // transaction can still be tracked and finished. Without this
3126
+ // fallback the Promise would never resolve, leaving the native
3127
+ // transaction unfinished (causing the purchase dialog to loop on
3128
+ // iOS — see #1568).
3129
+ this.log.warn('Application receipt unavailable, creating a fallback receipt to avoid blocking transactions');
3130
+ this._receipt = new AppleAppStore.SKApplicationReceipt({ appStoreReceipt: '', bundleIdentifier: '',
3131
+ bundleShortVersion: '', bundleNumericVersion: 0, bundleSignature: '' }, this.needAppReceipt, this.context.apiDecorators);
3037
3132
  }
3038
3133
  const existing = (_a = this._receipt) === null || _a === void 0 ? void 0 : _a.transactions.find(t => t.transactionId === transactionId);
3039
3134
  if (existing) {
@@ -3195,8 +3290,22 @@ var CdvPurchase;
3195
3290
  });
3196
3291
  }
3197
3292
  loadReceipts() {
3198
- return new Promise((resolve) => {
3199
- setTimeout(() => {
3293
+ return __awaiter(this, void 0, void 0, function* () {
3294
+ // Wait for native pending transactions to be processed before
3295
+ // initializing the receipt. Previously used a fixed 300ms delay
3296
+ // which was unreliable and could miss transactions that arrived
3297
+ // from the native queue (see #1529).
3298
+ //
3299
+ // The pendingTransactionsReady Promise resolves when the native
3300
+ // `processPendingTransactions` call returns, but the per-transaction
3301
+ // callbacks it triggers via evalJs land asynchronously. Yield one
3302
+ // task so those purchased/restored callbacks (and their async
3303
+ // upsertTransaction chain) settle before we snapshot _receipt.
3304
+ if (this.bridge.pendingTransactionsReady) {
3305
+ yield this.bridge.pendingTransactionsReady;
3306
+ yield new Promise(r => setTimeout(r, 0));
3307
+ }
3308
+ return new Promise((resolve) => {
3200
3309
  this.initializeAppReceipt(() => {
3201
3310
  this.receiptsUpdated.call();
3202
3311
  if (this._receipt) {
@@ -3206,7 +3315,7 @@ var CdvPurchase;
3206
3315
  resolve([this.pseudoReceipt]);
3207
3316
  }
3208
3317
  });
3209
- }, 300);
3318
+ });
3210
3319
  });
3211
3320
  }
3212
3321
  canMakePayments() {
@@ -3262,6 +3371,7 @@ var CdvPurchase;
3262
3371
  return;
3263
3372
  }
3264
3373
  this._receipt = new AppleAppStore.SKApplicationReceipt(nativeData, this.needAppReceipt, this.context.apiDecorators);
3374
+ this._appStoreReceiptLoading = false;
3265
3375
  callCallbacks(undefined);
3266
3376
  });
3267
3377
  }
@@ -3475,16 +3585,20 @@ var CdvPurchase;
3475
3585
  this.receiptsUpdated.call();
3476
3586
  return resolve(undefined);
3477
3587
  }
3588
+ const wasAlreadyFinished = transaction.state === CdvPurchase.TransactionState.FINISHED;
3478
3589
  const success = () => {
3479
3590
  transaction.state = CdvPurchase.TransactionState.FINISHED;
3480
- this.receiptsUpdated.call();
3591
+ if (!wasAlreadyFinished) {
3592
+ this.receiptsUpdated.call();
3593
+ }
3481
3594
  resolve(undefined);
3482
3595
  };
3483
3596
  const error = (msg) => {
3484
3597
  var _a, _b;
3485
3598
  if (msg === null || msg === void 0 ? void 0 : msg.includes('[#CdvPurchase:100]')) {
3486
- // already finished
3487
- success();
3599
+ // already finished at the native level
3600
+ transaction.state = CdvPurchase.TransactionState.FINISHED;
3601
+ resolve(undefined);
3488
3602
  }
3489
3603
  else {
3490
3604
  resolve(appStoreError(CdvPurchase.ErrorCode.FINISH, 'Failed to finish transaction', (_b = (_a = transaction.products[0]) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null));
@@ -4046,6 +4160,9 @@ var CdvPurchase;
4046
4160
  protectCall(this.options.ready, 'options.ready');
4047
4161
  protectCall(success, 'init.success');
4048
4162
  this.initialized = true;
4163
+ this.pendingTransactionsReady = new Promise(resolve => {
4164
+ this._pendingTransactionsResolve = resolve;
4165
+ });
4049
4166
  setTimeout(() => this.processPendingTransactions(), 50);
4050
4167
  };
4051
4168
  const setupFailed = (err) => {
@@ -4058,6 +4175,10 @@ var CdvPurchase;
4058
4175
  log('processing pending transactions');
4059
4176
  exec('processPendingTransactions', [], () => {
4060
4177
  this.finalizeTransactionUpdates();
4178
+ if (this._pendingTransactionsResolve) {
4179
+ this._pendingTransactionsResolve();
4180
+ this._pendingTransactionsResolve = undefined;
4181
+ }
4061
4182
  }, undefined);
4062
4183
  }
4063
4184
  purchase(productId, quantity, applicationUsername, discount, success, error) {
@@ -4365,6 +4486,9 @@ var CdvPurchase;
4365
4486
  protectCall(this.options.ready, 'options.ready');
4366
4487
  protectCall(success, 'init.success');
4367
4488
  this.initialized = true;
4489
+ this.pendingTransactionsReady = new Promise(resolve => {
4490
+ this._pendingTransactionsResolve = resolve;
4491
+ });
4368
4492
  setTimeout(() => this.processPendingTransactions(), 50);
4369
4493
  };
4370
4494
  const setupFailed = (err) => {
@@ -4378,6 +4502,10 @@ var CdvPurchase;
4378
4502
  log('processing pending transactions');
4379
4503
  exec('processPendingTransactions', [], () => {
4380
4504
  this.finalizeTransactionUpdates();
4505
+ if (this._pendingTransactionsResolve) {
4506
+ this._pendingTransactionsResolve();
4507
+ this._pendingTransactionsResolve = undefined;
4508
+ }
4381
4509
  }, undefined);
4382
4510
  }
4383
4511
  /**
@@ -4757,6 +4885,13 @@ var CdvPurchase;
4757
4885
  offerType: 'Default',
4758
4886
  }, decorator));
4759
4887
  }
4888
+ // Ensure the default offer is always first so that getOffer()
4889
+ // without arguments returns it instead of a discount offer.
4890
+ const defaultIndex = this.offers.findIndex(o => o.id === AppleAppStore.DEFAULT_OFFER_ID);
4891
+ if (defaultIndex > 0) {
4892
+ const [defaultOffer] = this.offers.splice(defaultIndex, 1);
4893
+ this.offers.unshift(defaultOffer);
4894
+ }
4760
4895
  function hasIntroductoryOffer(product) {
4761
4896
  return product.offers.filter(offer => {
4762
4897
  const skOffer = offer;
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;;;;"}
@@ -7,6 +7,10 @@ private class SK2State {
7
7
  var products: [String: Product] = [:]
8
8
  var unfinishedTransactions: [String: Transaction] = [:]
9
9
  var transactionObserverTask: Task<Void, Never>?
10
+ /// Transaction IDs already emitted to JS. Prevents duplicate delivery when
11
+ /// both init() (via currentEntitlements) and Transaction.updates deliver
12
+ /// the same transaction. Access must be serialized on the main thread.
13
+ var processedTransactionIds: Set<UInt64> = []
10
14
  }
11
15
 
12
16
  @objc(PurchasePlugin)
@@ -69,6 +73,18 @@ public class PurchasePlugin: CAPPlugin, CAPBridgedPlugin {
69
73
  let jwsRepresentation = result.jwsRepresentation
70
74
  switch result {
71
75
  case .verified(let transaction):
76
+ // Serialize access to processedTransactionIds on the main thread
77
+ // since this method runs from a detached Task while init()/purchase()
78
+ // also mutate the set.
79
+ let shouldEmit: Bool = await MainActor.run {
80
+ if sk2.processedTransactionIds.contains(transaction.id) {
81
+ debugLog("Transaction.updates: skipping duplicate id=\(transaction.id)")
82
+ return false
83
+ }
84
+ sk2.processedTransactionIds.insert(transaction.id)
85
+ return true
86
+ }
87
+ guard shouldEmit else { return }
72
88
  await emitTransactionUpdate(transaction, state: "PaymentTransactionStatePurchased",
73
89
  jwsRepresentation: jwsRepresentation)
74
90
  case .unverified(let transaction, _):
@@ -88,11 +104,27 @@ public class PurchasePlugin: CAPPlugin, CAPBridgedPlugin {
88
104
  debugEnabled = call.getBool("debug", false)
89
105
  debugLog("init")
90
106
 
91
- // Process any pending transactions
107
+ // Load current entitlements and emit to JS so existing subscriptions
108
+ // are visible immediately on app launch (without requiring a manual restore).
92
109
  Task {
93
110
  for await result in Transaction.currentEntitlements {
94
- if case .verified(let transaction) = result {
111
+ switch result {
112
+ case .verified(let transaction):
113
+ if transaction.isUpgraded {
114
+ debugLog("init: skipping upgraded entitlement id=\(transaction.id) product=\(transaction.productID)")
115
+ continue
116
+ }
117
+ if self.sk2.processedTransactionIds.contains(transaction.id) {
118
+ debugLog("init: skipping already-processed entitlement id=\(transaction.id)")
119
+ continue
120
+ }
121
+ self.sk2.processedTransactionIds.insert(transaction.id)
95
122
  self.sk2.unfinishedTransactions[String(transaction.id)] = transaction
123
+ await self.emitTransactionUpdate(transaction,
124
+ state: "PaymentTransactionStateRestored",
125
+ jwsRepresentation: result.jwsRepresentation)
126
+ case .unverified(let transaction, let error):
127
+ debugLog("init: unverified entitlement id=\(transaction.id) product=\(transaction.productID) error=\(error)")
96
128
  }
97
129
  }
98
130
  call.resolve()
@@ -162,12 +194,16 @@ public class PurchasePlugin: CAPPlugin, CAPBridgedPlugin {
162
194
  options.insert(.quantity(quantity))
163
195
  }
164
196
 
197
+ // Clear expired unfinished transactions that could block the purchase
198
+ await clearExpiredUnfinishedTransactions()
199
+
165
200
  let result = try await product.purchase(options: options)
166
201
  switch result {
167
202
  case .success(let verification):
168
203
  let jwsRepresentation = verification.jwsRepresentation
169
204
  switch verification {
170
205
  case .verified(let transaction):
206
+ sk2.processedTransactionIds.insert(transaction.id)
171
207
  await emitTransactionUpdate(transaction,
172
208
  state: "PaymentTransactionStatePurchased",
173
209
  jwsRepresentation: jwsRepresentation)
@@ -218,6 +254,10 @@ public class PurchasePlugin: CAPPlugin, CAPBridgedPlugin {
218
254
  if let transaction = sk2.unfinishedTransactions[transactionId] {
219
255
  await transaction.finish()
220
256
  sk2.unfinishedTransactions.removeValue(forKey: transactionId)
257
+ // Note: do NOT remove from processedTransactionIds here.
258
+ // Once a transaction ID has been emitted to JS, it must stay in the set
259
+ // for the lifetime of the session to prevent Transaction.updates from
260
+ // re-delivering it.
221
261
  notifyListeners("transactionUpdated", data: [
222
262
  "state": "PaymentTransactionStateFinished",
223
263
  "transactionIdentifier": transactionId,
@@ -247,10 +287,18 @@ public class PurchasePlugin: CAPPlugin, CAPBridgedPlugin {
247
287
  do {
248
288
  try await AppStore.sync()
249
289
  for await result in Transaction.currentEntitlements {
250
- if case .verified(let transaction) = result {
290
+ switch result {
291
+ case .verified(let transaction):
292
+ if transaction.isUpgraded {
293
+ debugLog("restore: skipping upgraded entitlement id=\(transaction.id) product=\(transaction.productID)")
294
+ continue
295
+ }
296
+ sk2.processedTransactionIds.insert(transaction.id)
251
297
  await emitTransactionUpdate(transaction,
252
298
  state: "PaymentTransactionStateRestored",
253
299
  jwsRepresentation: result.jwsRepresentation)
300
+ case .unverified(let transaction, let error):
301
+ debugLog("restore: unverified entitlement id=\(transaction.id) product=\(transaction.productID) error=\(error)")
254
302
  }
255
303
  }
256
304
  notifyListeners("restoreCompleted", data: [:])
@@ -346,6 +394,23 @@ public class PurchasePlugin: CAPPlugin, CAPBridgedPlugin {
346
394
 
347
395
  // MARK: - Helpers
348
396
 
397
+ /// Finish any unfinished transactions whose subscription has already expired.
398
+ /// Stale unfinished transactions can block product.purchase() from initiating
399
+ /// a new purchase flow (confirmed on Apple Developer Forums).
400
+ @available(iOS 15.0, *)
401
+ private func clearExpiredUnfinishedTransactions() async {
402
+ for await result in Transaction.unfinished {
403
+ guard case .verified(let transaction) = result else { continue }
404
+ if let expirationDate = transaction.expirationDate, expirationDate < Date() {
405
+ debugLog("clearExpired: finishing expired transaction id=\(transaction.id) product=\(transaction.productID) expired=\(expirationDate)")
406
+ await transaction.finish()
407
+ sk2.unfinishedTransactions.removeValue(forKey: String(transaction.id))
408
+ // Note: do NOT remove from processedTransactionIds.
409
+ // The set must grow monotonically to prevent re-delivery via Transaction.updates.
410
+ }
411
+ }
412
+ }
413
+
349
414
  @available(iOS 15.0, *)
350
415
  private func emitTransactionUpdate(_ transaction: Transaction, state: String,
351
416
  errorCode: Int? = nil, errorText: String? = nil,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-plugin-cdv-purchase",
3
- "version": "13.15.1",
3
+ "version": "13.15.3",
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",
@@ -17,7 +17,7 @@
17
17
  "android": { "src": "android" }
18
18
  },
19
19
  "peerDependencies": {
20
- "@capacitor/core": "^6.0.0"
20
+ "@capacitor/core": "^6.0.0 || ^7.0.0 || ^8.0.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@capacitor/core": "^6.0.0",
@@ -35,7 +35,9 @@
35
35
  "www/",
36
36
  "android/",
37
37
  "!android/build",
38
- "ios/"
38
+ "ios/",
39
+ "Package.swift",
40
+ "CapacitorPluginCdvPurchase.podspec"
39
41
  ],
40
42
  "license": "MIT",
41
43
  "repository": {
package/www/store.d.ts CHANGED
@@ -376,6 +376,9 @@ declare namespace CdvPurchase {
376
376
  private runOnReceipt;
377
377
  private runValidatorFunction;
378
378
  private buildRequestBody;
379
+ /** Check if the products array should be included in the validation request.
380
+ * Returns true at most once per day, tracked via localStorage. */
381
+ private shouldSendProducts;
379
382
  /**
380
383
  * For each md5-hashed values of the validator request's ".transaction" field,
381
384
  * store the response from the server.
@@ -564,6 +567,8 @@ declare namespace CdvPurchase {
564
567
  updatedCallbacks: Callbacks<Product>;
565
568
  updatedReceiptCallbacks: Callbacks<Receipt>;
566
569
  receiptsReadyCallbacks: Callbacks<void>;
570
+ /** Finish a duplicate subscription transaction at the native level so StoreKit won't re-deliver it */
571
+ finishDuplicate(transaction: Transaction): void;
567
572
  }
568
573
  /**
569
574
  * Monitor the updates for products and receipt.
@@ -582,6 +587,21 @@ declare namespace CdvPurchase {
582
587
  [transactionToken: string]: TransactionState;
583
588
  };
584
589
  static makeTransactionToken(transaction: Transaction): string;
590
+ /**
591
+ * Create a subscription dedup key from a transaction.
592
+ *
593
+ * StoreKit 2 can deliver the same subscription purchase event twice with
594
+ * different `transactionId` but identical `originalTransactionId` and
595
+ * `purchaseDate`. This key groups those duplicates together so only one
596
+ * `approved`/`finished` event is surfaced per billing period.
597
+ *
598
+ * Returns `undefined` for non-subscription transactions (no `originalTransactionId`).
599
+ */
600
+ static makeSubscriptionKey(transaction: Transaction): string | undefined;
601
+ /** Remember the first transactionId for each subscription dedup key */
602
+ subscriptionFirstTransactionId: {
603
+ [subscriptionKey: string]: string;
604
+ };
585
605
  /** Store the listener's latest calling time (in ms) for a given transaction at a given state */
586
606
  lastCallTimeForState: {
587
607
  [transactionTokenWithState: string]: number;
@@ -696,7 +716,9 @@ declare namespace CdvPurchase {
696
716
  /**
697
717
  * Name of the group your subscription product is a member of.
698
718
  *
699
- * If you don't set anything, all subscription will be members of the same group.
719
+ * When set, purchasing a subscription in a group will replace the currently
720
+ * owned one (on Google Play, this triggers the subscription replacement flow).
721
+ * When not set, subscriptions are independent — no automatic replacement.
700
722
  */
701
723
  group?: string;
702
724
  }
@@ -864,7 +886,7 @@ declare namespace CdvPurchase {
864
886
  /**
865
887
  * Current release number of the plugin.
866
888
  */
867
- const PLUGIN_VERSION = "13.15.1";
889
+ const PLUGIN_VERSION = "13.15.3";
868
890
  /**
869
891
  * Entry class of the plugin.
870
892
  */
@@ -2812,6 +2834,8 @@ declare namespace CdvPurchase {
2812
2834
  };
2813
2835
  /** Whether this bridge uses StoreKit 2 */
2814
2836
  readonly isSK2?: boolean;
2837
+ /** Resolves when pending transactions from the native queue have been processed */
2838
+ pendingTransactionsReady?: Promise<void>;
2815
2839
  init(options: Partial<BridgeOptions>, success: () => void, error: (code: ErrorCode, message: string) => void): void;
2816
2840
  load(productIds: string[], success: (validProducts: ValidProduct[], invalidProductIds: string[]) => void, error: (code: ErrorCode, message: string) => void): void;
2817
2841
  purchase(productId: string, quantity: number, applicationUsername: string | undefined, discount: PaymentDiscount | undefined, success: () => void, error: () => void): void;
@@ -2846,6 +2870,8 @@ declare namespace CdvPurchase {
2846
2870
  appStoreReceipt?: AppleAppStore.ApplicationReceipt | null;
2847
2871
  private registeredProducts;
2848
2872
  private needRestoreNotification;
2873
+ pendingTransactionsReady?: Promise<void>;
2874
+ private _pendingTransactionsResolve?;
2849
2875
  private pendingUpdates;
2850
2876
  /** True when this bridge is active (SK2 extension installed + iOS 15+) */
2851
2877
  readonly isSK2 = true;
@@ -3038,6 +3064,9 @@ declare namespace CdvPurchase {
3038
3064
  private registeredProducts;
3039
3065
  /** True if "restoreCompleted" or "restoreFailed" should be called when restore is done */
3040
3066
  private needRestoreNotification;
3067
+ /** Resolves when pending transactions from the native queue have been processed */
3068
+ pendingTransactionsReady?: Promise<void>;
3069
+ private _pendingTransactionsResolve?;
3041
3070
  /** List of transaction updates to process */
3042
3071
  private pendingUpdates;
3043
3072
  constructor();
@@ -6168,8 +6197,8 @@ declare namespace CdvPurchase {
6168
6197
  trialPeriodUnit?: SubscriptionPeriodUnit;
6169
6198
  /** Metadata about the user's device */
6170
6199
  device?: CdvPurchase.Validator.DeviceInfo;
6171
- /** List of products available in the store */
6172
- products: {
6200
+ /** List of products available in the store. Included at most once per day. */
6201
+ products?: {
6173
6202
  /** Type of product (subscription, consumable, etc.) */
6174
6203
  type: ProductType;
6175
6204
  /** Product identifier on the store (unique per platform) */
package/www/store.js CHANGED
@@ -688,9 +688,39 @@ var CdvPurchase;
688
688
  body.introPriceMicros = intro.priceMicros;
689
689
  }
690
690
  }
691
+ // Only include the products array once per day to reduce request size.
692
+ // The products array is used by the validator for analytics (price history,
693
+ // transaction amounts) but not for the validation flow itself.
694
+ // The top-level price/currency fields serve as fallback when products is omitted.
695
+ if (!this.shouldSendProducts()) {
696
+ delete body.products;
697
+ }
691
698
  return body;
692
699
  });
693
700
  }
701
+ /** Check if the products array should be included in the validation request.
702
+ * Returns true at most once per day, tracked via localStorage. */
703
+ shouldSendProducts() {
704
+ var _a, _b;
705
+ const STORAGE_KEY = 'cdvpurchase_has_sent_products_in_validation';
706
+ const ONE_DAY_MS = 86400000;
707
+ try {
708
+ const stored = (_a = window.localStorage) === null || _a === void 0 ? void 0 : _a.getItem(STORAGE_KEY);
709
+ if (stored) {
710
+ const lastSent = parseInt(stored, 10);
711
+ if (!isNaN(lastSent) && (Date.now() - lastSent) < ONE_DAY_MS) {
712
+ return false;
713
+ }
714
+ }
715
+ // Send products this time and record the date
716
+ (_b = window.localStorage) === null || _b === void 0 ? void 0 : _b.setItem(STORAGE_KEY, String(Date.now()));
717
+ return true;
718
+ }
719
+ catch (_e) {
720
+ // localStorage not available (e.g. private browsing)
721
+ return true;
722
+ }
723
+ }
694
724
  removeExpiredCache() {
695
725
  const now = +new Date();
696
726
  const deleteList = [];
@@ -1047,6 +1077,8 @@ var CdvPurchase;
1047
1077
  /** Those platforms have reported that their receipts are ready */
1048
1078
  this.platformWithReceiptsReady = [];
1049
1079
  this.lastTransactionState = {};
1080
+ /** Remember the first transactionId for each subscription dedup key */
1081
+ this.subscriptionFirstTransactionId = {};
1050
1082
  /** Store the listener's latest calling time (in ms) for a given transaction at a given state */
1051
1083
  this.lastCallTimeForState = {};
1052
1084
  this.updatedReceiptsToProcess = [];
@@ -1056,6 +1088,26 @@ var CdvPurchase;
1056
1088
  static makeTransactionToken(transaction) {
1057
1089
  return transaction.platform + '|' + transaction.transactionId;
1058
1090
  }
1091
+ /**
1092
+ * Create a subscription dedup key from a transaction.
1093
+ *
1094
+ * StoreKit 2 can deliver the same subscription purchase event twice with
1095
+ * different `transactionId` but identical `originalTransactionId` and
1096
+ * `purchaseDate`. This key groups those duplicates together so only one
1097
+ * `approved`/`finished` event is surfaced per billing period.
1098
+ *
1099
+ * Returns `undefined` for non-subscription transactions (no `originalTransactionId`).
1100
+ */
1101
+ static makeSubscriptionKey(transaction) {
1102
+ if (transaction.platform !== CdvPurchase.Platform.APPLE_APPSTORE)
1103
+ return undefined;
1104
+ const skTransaction = transaction;
1105
+ if (!skTransaction.originalTransactionId)
1106
+ return undefined;
1107
+ if (!transaction.purchaseDate)
1108
+ return undefined;
1109
+ return transaction.platform + '|' + skTransaction.originalTransactionId + '|' + transaction.purchaseDate.getTime();
1110
+ }
1059
1111
  /**
1060
1112
  * Set the list of supported platforms.
1061
1113
  *
@@ -1135,16 +1187,32 @@ var CdvPurchase;
1135
1187
  const transactionToken = StoreAdapterListener.makeTransactionToken(transaction);
1136
1188
  const tokenWithState = transactionToken + '@' + transaction.state;
1137
1189
  const lastState = this.lastTransactionState[transactionToken];
1190
+ const subscriptionKey = StoreAdapterListener.makeSubscriptionKey(transaction);
1191
+ const isSubscriptionDuplicate = !!subscriptionKey
1192
+ && !!this.subscriptionFirstTransactionId[subscriptionKey]
1193
+ && this.subscriptionFirstTransactionId[subscriptionKey] !== transaction.transactionId;
1194
+ // Remember the first transactionId for this subscription key
1195
+ if (subscriptionKey && !this.subscriptionFirstTransactionId[subscriptionKey]) {
1196
+ this.subscriptionFirstTransactionId[subscriptionKey] = transaction.transactionId;
1197
+ }
1138
1198
  // Retrigger "approved", so validation is rerun on potential update.
1139
1199
  if (transaction.state === CdvPurchase.TransactionState.APPROVED) {
1140
- // prevent calling approved twice in a very short period (60 seconds).
1141
- const lastCalled = (_a = this.lastCallTimeForState[tokenWithState]) !== null && _a !== void 0 ? _a : 0;
1142
- if (now - lastCalled > 60000) {
1143
- this.lastCallTimeForState[tokenWithState] = now;
1144
- this.delegate.approvedCallbacks.trigger(transaction, 'adapterListener_receiptsUpdated_approved');
1200
+ if (isSubscriptionDuplicate) {
1201
+ // Finish the duplicate at the native level so StoreKit won't re-deliver it
1202
+ this.log.debug(`Auto-finishing subscription duplicate ${transactionToken}, already processed as ${this.subscriptionFirstTransactionId[subscriptionKey]}`);
1203
+ this.delegate.finishDuplicate(transaction);
1145
1204
  }
1146
1205
  else {
1147
- this.log.debug(`Skipping ${tokenWithState}, because it has been last called ${lastCalled > 0 ? Math.round(now - lastCalled) + 'ms ago (' + now + '-' + lastCalled + ')' : 'never'}`);
1206
+ // Use subscription key for dedup when available, otherwise fall back to transactionToken
1207
+ const dedupKey = subscriptionKey ? subscriptionKey + '@' + transaction.state : tokenWithState;
1208
+ const lastCalled = (_a = this.lastCallTimeForState[dedupKey]) !== null && _a !== void 0 ? _a : 0;
1209
+ if (now - lastCalled > 60000) {
1210
+ this.lastCallTimeForState[dedupKey] = now;
1211
+ this.delegate.approvedCallbacks.trigger(transaction, 'adapterListener_receiptsUpdated_approved');
1212
+ }
1213
+ else {
1214
+ this.log.debug(`Skipping ${tokenWithState}, because it has been last called ${lastCalled > 0 ? Math.round(now - lastCalled) + 'ms ago (' + now + '-' + lastCalled + ')' : 'never'}`);
1215
+ }
1148
1216
  }
1149
1217
  }
1150
1218
  else if (lastState !== transaction.state) {
@@ -1153,15 +1221,31 @@ var CdvPurchase;
1153
1221
  this.delegate.initiatedCallbacks.trigger(transaction, 'adapterListener_receiptsUpdated_initiated');
1154
1222
  }
1155
1223
  else if (transaction.state === CdvPurchase.TransactionState.FINISHED) {
1156
- this.lastCallTimeForState[tokenWithState] = now;
1157
- this.delegate.finishedCallbacks.trigger(transaction, 'adapterListener_receiptsUpdated_finished');
1224
+ if (isSubscriptionDuplicate) {
1225
+ // Finish the duplicate at the native level so StoreKit won't re-deliver it
1226
+ this.log.debug(`Auto-finishing subscription duplicate ${transactionToken}, already processed as ${this.subscriptionFirstTransactionId[subscriptionKey]}`);
1227
+ this.delegate.finishDuplicate(transaction);
1228
+ }
1229
+ else {
1230
+ this.lastCallTimeForState[tokenWithState] = now;
1231
+ this.delegate.finishedCallbacks.trigger(transaction, 'adapterListener_receiptsUpdated_finished');
1232
+ }
1158
1233
  }
1159
1234
  else if (transaction.state === CdvPurchase.TransactionState.PENDING) {
1160
1235
  this.lastCallTimeForState[tokenWithState] = now;
1161
1236
  this.delegate.pendingCallbacks.trigger(transaction, 'adapterListener_receiptsUpdated_pending');
1162
1237
  }
1163
1238
  }
1164
- this.lastTransactionState[transactionToken] = transaction.state;
1239
+ // Mark subscription duplicates as FINISHED so the re-processing triggered
1240
+ // by finishDuplicate()'s receiptsUpdated.call() won't enter the FINISHED
1241
+ // handler and call finishDuplicate() a second time on an already-finished
1242
+ // native transaction.
1243
+ if (isSubscriptionDuplicate) {
1244
+ this.lastTransactionState[transactionToken] = CdvPurchase.TransactionState.FINISHED;
1245
+ }
1246
+ else {
1247
+ this.lastTransactionState[transactionToken] = transaction.state;
1248
+ }
1165
1249
  });
1166
1250
  });
1167
1251
  }
@@ -1578,7 +1662,7 @@ var CdvPurchase;
1578
1662
  /**
1579
1663
  * Current release number of the plugin.
1580
1664
  */
1581
- CdvPurchase.PLUGIN_VERSION = '13.15.1';
1665
+ CdvPurchase.PLUGIN_VERSION = '13.15.3';
1582
1666
  /**
1583
1667
  * Entry class of the plugin.
1584
1668
  */
@@ -1658,6 +1742,12 @@ var CdvPurchase;
1658
1742
  finishedCallbacks: this.finishedCallbacks,
1659
1743
  pendingCallbacks: this.pendingCallbacks,
1660
1744
  receiptsReadyCallbacks: this.receiptsReadyCallbacks,
1745
+ finishDuplicate: (transaction) => {
1746
+ const adapter = this.adapters.findReady(transaction.platform);
1747
+ if (adapter) {
1748
+ adapter.finish(transaction);
1749
+ }
1750
+ },
1661
1751
  }, this.log);
1662
1752
  this.transactionStateMonitors = new CdvPurchase.Internal.TransactionStateMonitors(this.when());
1663
1753
  this._validator = new CdvPurchase.Internal.Validator({
@@ -3104,9 +3194,14 @@ var CdvPurchase;
3104
3194
  this.initializeAppReceipt(() => {
3105
3195
  var _a;
3106
3196
  if (!this._receipt) {
3107
- // this should not happen
3108
- this.log.warn('Failed to load the application receipt, cannot proceed with handling the purchase');
3109
- return;
3197
+ // Receipt failed to load — create a minimal receipt so the
3198
+ // transaction can still be tracked and finished. Without this
3199
+ // fallback the Promise would never resolve, leaving the native
3200
+ // transaction unfinished (causing the purchase dialog to loop on
3201
+ // iOS — see #1568).
3202
+ this.log.warn('Application receipt unavailable, creating a fallback receipt to avoid blocking transactions');
3203
+ this._receipt = new AppleAppStore.SKApplicationReceipt({ appStoreReceipt: '', bundleIdentifier: '',
3204
+ bundleShortVersion: '', bundleNumericVersion: 0, bundleSignature: '' }, this.needAppReceipt, this.context.apiDecorators);
3110
3205
  }
3111
3206
  const existing = (_a = this._receipt) === null || _a === void 0 ? void 0 : _a.transactions.find(t => t.transactionId === transactionId);
3112
3207
  if (existing) {
@@ -3268,8 +3363,22 @@ var CdvPurchase;
3268
3363
  });
3269
3364
  }
3270
3365
  loadReceipts() {
3271
- return new Promise((resolve) => {
3272
- setTimeout(() => {
3366
+ return __awaiter(this, void 0, void 0, function* () {
3367
+ // Wait for native pending transactions to be processed before
3368
+ // initializing the receipt. Previously used a fixed 300ms delay
3369
+ // which was unreliable and could miss transactions that arrived
3370
+ // from the native queue (see #1529).
3371
+ //
3372
+ // The pendingTransactionsReady Promise resolves when the native
3373
+ // `processPendingTransactions` call returns, but the per-transaction
3374
+ // callbacks it triggers via evalJs land asynchronously. Yield one
3375
+ // task so those purchased/restored callbacks (and their async
3376
+ // upsertTransaction chain) settle before we snapshot _receipt.
3377
+ if (this.bridge.pendingTransactionsReady) {
3378
+ yield this.bridge.pendingTransactionsReady;
3379
+ yield new Promise(r => setTimeout(r, 0));
3380
+ }
3381
+ return new Promise((resolve) => {
3273
3382
  this.initializeAppReceipt(() => {
3274
3383
  this.receiptsUpdated.call();
3275
3384
  if (this._receipt) {
@@ -3279,7 +3388,7 @@ var CdvPurchase;
3279
3388
  resolve([this.pseudoReceipt]);
3280
3389
  }
3281
3390
  });
3282
- }, 300);
3391
+ });
3283
3392
  });
3284
3393
  }
3285
3394
  canMakePayments() {
@@ -3335,6 +3444,7 @@ var CdvPurchase;
3335
3444
  return;
3336
3445
  }
3337
3446
  this._receipt = new AppleAppStore.SKApplicationReceipt(nativeData, this.needAppReceipt, this.context.apiDecorators);
3447
+ this._appStoreReceiptLoading = false;
3338
3448
  callCallbacks(undefined);
3339
3449
  });
3340
3450
  }
@@ -3548,16 +3658,20 @@ var CdvPurchase;
3548
3658
  this.receiptsUpdated.call();
3549
3659
  return resolve(undefined);
3550
3660
  }
3661
+ const wasAlreadyFinished = transaction.state === CdvPurchase.TransactionState.FINISHED;
3551
3662
  const success = () => {
3552
3663
  transaction.state = CdvPurchase.TransactionState.FINISHED;
3553
- this.receiptsUpdated.call();
3664
+ if (!wasAlreadyFinished) {
3665
+ this.receiptsUpdated.call();
3666
+ }
3554
3667
  resolve(undefined);
3555
3668
  };
3556
3669
  const error = (msg) => {
3557
3670
  var _a, _b;
3558
3671
  if (msg === null || msg === void 0 ? void 0 : msg.includes('[#CdvPurchase:100]')) {
3559
- // already finished
3560
- success();
3672
+ // already finished at the native level
3673
+ transaction.state = CdvPurchase.TransactionState.FINISHED;
3674
+ resolve(undefined);
3561
3675
  }
3562
3676
  else {
3563
3677
  resolve(appStoreError(CdvPurchase.ErrorCode.FINISH, 'Failed to finish transaction', (_b = (_a = transaction.products[0]) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null));
@@ -4123,6 +4237,9 @@ var CdvPurchase;
4123
4237
  protectCall(this.options.ready, 'options.ready');
4124
4238
  protectCall(success, 'init.success');
4125
4239
  this.initialized = true;
4240
+ this.pendingTransactionsReady = new Promise(resolve => {
4241
+ this._pendingTransactionsResolve = resolve;
4242
+ });
4126
4243
  setTimeout(() => this.processPendingTransactions(), 50);
4127
4244
  };
4128
4245
  const setupFailed = (err) => {
@@ -4135,6 +4252,10 @@ var CdvPurchase;
4135
4252
  log('processing pending transactions');
4136
4253
  exec('processPendingTransactions', [], () => {
4137
4254
  this.finalizeTransactionUpdates();
4255
+ if (this._pendingTransactionsResolve) {
4256
+ this._pendingTransactionsResolve();
4257
+ this._pendingTransactionsResolve = undefined;
4258
+ }
4138
4259
  }, undefined);
4139
4260
  }
4140
4261
  purchase(productId, quantity, applicationUsername, discount, success, error) {
@@ -4446,6 +4567,9 @@ var CdvPurchase;
4446
4567
  protectCall(this.options.ready, 'options.ready');
4447
4568
  protectCall(success, 'init.success');
4448
4569
  this.initialized = true;
4570
+ this.pendingTransactionsReady = new Promise(resolve => {
4571
+ this._pendingTransactionsResolve = resolve;
4572
+ });
4449
4573
  setTimeout(() => this.processPendingTransactions(), 50);
4450
4574
  };
4451
4575
  const setupFailed = (err) => {
@@ -4459,6 +4583,10 @@ var CdvPurchase;
4459
4583
  log('processing pending transactions');
4460
4584
  exec('processPendingTransactions', [], () => {
4461
4585
  this.finalizeTransactionUpdates();
4586
+ if (this._pendingTransactionsResolve) {
4587
+ this._pendingTransactionsResolve();
4588
+ this._pendingTransactionsResolve = undefined;
4589
+ }
4462
4590
  }, undefined);
4463
4591
  }
4464
4592
  /**
@@ -4848,6 +4976,13 @@ var CdvPurchase;
4848
4976
  offerType: 'Default',
4849
4977
  }, decorator));
4850
4978
  }
4979
+ // Ensure the default offer is always first so that getOffer()
4980
+ // without arguments returns it instead of a discount offer.
4981
+ const defaultIndex = this.offers.findIndex(o => o.id === AppleAppStore.DEFAULT_OFFER_ID);
4982
+ if (defaultIndex > 0) {
4983
+ const [defaultOffer] = this.offers.splice(defaultIndex, 1);
4984
+ this.offers.unshift(defaultOffer);
4985
+ }
4851
4986
  function hasIntroductoryOffer(product) {
4852
4987
  return product.offers.filter(offer => {
4853
4988
  const skOffer = offer;