@capgo/native-purchases 7.17.0 → 7.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,7 +30,6 @@ import com.google.common.collect.ImmutableList;
30
30
  import java.util.ArrayList;
31
31
  import java.util.Collections;
32
32
  import java.util.List;
33
- import java.util.Objects;
34
33
  import java.util.concurrent.CountDownLatch;
35
34
  import java.util.concurrent.Phaser;
36
35
  import java.util.concurrent.TimeUnit;
@@ -42,7 +41,7 @@ import org.json.JSONArray;
42
41
  @CapacitorPlugin(name = "NativePurchases")
43
42
  public class NativePurchasesPlugin extends Plugin {
44
43
 
45
- private final String pluginVersion = "7.17.0";
44
+ private final String pluginVersion = "7.18.0";
46
45
  public static final String TAG = "NativePurchases";
47
46
  private static final Phaser semaphoreReady = new Phaser(1);
48
47
  private BillingClient billingClient;
@@ -52,10 +51,32 @@ public class NativePurchasesPlugin extends Plugin {
52
51
  @PluginMethod
53
52
  public void isBillingSupported(PluginCall call) {
54
53
  Log.d(TAG, "isBillingSupported() called");
55
- JSObject ret = new JSObject();
56
- ret.put("isBillingSupported", true);
57
- Log.d(TAG, "isBillingSupported() returning true");
58
- call.resolve(ret);
54
+ try {
55
+ // Try to initialize billing client to check if billing is actually available
56
+ // Pass null so initBillingClient doesn't reject the call - we'll handle the result ourselves
57
+ this.initBillingClient(null);
58
+ // If initialization succeeded, billing is supported
59
+ JSObject ret = new JSObject();
60
+ ret.put("isBillingSupported", true);
61
+ Log.d(TAG, "isBillingSupported() returning true - billing client initialized successfully");
62
+ closeBillingClient();
63
+ call.resolve(ret);
64
+ } catch (RuntimeException e) {
65
+ Log.e(TAG, "isBillingSupported() - billing client initialization failed: " + e.getMessage());
66
+ closeBillingClient();
67
+ // Return false instead of rejecting - this is a check method
68
+ JSObject ret = new JSObject();
69
+ ret.put("isBillingSupported", false);
70
+ Log.d(TAG, "isBillingSupported() returning false - billing not available");
71
+ call.resolve(ret);
72
+ } catch (Exception e) {
73
+ Log.e(TAG, "isBillingSupported() - unexpected error: " + e.getMessage());
74
+ closeBillingClient();
75
+ JSObject ret = new JSObject();
76
+ ret.put("isBillingSupported", false);
77
+ Log.d(TAG, "isBillingSupported() returning false - unexpected error");
78
+ call.resolve(ret);
79
+ }
59
80
  }
60
81
 
61
82
  @Override
@@ -665,44 +686,67 @@ public class NativePurchasesPlugin extends Plugin {
665
686
  if (productType.equals("inapp")) {
666
687
  Log.d(TAG, "Processing as in-app product");
667
688
  product.put("identifier", productDetails.getProductId());
668
- double price =
669
- Objects.requireNonNull(productDetails.getOneTimePurchaseOfferDetails()).getPriceAmountMicros() / 1000000.0;
689
+ ProductDetails.OneTimePurchaseOfferDetails oneTimeOfferDetails =
690
+ productDetails.getOneTimePurchaseOfferDetails();
691
+ if (oneTimeOfferDetails == null) {
692
+ Log.w(TAG, "No one-time purchase offer details found for product: " + productDetails.getProductId());
693
+ closeBillingClient();
694
+ call.reject("No one-time purchase offer details found for product: " + productDetails.getProductId());
695
+ return;
696
+ }
697
+ double price = oneTimeOfferDetails.getPriceAmountMicros() / 1000000.0;
670
698
  product.put("price", price);
671
- product.put("priceString", productDetails.getOneTimePurchaseOfferDetails().getFormattedPrice());
672
- product.put("currencyCode", productDetails.getOneTimePurchaseOfferDetails().getPriceCurrencyCode());
699
+ product.put("priceString", oneTimeOfferDetails.getFormattedPrice());
700
+ product.put("currencyCode", oneTimeOfferDetails.getPriceCurrencyCode());
673
701
  Log.d(TAG, "Price: " + price);
674
- Log.d(TAG, "Formatted price: " + productDetails.getOneTimePurchaseOfferDetails().getFormattedPrice());
675
- Log.d(TAG, "Currency: " + productDetails.getOneTimePurchaseOfferDetails().getPriceCurrencyCode());
702
+ Log.d(TAG, "Formatted price: " + oneTimeOfferDetails.getFormattedPrice());
703
+ Log.d(TAG, "Currency: " + oneTimeOfferDetails.getPriceCurrencyCode());
676
704
  } else {
677
705
  Log.d(TAG, "Processing as subscription product");
678
- ProductDetails.SubscriptionOfferDetails selectedOfferDetails = productDetails
679
- .getSubscriptionOfferDetails()
706
+ List<ProductDetails.SubscriptionOfferDetails> offerDetailsList = productDetails.getSubscriptionOfferDetails();
707
+ if (offerDetailsList == null || offerDetailsList.isEmpty()) {
708
+ Log.w(TAG, "No subscription offer details found for product: " + productDetails.getProductId());
709
+ closeBillingClient();
710
+ call.reject("No subscription offers found for product: " + productDetails.getProductId());
711
+ return;
712
+ }
713
+
714
+ ProductDetails.SubscriptionOfferDetails selectedOfferDetails = null;
715
+ for (ProductDetails.SubscriptionOfferDetails offerDetails : offerDetailsList) {
716
+ if (
717
+ offerDetails.getPricingPhases() != null &&
718
+ !offerDetails.getPricingPhases().getPricingPhaseList().isEmpty()
719
+ ) {
720
+ selectedOfferDetails = offerDetails;
721
+ break;
722
+ }
723
+ }
724
+
725
+ if (selectedOfferDetails == null) {
726
+ Log.w(TAG, "No offers with pricing phases found for product: " + productDetails.getProductId());
727
+ closeBillingClient();
728
+ call.reject("No pricing phases found for product: " + productDetails.getProductId());
729
+ return;
730
+ }
731
+
732
+ ProductDetails.PricingPhase firstPricingPhase = selectedOfferDetails
733
+ .getPricingPhases()
734
+ .getPricingPhaseList()
680
735
  .get(0);
681
736
  product.put("planIdentifier", productDetails.getProductId());
682
737
  product.put("identifier", selectedOfferDetails.getBasePlanId());
683
- double price =
684
- selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getPriceAmountMicros() / 1000000.0;
738
+ product.put("offerToken", selectedOfferDetails.getOfferToken());
739
+ product.put("offerId", selectedOfferDetails.getOfferId());
740
+ double price = firstPricingPhase.getPriceAmountMicros() / 1000000.0;
685
741
  product.put("price", price);
686
- product.put(
687
- "priceString",
688
- selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice()
689
- );
690
- product.put(
691
- "currencyCode",
692
- selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getPriceCurrencyCode()
693
- );
742
+ product.put("priceString", firstPricingPhase.getFormattedPrice());
743
+ product.put("currencyCode", firstPricingPhase.getPriceCurrencyCode());
694
744
  Log.d(TAG, "Plan identifier: " + productDetails.getProductId());
695
745
  Log.d(TAG, "Base plan ID: " + selectedOfferDetails.getBasePlanId());
746
+ Log.d(TAG, "Offer token: " + selectedOfferDetails.getOfferToken());
696
747
  Log.d(TAG, "Price: " + price);
697
- Log.d(
698
- TAG,
699
- "Formatted price: " +
700
- selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice()
701
- );
702
- Log.d(
703
- TAG,
704
- "Currency: " + selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getPriceCurrencyCode()
705
- );
748
+ Log.d(TAG, "Formatted price: " + firstPricingPhase.getFormattedPrice());
749
+ Log.d(TAG, "Currency: " + firstPricingPhase.getPriceCurrencyCode());
706
750
  }
707
751
  product.put("isFamilyShareable", false);
708
752
 
@@ -777,58 +821,86 @@ public class NativePurchasesPlugin extends Plugin {
777
821
  JSONArray products = new JSONArray();
778
822
  for (ProductDetails productDetails : productDetailsList) {
779
823
  Log.d(TAG, "Processing product details: " + productDetails.getProductId());
780
- JSObject product = new JSObject();
781
- product.put("title", productDetails.getName());
782
- product.put("description", productDetails.getDescription());
783
824
  Log.d(TAG, "Product title: " + productDetails.getName());
784
825
  Log.d(TAG, "Product description: " + productDetails.getDescription());
785
826
 
786
827
  if (productType.equals("inapp")) {
787
828
  Log.d(TAG, "Processing as in-app product");
829
+ JSObject product = new JSObject();
830
+ product.put("title", productDetails.getName());
831
+ product.put("description", productDetails.getDescription());
788
832
  product.put("identifier", productDetails.getProductId());
789
- double price =
790
- Objects.requireNonNull(productDetails.getOneTimePurchaseOfferDetails()).getPriceAmountMicros() /
791
- 1000000.0;
833
+
834
+ ProductDetails.OneTimePurchaseOfferDetails oneTimeOfferDetails =
835
+ productDetails.getOneTimePurchaseOfferDetails();
836
+ if (oneTimeOfferDetails == null) {
837
+ Log.w(TAG, "No one-time purchase offer details found for product: " + productDetails.getProductId());
838
+ continue;
839
+ }
840
+
841
+ double price = oneTimeOfferDetails.getPriceAmountMicros() / 1000000.0;
792
842
  product.put("price", price);
793
- product.put("priceString", productDetails.getOneTimePurchaseOfferDetails().getFormattedPrice());
794
- product.put("currencyCode", productDetails.getOneTimePurchaseOfferDetails().getPriceCurrencyCode());
843
+ product.put("priceString", oneTimeOfferDetails.getFormattedPrice());
844
+ product.put("currencyCode", oneTimeOfferDetails.getPriceCurrencyCode());
845
+ product.put("isFamilyShareable", false);
795
846
  Log.d(TAG, "Price: " + price);
796
- Log.d(TAG, "Formatted price: " + productDetails.getOneTimePurchaseOfferDetails().getFormattedPrice());
797
- Log.d(TAG, "Currency: " + productDetails.getOneTimePurchaseOfferDetails().getPriceCurrencyCode());
847
+ Log.d(TAG, "Formatted price: " + oneTimeOfferDetails.getFormattedPrice());
848
+ Log.d(TAG, "Currency: " + oneTimeOfferDetails.getPriceCurrencyCode());
849
+ products.put(product);
798
850
  } else {
799
851
  Log.d(TAG, "Processing as subscription product");
800
- ProductDetails.SubscriptionOfferDetails selectedOfferDetails = productDetails
801
- .getSubscriptionOfferDetails()
802
- .get(0);
803
- product.put("planIdentifier", productDetails.getProductId());
804
- product.put("identifier", selectedOfferDetails.getBasePlanId());
805
- double price =
806
- selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getPriceAmountMicros() / 1000000.0;
807
- product.put("price", price);
808
- product.put(
809
- "priceString",
810
- selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice()
811
- );
812
- product.put(
813
- "currencyCode",
814
- selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getPriceCurrencyCode()
815
- );
816
- Log.d(TAG, "Plan identifier: " + productDetails.getProductId());
817
- Log.d(TAG, "Base plan ID: " + selectedOfferDetails.getBasePlanId());
818
- Log.d(TAG, "Price: " + price);
819
- Log.d(
820
- TAG,
821
- "Formatted price: " +
822
- selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice()
823
- );
824
- Log.d(
825
- TAG,
826
- "Currency: " +
827
- selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getPriceCurrencyCode()
828
- );
852
+ List<ProductDetails.SubscriptionOfferDetails> offerDetailsList =
853
+ productDetails.getSubscriptionOfferDetails();
854
+ if (offerDetailsList == null || offerDetailsList.isEmpty()) {
855
+ Log.w(TAG, "No subscription offer details found for product: " + productDetails.getProductId());
856
+ continue;
857
+ }
858
+
859
+ int addedOffers = 0;
860
+ for (ProductDetails.SubscriptionOfferDetails offerDetails : offerDetailsList) {
861
+ if (
862
+ offerDetails.getPricingPhases() == null ||
863
+ offerDetails.getPricingPhases().getPricingPhaseList().isEmpty()
864
+ ) {
865
+ Log.w(TAG, "No pricing phases found for offer: " + offerDetails.getBasePlanId());
866
+ continue;
867
+ }
868
+
869
+ JSObject product = new JSObject();
870
+ product.put("title", productDetails.getName());
871
+ product.put("description", productDetails.getDescription());
872
+ product.put("planIdentifier", productDetails.getProductId());
873
+ product.put("identifier", offerDetails.getBasePlanId());
874
+ product.put("offerToken", offerDetails.getOfferToken());
875
+ product.put("offerId", offerDetails.getOfferId());
876
+
877
+ ProductDetails.PricingPhase firstPricingPhase = offerDetails
878
+ .getPricingPhases()
879
+ .getPricingPhaseList()
880
+ .get(0);
881
+ double price = firstPricingPhase.getPriceAmountMicros() / 1000000.0;
882
+ product.put("price", price);
883
+ product.put("priceString", firstPricingPhase.getFormattedPrice());
884
+ product.put("currencyCode", firstPricingPhase.getPriceCurrencyCode());
885
+ product.put("isFamilyShareable", false);
886
+
887
+ Log.d(TAG, "Plan identifier: " + productDetails.getProductId());
888
+ Log.d(TAG, "Base plan ID: " + offerDetails.getBasePlanId());
889
+ Log.d(TAG, "Price: " + price);
890
+ Log.d(TAG, "Formatted price: " + firstPricingPhase.getFormattedPrice());
891
+ Log.d(TAG, "Currency: " + firstPricingPhase.getPriceCurrencyCode());
892
+
893
+ products.put(product);
894
+ addedOffers++;
895
+ }
896
+
897
+ if (addedOffers == 0) {
898
+ Log.w(
899
+ TAG,
900
+ "All subscription offers missing pricing phases for product: " + productDetails.getProductId()
901
+ );
902
+ }
829
903
  }
830
- product.put("isFamilyShareable", false);
831
- products.put(product);
832
904
  }
833
905
  JSObject ret = new JSObject();
834
906
  ret.put("products", products);
@@ -1122,6 +1194,50 @@ public class NativePurchasesPlugin extends Plugin {
1122
1194
  }
1123
1195
  }
1124
1196
 
1197
+ @PluginMethod
1198
+ public void consumePurchase(PluginCall call) {
1199
+ Log.d(TAG, "consumePurchase() called");
1200
+ String purchaseToken = call.getString("purchaseToken");
1201
+
1202
+ if (purchaseToken == null || purchaseToken.isEmpty()) {
1203
+ Log.d(TAG, "Error: purchaseToken is empty");
1204
+ call.reject("purchaseToken is required");
1205
+ return;
1206
+ }
1207
+
1208
+ Log.d(TAG, "Consuming purchase with token: " + purchaseToken);
1209
+ try {
1210
+ this.initBillingClient(call);
1211
+ } catch (RuntimeException e) {
1212
+ Log.e(TAG, "Failed to initialize billing client: " + e.getMessage());
1213
+ closeBillingClient();
1214
+ return;
1215
+ }
1216
+
1217
+ try {
1218
+ ConsumeParams consumeParams = ConsumeParams.newBuilder().setPurchaseToken(purchaseToken).build();
1219
+
1220
+ billingClient.consumeAsync(consumeParams, (billingResult, consumedToken) -> {
1221
+ Log.d(TAG, "onConsumeResponse() called");
1222
+ Log.d(TAG, "Consume result: " + billingResult.getResponseCode() + " - " + billingResult.getDebugMessage());
1223
+
1224
+ if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
1225
+ Log.d(TAG, "Purchase consumed successfully");
1226
+ closeBillingClient();
1227
+ call.resolve();
1228
+ } else {
1229
+ Log.d(TAG, "Purchase consumption failed");
1230
+ closeBillingClient();
1231
+ call.reject("Failed to consume purchase: " + billingResult.getDebugMessage());
1232
+ }
1233
+ });
1234
+ } catch (Exception e) {
1235
+ Log.d(TAG, "Exception during consumePurchase: " + e.getMessage());
1236
+ closeBillingClient();
1237
+ call.reject(e.getMessage());
1238
+ }
1239
+ }
1240
+
1125
1241
  @PluginMethod
1126
1242
  public void getAppTransaction(PluginCall call) {
1127
1243
  Log.d(TAG, "getAppTransaction() called");
package/dist/docs.json CHANGED
@@ -362,6 +362,51 @@
362
362
  "complexTypes": [],
363
363
  "slug": "acknowledgepurchase"
364
364
  },
365
+ {
366
+ "name": "consumePurchase",
367
+ "signature": "(options: { purchaseToken: string; }) => Promise<void>",
368
+ "parameters": [
369
+ {
370
+ "name": "options",
371
+ "docs": "- The purchase to consume",
372
+ "type": "{ purchaseToken: string; }"
373
+ }
374
+ ],
375
+ "returns": "Promise<void>",
376
+ "tags": [
377
+ {
378
+ "name": "param",
379
+ "text": "options - The purchase to consume"
380
+ },
381
+ {
382
+ "name": "param",
383
+ "text": "options.purchaseToken - The purchase token from the Transaction object"
384
+ },
385
+ {
386
+ "name": "returns",
387
+ "text": "Promise that resolves when the purchase is consumed"
388
+ },
389
+ {
390
+ "name": "throws",
391
+ "text": "Error if consumption fails, token is invalid, or called on iOS/web"
392
+ },
393
+ {
394
+ "name": "platform",
395
+ "text": "android"
396
+ },
397
+ {
398
+ "name": "since",
399
+ "text": "8.2.0"
400
+ },
401
+ {
402
+ "name": "example",
403
+ "text": "```typescript\nconst transaction = await NativePurchases.purchaseProduct({\n productIdentifier: 'coins_100',\n isConsumable: false,\n autoAcknowledgePurchases: false\n});\n\n// Validate with your backend first\nconst isValid = await validateWithServer(transaction.purchaseToken);\n\nif (isValid) {\n // Grant the coins, then consume to allow re-purchase\n await NativePurchases.consumePurchase({\n purchaseToken: transaction.purchaseToken!\n });\n}\n```"
404
+ }
405
+ ],
406
+ "docs": "Consume an in-app purchase on Android.\n\nConsuming a purchase does two things:\n1. Acknowledges the purchase (so you don't need to call acknowledgePurchase separately)\n2. Removes ownership, allowing the user to buy the same product again\n\nUse this for consumable products like virtual currency, extra lives, or credits.\n\n**Important:** In Google Play Billing Library 8.x, consumed purchases can no longer\nbe queried via getPurchases(). Once consumed, the purchase is gone.\n\nAndroid only — iOS does not have a separate consume concept.\nOn iOS and web, this method rejects with an error.",
407
+ "complexTypes": [],
408
+ "slug": "consumepurchase"
409
+ },
365
410
  {
366
411
  "name": "addListener",
367
412
  "signature": "(eventName: 'transactionUpdated', listenerFunc: (transaction: Transaction) => void) => Promise<PluginListenerHandle>",
@@ -619,9 +664,13 @@
619
664
  {
620
665
  "text": "android Not available (use purchaseToken instead)",
621
666
  "name": "platform"
667
+ },
668
+ {
669
+ "text": "```typescript\nconst transaction = await NativePurchases.purchaseProduct({ ... });\nif (transaction.receipt) {\n // Send to your backend for validation\n await fetch('/api/validate-receipt', {\n method: 'POST',\n body: JSON.stringify({ receipt: transaction.receipt })\n });\n}\n```",
670
+ "name": "example"
622
671
  }
623
672
  ],
624
- "docs": "Receipt data for validation (base64 encoded StoreKit receipt).\n\nSend this to your backend for server-side validation with Apple's receipt verification API.\nThe receipt remains available even after refund - server validation is required to detect refunded transactions.",
673
+ "docs": "Receipt data for validation (base64 encoded StoreKit receipt).\n\n**This is the full verified receipt payload from Apple StoreKit.**\nSend this to your backend for server-side validation with Apple's receipt verification API.\nThe receipt remains available even after refund - server validation is required to detect refunded transactions.\n\n**For backend validation:**\n- Use Apple's receipt verification API: https://buy.itunes.apple.com/verifyReceipt (production)\n- Or sandbox: https://sandbox.itunes.apple.com/verifyReceipt\n- This contains all transaction data needed for validation\n\n**Note:** Apple recommends migrating to App Store Server API v2 with `jwsRepresentation` for new implementations.\nThe legacy receipt verification API continues to work but may be deprecated in the future.",
625
674
  "complexTypes": [],
626
675
  "type": "string | undefined"
627
676
  },
@@ -639,9 +688,13 @@
639
688
  {
640
689
  "text": "android Not available",
641
690
  "name": "platform"
691
+ },
692
+ {
693
+ "text": "```typescript\nconst transaction = await NativePurchases.purchaseProduct({ ... });\nif (transaction.jwsRepresentation) {\n // Send to your backend for validation with App Store Server API v2\n await fetch('/api/validate-jws', {\n method: 'POST',\n body: JSON.stringify({ jws: transaction.jwsRepresentation })\n });\n}\n```",
694
+ "name": "example"
642
695
  }
643
696
  ],
644
- "docs": "StoreKit 2 JSON Web Signature (JWS) payload describing the verified transaction.\n\nSend this to your backend when using Apple's App Store Server API v2 instead of raw receipts.\nOnly available when the transaction originated from StoreKit 2 APIs (e.g. Transaction.updates).",
697
+ "docs": "StoreKit 2 JSON Web Signature (JWS) payload describing the verified transaction.\n\n**This is the full verified receipt in JWS format (StoreKit 2).**\nSend this to your backend when using Apple's App Store Server API v2 instead of raw receipts.\nOnly available when the transaction originated from StoreKit 2 APIs (e.g. Transaction.updates).\n\n**For backend validation:**\n- Use Apple's App Store Server API v2 to decode and verify the JWS\n- This is the modern alternative to the legacy receipt format\n- Contains signed transaction information from Apple",
645
698
  "complexTypes": [],
646
699
  "type": "string | undefined"
647
700
  },
@@ -918,9 +971,13 @@
918
971
  {
919
972
  "text": "android Always present",
920
973
  "name": "platform"
974
+ },
975
+ {
976
+ "text": "```typescript\nconst transaction = await NativePurchases.purchaseProduct({ ... });\nif (transaction.purchaseToken) {\n // Send to your backend for validation\n await fetch('/api/validate-purchase', {\n method: 'POST',\n body: JSON.stringify({\n purchaseToken: transaction.purchaseToken,\n productId: transaction.productIdentifier\n })\n });\n}\n```",
977
+ "name": "example"
921
978
  }
922
979
  ],
923
- "docs": "Purchase token associated with the transaction.\n\nSend this to your backend for server-side validation with Google Play Developer API.\nThis is the Android equivalent of iOS's receipt field.",
980
+ "docs": "Purchase token associated with the transaction.\n\n**This is the full verified purchase token from Google Play.**\nSend this to your backend for server-side validation with Google Play Developer API.\nThis is the Android equivalent of iOS's receipt field.\n\n**For backend validation:**\n- Use Google Play Developer API v3 to verify the purchase\n- API endpoint: androidpublisher.purchases.products.get() or purchases.subscriptions.get()\n- This token contains all data needed for validation with Google servers\n- Can also be used for subscription status checks and cancellation detection",
924
981
  "complexTypes": [],
925
982
  "type": "string | undefined"
926
983
  },
@@ -1120,7 +1177,7 @@
1120
1177
  {
1121
1178
  "name": "identifier",
1122
1179
  "tags": [],
1123
- "docs": "Product Id.",
1180
+ "docs": "Product Id.\n\nAndroid subscriptions note:\n- `identifier` is the base plan ID (`offerDetails.getBasePlanId()`).\n- `planIdentifier` is the subscription product ID (`productDetails.getProductId()`).\n\nIf you group/filter Android subscription results by `identifier`, you are grouping by base plan.",
1124
1181
  "complexTypes": [],
1125
1182
  "type": "string"
1126
1183
  },
@@ -1180,6 +1237,27 @@
1180
1237
  "complexTypes": [],
1181
1238
  "type": "string"
1182
1239
  },
1240
+ {
1241
+ "name": "planIdentifier",
1242
+ "tags": [],
1243
+ "docs": "Android subscriptions only: Google Play product identifier tied to the offer/base plan set.",
1244
+ "complexTypes": [],
1245
+ "type": "string | undefined"
1246
+ },
1247
+ {
1248
+ "name": "offerToken",
1249
+ "tags": [],
1250
+ "docs": "Android subscriptions only: offer token required when purchasing specific offers.",
1251
+ "complexTypes": [],
1252
+ "type": "string | undefined"
1253
+ },
1254
+ {
1255
+ "name": "offerId",
1256
+ "tags": [],
1257
+ "docs": "Android subscriptions only: offer identifier (null/undefined for base offers).",
1258
+ "complexTypes": [],
1259
+ "type": "string | null | undefined"
1260
+ },
1183
1261
  {
1184
1262
  "name": "subscriptionPeriod",
1185
1263
  "tags": [],
@@ -132,23 +132,60 @@ export interface Transaction {
132
132
  /**
133
133
  * Receipt data for validation (base64 encoded StoreKit receipt).
134
134
  *
135
+ * **This is the full verified receipt payload from Apple StoreKit.**
135
136
  * Send this to your backend for server-side validation with Apple's receipt verification API.
136
137
  * The receipt remains available even after refund - server validation is required to detect refunded transactions.
137
138
  *
139
+ * **For backend validation:**
140
+ * - Use Apple's receipt verification API: https://buy.itunes.apple.com/verifyReceipt (production)
141
+ * - Or sandbox: https://sandbox.itunes.apple.com/verifyReceipt
142
+ * - This contains all transaction data needed for validation
143
+ *
144
+ * **Note:** Apple recommends migrating to App Store Server API v2 with `jwsRepresentation` for new implementations.
145
+ * The legacy receipt verification API continues to work but may be deprecated in the future.
146
+ *
138
147
  * @since 1.0.0
139
148
  * @platform ios Always present
140
149
  * @platform android Not available (use purchaseToken instead)
150
+ * @example
151
+ * ```typescript
152
+ * const transaction = await NativePurchases.purchaseProduct({ ... });
153
+ * if (transaction.receipt) {
154
+ * // Send to your backend for validation
155
+ * await fetch('/api/validate-receipt', {
156
+ * method: 'POST',
157
+ * body: JSON.stringify({ receipt: transaction.receipt })
158
+ * });
159
+ * }
160
+ * ```
141
161
  */
142
162
  readonly receipt?: string;
143
163
  /**
144
164
  * StoreKit 2 JSON Web Signature (JWS) payload describing the verified transaction.
145
165
  *
166
+ * **This is the full verified receipt in JWS format (StoreKit 2).**
146
167
  * Send this to your backend when using Apple's App Store Server API v2 instead of raw receipts.
147
168
  * Only available when the transaction originated from StoreKit 2 APIs (e.g. Transaction.updates).
148
169
  *
170
+ * **For backend validation:**
171
+ * - Use Apple's App Store Server API v2 to decode and verify the JWS
172
+ * - This is the modern alternative to the legacy receipt format
173
+ * - Contains signed transaction information from Apple
174
+ *
149
175
  * @since 7.13.2
150
176
  * @platform ios Present for StoreKit 2 transactions (iOS 15+)
151
177
  * @platform android Not available
178
+ * @example
179
+ * ```typescript
180
+ * const transaction = await NativePurchases.purchaseProduct({ ... });
181
+ * if (transaction.jwsRepresentation) {
182
+ * // Send to your backend for validation with App Store Server API v2
183
+ * await fetch('/api/validate-jws', {
184
+ * method: 'POST',
185
+ * body: JSON.stringify({ jws: transaction.jwsRepresentation })
186
+ * });
187
+ * }
188
+ * ```
152
189
  */
153
190
  readonly jwsRepresentation?: string;
154
191
  /**
@@ -325,12 +362,33 @@ export interface Transaction {
325
362
  /**
326
363
  * Purchase token associated with the transaction.
327
364
  *
365
+ * **This is the full verified purchase token from Google Play.**
328
366
  * Send this to your backend for server-side validation with Google Play Developer API.
329
367
  * This is the Android equivalent of iOS's receipt field.
330
368
  *
369
+ * **For backend validation:**
370
+ * - Use Google Play Developer API v3 to verify the purchase
371
+ * - API endpoint: androidpublisher.purchases.products.get() or purchases.subscriptions.get()
372
+ * - This token contains all data needed for validation with Google servers
373
+ * - Can also be used for subscription status checks and cancellation detection
374
+ *
331
375
  * @since 1.0.0
332
376
  * @platform ios Not available (use receipt instead)
333
377
  * @platform android Always present
378
+ * @example
379
+ * ```typescript
380
+ * const transaction = await NativePurchases.purchaseProduct({ ... });
381
+ * if (transaction.purchaseToken) {
382
+ * // Send to your backend for validation
383
+ * await fetch('/api/validate-purchase', {
384
+ * method: 'POST',
385
+ * body: JSON.stringify({
386
+ * purchaseToken: transaction.purchaseToken,
387
+ * productId: transaction.productIdentifier
388
+ * })
389
+ * });
390
+ * }
391
+ * ```
334
392
  */
335
393
  readonly purchaseToken?: string;
336
394
  /**
@@ -589,6 +647,12 @@ export interface SKProductDiscount {
589
647
  export interface Product {
590
648
  /**
591
649
  * Product Id.
650
+ *
651
+ * Android subscriptions note:
652
+ * - `identifier` is the base plan ID (`offerDetails.getBasePlanId()`).
653
+ * - `planIdentifier` is the subscription product ID (`productDetails.getProductId()`).
654
+ *
655
+ * If you group/filter Android subscription results by `identifier`, you are grouping by base plan.
592
656
  */
593
657
  readonly identifier: string;
594
658
  /**
@@ -623,6 +687,18 @@ export interface Product {
623
687
  * Group identifier for the product.
624
688
  */
625
689
  readonly subscriptionGroupIdentifier: string;
690
+ /**
691
+ * Android subscriptions only: Google Play product identifier tied to the offer/base plan set.
692
+ */
693
+ readonly planIdentifier?: string;
694
+ /**
695
+ * Android subscriptions only: offer token required when purchasing specific offers.
696
+ */
697
+ readonly offerToken?: string;
698
+ /**
699
+ * Android subscriptions only: offer identifier (null/undefined for base offers).
700
+ */
701
+ readonly offerId?: string | null;
626
702
  /**
627
703
  * The Product subscription group identifier.
628
704
  */
@@ -893,6 +969,50 @@ export interface NativePurchasesPlugin {
893
969
  acknowledgePurchase(options: {
894
970
  purchaseToken: string;
895
971
  }): Promise<void>;
972
+ /**
973
+ * Consume an in-app purchase on Android.
974
+ *
975
+ * Consuming a purchase does two things:
976
+ * 1. Acknowledges the purchase (so you don't need to call acknowledgePurchase separately)
977
+ * 2. Removes ownership, allowing the user to buy the same product again
978
+ *
979
+ * Use this for consumable products like virtual currency, extra lives, or credits.
980
+ *
981
+ * **Important:** In Google Play Billing Library 8.x, consumed purchases can no longer
982
+ * be queried via getPurchases(). Once consumed, the purchase is gone.
983
+ *
984
+ * Android only — iOS does not have a separate consume concept.
985
+ * On iOS and web, this method rejects with an error.
986
+ *
987
+ * @param options - The purchase to consume
988
+ * @param options.purchaseToken - The purchase token from the Transaction object
989
+ * @returns {Promise<void>} Promise that resolves when the purchase is consumed
990
+ * @throws Error if consumption fails, token is invalid, or called on iOS/web
991
+ * @platform android
992
+ * @since 8.2.0
993
+ *
994
+ * @example
995
+ * ```typescript
996
+ * const transaction = await NativePurchases.purchaseProduct({
997
+ * productIdentifier: 'coins_100',
998
+ * isConsumable: false,
999
+ * autoAcknowledgePurchases: false
1000
+ * });
1001
+ *
1002
+ * // Validate with your backend first
1003
+ * const isValid = await validateWithServer(transaction.purchaseToken);
1004
+ *
1005
+ * if (isValid) {
1006
+ * // Grant the coins, then consume to allow re-purchase
1007
+ * await NativePurchases.consumePurchase({
1008
+ * purchaseToken: transaction.purchaseToken!
1009
+ * });
1010
+ * }
1011
+ * ```
1012
+ */
1013
+ consumePurchase(options: {
1014
+ purchaseToken: string;
1015
+ }): Promise<void>;
896
1016
  /**
897
1017
  * Listen for StoreKit transaction updates delivered by Apple's Transaction.updates.
898
1018
  * Fires on app launch if there are unfinished transactions, and for any updates afterward.