@capgo/native-purchases 7.18.0-alpha.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.18.0-alpha.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
@@ -506,9 +527,8 @@ public class NativePurchasesPlugin extends Plugin {
506
527
  }
507
528
  productDetailsParamsList.add(productDetailsParams.build());
508
529
  }
509
- BillingFlowParams.Builder billingFlowBuilder = BillingFlowParams.newBuilder().setProductDetailsParamsList(
510
- productDetailsParamsList
511
- );
530
+ BillingFlowParams.Builder billingFlowBuilder = BillingFlowParams.newBuilder()
531
+ .setProductDetailsParamsList(productDetailsParamsList);
512
532
  if (accountIdentifier != null && !accountIdentifier.isEmpty()) {
513
533
  billingFlowBuilder.setObfuscatedAccountId(accountIdentifier);
514
534
  }
@@ -666,44 +686,67 @@ public class NativePurchasesPlugin extends Plugin {
666
686
  if (productType.equals("inapp")) {
667
687
  Log.d(TAG, "Processing as in-app product");
668
688
  product.put("identifier", productDetails.getProductId());
669
- double price =
670
- 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;
671
698
  product.put("price", price);
672
- product.put("priceString", productDetails.getOneTimePurchaseOfferDetails().getFormattedPrice());
673
- product.put("currencyCode", productDetails.getOneTimePurchaseOfferDetails().getPriceCurrencyCode());
699
+ product.put("priceString", oneTimeOfferDetails.getFormattedPrice());
700
+ product.put("currencyCode", oneTimeOfferDetails.getPriceCurrencyCode());
674
701
  Log.d(TAG, "Price: " + price);
675
- Log.d(TAG, "Formatted price: " + productDetails.getOneTimePurchaseOfferDetails().getFormattedPrice());
676
- Log.d(TAG, "Currency: " + productDetails.getOneTimePurchaseOfferDetails().getPriceCurrencyCode());
702
+ Log.d(TAG, "Formatted price: " + oneTimeOfferDetails.getFormattedPrice());
703
+ Log.d(TAG, "Currency: " + oneTimeOfferDetails.getPriceCurrencyCode());
677
704
  } else {
678
705
  Log.d(TAG, "Processing as subscription product");
679
- ProductDetails.SubscriptionOfferDetails selectedOfferDetails = productDetails
680
- .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()
681
735
  .get(0);
682
736
  product.put("planIdentifier", productDetails.getProductId());
683
737
  product.put("identifier", selectedOfferDetails.getBasePlanId());
684
- double price =
685
- 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;
686
741
  product.put("price", price);
687
- product.put(
688
- "priceString",
689
- selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice()
690
- );
691
- product.put(
692
- "currencyCode",
693
- selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getPriceCurrencyCode()
694
- );
742
+ product.put("priceString", firstPricingPhase.getFormattedPrice());
743
+ product.put("currencyCode", firstPricingPhase.getPriceCurrencyCode());
695
744
  Log.d(TAG, "Plan identifier: " + productDetails.getProductId());
696
745
  Log.d(TAG, "Base plan ID: " + selectedOfferDetails.getBasePlanId());
746
+ Log.d(TAG, "Offer token: " + selectedOfferDetails.getOfferToken());
697
747
  Log.d(TAG, "Price: " + price);
698
- Log.d(
699
- TAG,
700
- "Formatted price: " +
701
- selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice()
702
- );
703
- Log.d(
704
- TAG,
705
- "Currency: " + selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getPriceCurrencyCode()
706
- );
748
+ Log.d(TAG, "Formatted price: " + firstPricingPhase.getFormattedPrice());
749
+ Log.d(TAG, "Currency: " + firstPricingPhase.getPriceCurrencyCode());
707
750
  }
708
751
  product.put("isFamilyShareable", false);
709
752
 
@@ -778,58 +821,86 @@ public class NativePurchasesPlugin extends Plugin {
778
821
  JSONArray products = new JSONArray();
779
822
  for (ProductDetails productDetails : productDetailsList) {
780
823
  Log.d(TAG, "Processing product details: " + productDetails.getProductId());
781
- JSObject product = new JSObject();
782
- product.put("title", productDetails.getName());
783
- product.put("description", productDetails.getDescription());
784
824
  Log.d(TAG, "Product title: " + productDetails.getName());
785
825
  Log.d(TAG, "Product description: " + productDetails.getDescription());
786
826
 
787
827
  if (productType.equals("inapp")) {
788
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());
789
832
  product.put("identifier", productDetails.getProductId());
790
- double price =
791
- Objects.requireNonNull(productDetails.getOneTimePurchaseOfferDetails()).getPriceAmountMicros() /
792
- 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;
793
842
  product.put("price", price);
794
- product.put("priceString", productDetails.getOneTimePurchaseOfferDetails().getFormattedPrice());
795
- product.put("currencyCode", productDetails.getOneTimePurchaseOfferDetails().getPriceCurrencyCode());
843
+ product.put("priceString", oneTimeOfferDetails.getFormattedPrice());
844
+ product.put("currencyCode", oneTimeOfferDetails.getPriceCurrencyCode());
845
+ product.put("isFamilyShareable", false);
796
846
  Log.d(TAG, "Price: " + price);
797
- Log.d(TAG, "Formatted price: " + productDetails.getOneTimePurchaseOfferDetails().getFormattedPrice());
798
- 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);
799
850
  } else {
800
851
  Log.d(TAG, "Processing as subscription product");
801
- ProductDetails.SubscriptionOfferDetails selectedOfferDetails = productDetails
802
- .getSubscriptionOfferDetails()
803
- .get(0);
804
- product.put("planIdentifier", productDetails.getProductId());
805
- product.put("identifier", selectedOfferDetails.getBasePlanId());
806
- double price =
807
- selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getPriceAmountMicros() / 1000000.0;
808
- product.put("price", price);
809
- product.put(
810
- "priceString",
811
- selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice()
812
- );
813
- product.put(
814
- "currencyCode",
815
- selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getPriceCurrencyCode()
816
- );
817
- Log.d(TAG, "Plan identifier: " + productDetails.getProductId());
818
- Log.d(TAG, "Base plan ID: " + selectedOfferDetails.getBasePlanId());
819
- Log.d(TAG, "Price: " + price);
820
- Log.d(
821
- TAG,
822
- "Formatted price: " +
823
- selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice()
824
- );
825
- Log.d(
826
- TAG,
827
- "Currency: " +
828
- selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getPriceCurrencyCode()
829
- );
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
+ }
830
903
  }
831
- product.put("isFamilyShareable", false);
832
- products.put(product);
833
904
  }
834
905
  JSObject ret = new JSObject();
835
906
  ret.put("products", products);
package/dist/docs.json CHANGED
@@ -664,9 +664,13 @@
664
664
  {
665
665
  "text": "android Not available (use purchaseToken instead)",
666
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"
667
671
  }
668
672
  ],
669
- "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.",
670
674
  "complexTypes": [],
671
675
  "type": "string | undefined"
672
676
  },
@@ -684,9 +688,13 @@
684
688
  {
685
689
  "text": "android Not available",
686
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"
687
695
  }
688
696
  ],
689
- "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",
690
698
  "complexTypes": [],
691
699
  "type": "string | undefined"
692
700
  },
@@ -963,9 +971,13 @@
963
971
  {
964
972
  "text": "android Always present",
965
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"
966
978
  }
967
979
  ],
968
- "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",
969
981
  "complexTypes": [],
970
982
  "type": "string | undefined"
971
983
  },
@@ -1165,7 +1177,7 @@
1165
1177
  {
1166
1178
  "name": "identifier",
1167
1179
  "tags": [],
1168
- "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.",
1169
1181
  "complexTypes": [],
1170
1182
  "type": "string"
1171
1183
  },
@@ -1225,6 +1237,27 @@
1225
1237
  "complexTypes": [],
1226
1238
  "type": "string"
1227
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
+ },
1228
1261
  {
1229
1262
  "name": "subscriptionPeriod",
1230
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
  */