@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.
- package/README.md +293 -48
- package/android/build.gradle +2 -2
- package/android/src/main/java/ee/forgr/nativepurchases/NativePurchasesPlugin.java +148 -77
- package/dist/docs.json +37 -4
- package/dist/esm/definitions.d.ts +76 -0
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/NativePurchasesPlugin/NativePurchasesPlugin.swift +204 -304
- package/ios/Sources/NativePurchasesPlugin/Product+CapacitorPurchasesPlugin.swift +0 -1
- package/ios/Sources/NativePurchasesPlugin/TransactionHelpers.swift +85 -129
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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()
|
|
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
|
-
|
|
670
|
-
|
|
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",
|
|
673
|
-
product.put("currencyCode",
|
|
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: " +
|
|
676
|
-
Log.d(TAG, "Currency: " +
|
|
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
|
|
680
|
-
|
|
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
|
-
|
|
685
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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",
|
|
795
|
-
product.put("currencyCode",
|
|
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: " +
|
|
798
|
-
Log.d(TAG, "Currency: " +
|
|
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
|
|
802
|
-
.getSubscriptionOfferDetails()
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
"
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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
|
*/
|