@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.
- package/README.md +323 -48
- package/android/build.gradle +2 -2
- package/android/src/main/java/ee/forgr/nativepurchases/NativePurchasesPlugin.java +190 -74
- package/dist/docs.json +82 -4
- package/dist/esm/definitions.d.ts +120 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +3 -0
- package/dist/esm/web.js +3 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +3 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +3 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/NativePurchasesPlugin/NativePurchasesPlugin.swift +209 -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.
|
|
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
|
|
@@ -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
|
-
|
|
669
|
-
|
|
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",
|
|
672
|
-
product.put("currencyCode",
|
|
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: " +
|
|
675
|
-
Log.d(TAG, "Currency: " +
|
|
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
|
|
679
|
-
|
|
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
|
-
|
|
684
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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",
|
|
794
|
-
product.put("currencyCode",
|
|
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: " +
|
|
797
|
-
Log.d(TAG, "Currency: " +
|
|
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
|
|
801
|
-
.getSubscriptionOfferDetails()
|
|
802
|
-
|
|
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
|
-
|
|
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.
|