From bf09bf29bf5fa973d72ac2743a9ce8205dd9e0fa Mon Sep 17 00:00:00 2001 From: Nate Trost Date: Fri, 15 Oct 2021 16:23:01 -0500 Subject: [PATCH] Play Billing Library plugin 1.1 * Play Billing Library plugin 1.1 patch notes This patch updates the plugin to use the latest 4.0 release of the Play Billing Library. It includes adjustments to account for API changes and adds some missing functionality to the plugin. ** API Changes The Play Billing library has deprecated the `queryPurchases` function and replaced it with `queryPurchasesAsync`. The `queryPurchases` method of the plugin has changed as a result. Instead of directly returning the Dictionary purchases object, it is now sent as a parameter of the `query_purchases_response` signal which fires when `queryPurchasesAsync` completes. ** Feature additions *** Purchase Dictionary The Purchase dictionary contains two new keys: `quantity`: Quantity of purchase item (retrieved with Purchase.getQuantity()) `skus`: Array of skus included in purchase (retireved with Purchase.getSkus()) The existing `sku` key is still present, but will only include the first sku of a multi-sku purchase. *** Obfuscated id parameters Methods to set obfuscated profile and account id information have been added: `setObfuscatedAccountId` `setObfuscatedProfileId` Both take a single string parameter that may not be greater than 64 characters. If set, the obfuscated data will be attached to the purchase parameter data. *** Billing resume The plugin now sends a `billing_resume` signal when the plugin receives a resume event. Best practice is to recheck for purchases on a resume to handle purchases that may have occurred outside the app. *** Connection state A `getConnectionState` method has been added to retrieve the ConnectionState value of the Play Billing Library. *** Update subscriptions An `updateSubscription` method has been added to launch the subscription flow to upgrade or downgrade an existing subscription. The method returns a status dictionary matching the format returned by `purchase. The method takes three parameters: * Purchase Token of previous subscription (String) * Sku (product ID) of the new subscription (String) * Subscription proration mode (Int, see `BillingFlowParams.ProrationMode`) A `confirmPriceChange` method has been added to launch the subscription price change confirmation flow. It takes a single parameter: the Sku (product ID) of the affected subscription. The method returns a status dictionary that matches the format used by `purchase`. The result of the confirmation is sent using the `price_change_acknowledged` signal which returns the `BillingResult.getResponseCode()` result as a parameter. --- GodotGooglePlayBilling.gdap | 4 +- godot-google-play-billing/build.gradle | 12 +- .../GodotGooglePlayBilling.java | 116 +++++++++++++++--- .../utils/GooglePlayBillingUtils.java | 9 +- 4 files changed, 112 insertions(+), 29 deletions(-) diff --git a/GodotGooglePlayBilling.gdap b/GodotGooglePlayBilling.gdap index 03505b5..55cd56d 100644 --- a/GodotGooglePlayBilling.gdap +++ b/GodotGooglePlayBilling.gdap @@ -2,7 +2,7 @@ name="GodotGooglePlayBilling" binary_type="local" -binary="GodotGooglePlayBilling.1.0.1.release.aar" +binary="GodotGooglePlayBilling.1.1.0.release.aar" [dependencies] -remote=["com.android.billingclient:billing:3.0.0"] +remote=["com.android.billingclient:billing:4.0.0"] diff --git a/godot-google-play-billing/build.gradle b/godot-google-play-billing/build.gradle index 551c6ba..6306abb 100644 --- a/godot-google-play-billing/build.gradle +++ b/godot-google-play-billing/build.gradle @@ -2,16 +2,16 @@ plugins { id 'com.android.library' } -ext.pluginVersionCode = 2 -ext.pluginVersionName = "1.0.1" +ext.pluginVersionCode = 3 +ext.pluginVersionName = "1.1.0" android { - compileSdkVersion 29 - buildToolsVersion "29.0.3" + compileSdkVersion 30 + buildToolsVersion "30.0.3" defaultConfig { minSdkVersion 18 - targetSdkVersion 29 + targetSdkVersion 30 versionCode pluginVersionCode versionName pluginVersionName } @@ -25,6 +25,6 @@ android { dependencies { implementation "androidx.legacy:legacy-support-v4:1.0.0" - implementation 'com.android.billingclient:billing:3.0.0' + implementation 'com.android.billingclient:billing:4.0.0' compileOnly fileTree(dir: 'libs', include: ['godot-lib*.aar']) } diff --git a/godot-google-play-billing/src/main/java/org/godotengine/godot/plugin/googleplaybilling/GodotGooglePlayBilling.java b/godot-google-play-billing/src/main/java/org/godotengine/godot/plugin/googleplaybilling/GodotGooglePlayBilling.java index b5c1d12..beb0d14 100644 --- a/godot-google-play-billing/src/main/java/org/godotengine/godot/plugin/googleplaybilling/GodotGooglePlayBilling.java +++ b/godot-google-play-billing/src/main/java/org/godotengine/godot/plugin/googleplaybilling/GodotGooglePlayBilling.java @@ -48,7 +48,10 @@ import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.ConsumeParams; import com.android.billingclient.api.ConsumeResponseListener; +import com.android.billingclient.api.PriceChangeConfirmationListener; +import com.android.billingclient.api.PriceChangeFlowParams; import com.android.billingclient.api.Purchase; +import com.android.billingclient.api.PurchasesResponseListener; import com.android.billingclient.api.PurchasesUpdatedListener; import com.android.billingclient.api.SkuDetails; import com.android.billingclient.api.SkuDetailsParams; @@ -59,10 +62,12 @@ import java.util.List; import java.util.Set; -public class GodotGooglePlayBilling extends GodotPlugin implements PurchasesUpdatedListener, BillingClientStateListener { +public class GodotGooglePlayBilling extends GodotPlugin implements PurchasesUpdatedListener, BillingClientStateListener, PriceChangeConfirmationListener { private final BillingClient billingClient; private final HashMap skuDetailsCache = new HashMap<>(); // sku → SkuDetails + private String obfuscatedAccountId; + private String obfuscatedProfileId; public GodotGooglePlayBilling(Godot godot) { super(godot); @@ -72,6 +77,8 @@ public GodotGooglePlayBilling(Godot godot) { .enablePendingPurchases() .setListener(this) .build(); + obfuscatedAccountId = ""; + obfuscatedProfileId = ""; } public void startConnection() { @@ -86,20 +93,27 @@ public boolean isReady() { return this.billingClient.isReady(); } - public Dictionary queryPurchases(String type) { - Purchase.PurchasesResult result = billingClient.queryPurchases(type); - - Dictionary returnValue = new Dictionary(); - if (result.getBillingResult().getResponseCode() == BillingClient.BillingResponseCode.OK) { - returnValue.put("status", 0); // OK = 0 - returnValue.put("purchases", GooglePlayBillingUtils.convertPurchaseListToDictionaryObjectArray(result.getPurchasesList())); - } else { - returnValue.put("status", 1); // FAILED = 1 - returnValue.put("response_code", result.getBillingResult().getResponseCode()); - returnValue.put("debug_message", result.getBillingResult().getDebugMessage()); - } + public int getConnectionState() { + return billingClient.getConnectionState(); + } - return returnValue; + public void queryPurchases(String type) { + billingClient.queryPurchasesAsync(type, new PurchasesResponseListener() { + @Override + public void onQueryPurchasesResponse(BillingResult billingResult, + List purchaseList) { + Dictionary returnValue = new Dictionary(); + if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { + returnValue.put("status", 0); // OK = 0 + returnValue.put("purchases", GooglePlayBillingUtils.convertPurchaseListToDictionaryObjectArray(purchaseList)); + } else { + returnValue.put("status", 1); // FAILED = 1 + returnValue.put("response_code", billingResult.getResponseCode()); + returnValue.put("debug_message", billingResult.getDebugMessage()); + } + emitSignal("query_purchases_response", (Object)returnValue); + } + }); } public void querySkuDetails(final String[] list, String type) { @@ -173,7 +187,37 @@ public void onBillingServiceDisconnected() { emitSignal("disconnected"); } + public Dictionary confirmPriceChange(String sku) { + if (!skuDetailsCache.containsKey(sku)) { + Dictionary returnValue = new Dictionary(); + returnValue.put("status", 1); // FAILED = 1 + returnValue.put("response_code", null); // Null since there is no ResponseCode to return but to keep the interface (status, response_code, debug_message) + returnValue.put("debug_message", "You must query the sku details and wait for the result before confirming a price change!"); + return returnValue; + } + + SkuDetails skuDetails = skuDetailsCache.get(sku); + + PriceChangeFlowParams priceChangeFlowParams = + PriceChangeFlowParams.newBuilder().setSkuDetails(skuDetails).build(); + + billingClient.launchPriceChangeConfirmationFlow(getActivity(), priceChangeFlowParams, this); + + Dictionary returnValue = new Dictionary(); + returnValue.put("status", 0); // OK = 0 + return returnValue; + } + public Dictionary purchase(String sku) { + return purchaseInternal("", sku, + BillingFlowParams.ProrationMode.UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); + } + + public Dictionary updateSubscription(String oldToken, String sku, int prorationMode) { + return purchaseInternal(oldToken, sku, prorationMode); + } + + private Dictionary purchaseInternal(String oldToken, String sku, int prorationMode) { if (!skuDetailsCache.containsKey(sku)) { Dictionary returnValue = new Dictionary(); returnValue.put("status", 1); // FAILED = 1 @@ -183,11 +227,23 @@ public Dictionary purchase(String sku) { } SkuDetails skuDetails = skuDetailsCache.get(sku); - BillingFlowParams purchaseParams = BillingFlowParams.newBuilder() - .setSkuDetails(skuDetails) - .build(); - - BillingResult result = billingClient.launchBillingFlow(getActivity(), purchaseParams); + BillingFlowParams.Builder purchaseParamsBuilder = BillingFlowParams.newBuilder(); + purchaseParamsBuilder.setSkuDetails(skuDetails); + if (!obfuscatedAccountId.isEmpty()) { + purchaseParamsBuilder.setObfuscatedAccountId(obfuscatedAccountId); + } + if (!obfuscatedProfileId.isEmpty()) { + purchaseParamsBuilder.setObfuscatedProfileId(obfuscatedProfileId); + } + if (!oldToken.isEmpty() && prorationMode != BillingFlowParams.ProrationMode.UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY) { + BillingFlowParams.SubscriptionUpdateParams updateParams = + BillingFlowParams.SubscriptionUpdateParams.newBuilder() + .setOldSkuPurchaseToken(oldToken) + .setReplaceSkusProrationMode(prorationMode) + .build(); + purchaseParamsBuilder.setSubscriptionUpdateParams(updateParams); + } + BillingResult result = billingClient.launchBillingFlow(getActivity(), purchaseParamsBuilder.build()); Dictionary returnValue = new Dictionary(); if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) { @@ -199,6 +255,13 @@ public Dictionary purchase(String sku) { } return returnValue; + } + public void setObfuscatedAccountId(String accountId) { + obfuscatedAccountId = accountId; + } + + public void setObfuscatedProfileId(String profileId) { + obfuscatedProfileId = profileId; } @Override @@ -210,6 +273,16 @@ public void onPurchasesUpdated(final BillingResult billingResult, @Nullable fina } } + @Override + public void onPriceChangeConfirmationResult(BillingResult billingResult) { + emitSignal("price_change_acknowledged", billingResult.getResponseCode()); + } + + @Override + public void onMainResume() { + emitSignal("billing_resume"); + } + @NonNull @Override public String getPluginName() { @@ -219,7 +292,7 @@ public String getPluginName() { @NonNull @Override public List getPluginMethods() { - return Arrays.asList("startConnection", "endConnection", "purchase", "querySkuDetails", "isReady", "queryPurchases", "acknowledgePurchase", "consumePurchase"); + return Arrays.asList("startConnection", "endConnection", "confirmPriceChange", "purchase", "updateSubscription", "querySkuDetails", "isReady", "getConnectionState", "queryPurchases", "acknowledgePurchase", "consumePurchase"); } @NonNull @@ -229,11 +302,14 @@ public Set getPluginSignals() { signals.add(new SignalInfo("connected")); signals.add(new SignalInfo("disconnected")); + signals.add(new SignalInfo("billing_resume")); signals.add(new SignalInfo("connect_error", Integer.class, String.class)); signals.add(new SignalInfo("purchases_updated", Object[].class)); + signals.add(new SignalInfo("query_purchases_response", Object.class)); signals.add(new SignalInfo("purchase_error", Integer.class, String.class)); signals.add(new SignalInfo("sku_details_query_completed", Object[].class)); signals.add(new SignalInfo("sku_details_query_error", Integer.class, String.class, String[].class)); + signals.add(new SignalInfo("price_change_acknowledged", Integer.class)); signals.add(new SignalInfo("purchase_acknowledged", String.class)); signals.add(new SignalInfo("purchase_acknowledgement_error", Integer.class, String.class, String.class)); signals.add(new SignalInfo("purchase_consumed", String.class)); diff --git a/godot-google-play-billing/src/main/java/org/godotengine/godot/plugin/googleplaybilling/utils/GooglePlayBillingUtils.java b/godot-google-play-billing/src/main/java/org/godotengine/godot/plugin/googleplaybilling/utils/GooglePlayBillingUtils.java index 69fa0ba..724b110 100644 --- a/godot-google-play-billing/src/main/java/org/godotengine/godot/plugin/googleplaybilling/utils/GooglePlayBillingUtils.java +++ b/godot-google-play-billing/src/main/java/org/godotengine/godot/plugin/googleplaybilling/utils/GooglePlayBillingUtils.java @@ -35,6 +35,7 @@ import com.android.billingclient.api.Purchase; import com.android.billingclient.api.SkuDetails; +import java.util.ArrayList; import java.util.List; public class GooglePlayBillingUtils { @@ -45,8 +46,14 @@ public static Dictionary convertPurchaseToDictionary(Purchase purchase) { dictionary.put("purchase_state", purchase.getPurchaseState()); dictionary.put("purchase_time", purchase.getPurchaseTime()); dictionary.put("purchase_token", purchase.getPurchaseToken()); + dictionary.put("quantity", purchase.getQuantity()); dictionary.put("signature", purchase.getSignature()); - dictionary.put("sku", purchase.getSku()); + // PBL V4 replaced getSku with getSkus to support multi-sku purchases, + // use the first entry for "sku" and generate an array for "skus" + ArrayList skus = purchase.getSkus(); + dictionary.put("sku", skus.get(0)); + String[] skusArray = skus.toArray(new String[0]); + dictionary.put("skus", skusArray); dictionary.put("is_acknowledged", purchase.isAcknowledged()); dictionary.put("is_auto_renewing", purchase.isAutoRenewing()); return dictionary;