diff --git a/build.gradle b/build.gradle index 299319d..29ed82e 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { mavenCentral() } dependencies { - classpath "com.android.tools.build:gradle:8.2.0" + classpath "com.android.tools.build:gradle:8.12.0" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -19,6 +19,6 @@ allprojects { } } -task clean(type: Delete) { +tasks.register('clean', Delete) { delete rootProject.buildDir } diff --git a/godot-google-play-billing/build.gradle b/godot-google-play-billing/build.gradle index 4fcd45d..2af79d4 100644 --- a/godot-google-play-billing/build.gradle +++ b/godot-google-play-billing/build.gradle @@ -7,11 +7,11 @@ ext.pluginVersionName = "1.3.0" android { namespace 'org.godotengine.godot.plugin.googleplaybilling' - compileSdkVersion 34 + compileSdk 35 defaultConfig { minSdkVersion 21 - targetSdkVersion 34 + targetSdkVersion 35 versionCode pluginVersionCode versionName pluginVersionName } @@ -20,6 +20,7 @@ android { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } + //buildToolsVersion '35.0.0' libraryVariants.all { variant -> variant.outputs.all { output -> @@ -30,6 +31,6 @@ android { dependencies { implementation "androidx.legacy:legacy-support-v4:1.0.0" - implementation 'com.android.billingclient:billing:7.0.0' + implementation 'com.android.billingclient:billing:8.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 da7a5c9..a319bea 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 @@ -30,6 +30,10 @@ package org.godotengine.godot.plugin.googleplaybilling; +import static org.godotengine.godot.plugin.googleplaybilling.utils.GooglePlayBillingUtils.createQueryParams; + +import android.os.Build; + import org.godotengine.godot.Dictionary; import org.godotengine.godot.Godot; import org.godotengine.godot.plugin.GodotPlugin; @@ -50,22 +54,26 @@ import com.android.billingclient.api.ConsumeParams; import com.android.billingclient.api.ConsumeResponseListener; import com.android.billingclient.api.PendingPurchasesParams; +import com.android.billingclient.api.ProductDetails; +import com.android.billingclient.api.ProductDetailsResponseListener; 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; -import com.android.billingclient.api.SkuDetailsResponseListener; +import com.android.billingclient.api.QueryProductDetailsResult; +import com.android.billingclient.api.QueryPurchasesParams; +import com.android.billingclient.api.BillingFlowParams.ProductDetailsParams; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Objects; import java.util.Set; public class GodotGooglePlayBilling extends GodotPlugin implements PurchasesUpdatedListener, BillingClientStateListener { private final BillingClient billingClient; - private final HashMap skuDetailsCache = new HashMap<>(); // sku → SkuDetails + private final HashMap skuDetailsCache = new HashMap<>(); // sku → SkuDetails private boolean calledStartConnection; private String obfuscatedAccountId; private String obfuscatedProfileId; @@ -77,7 +85,7 @@ public GodotGooglePlayBilling(Godot godot) { PendingPurchasesParams.newBuilder().enableOneTimeProducts().build(); billingClient = BillingClient - .newBuilder(getActivity()) + .newBuilder(Objects.requireNonNull(getActivity())) .enablePendingPurchases(pendingPurchasesParams) .setListener(this) .build(); @@ -86,11 +94,13 @@ public GodotGooglePlayBilling(Godot godot) { obfuscatedProfileId = ""; } + @UsedByGodot public void startConnection() { calledStartConnection = true; billingClient.startConnection(this); } + @UsedByGodot public void endConnection() { billingClient.endConnection(); } @@ -105,10 +115,12 @@ public int getConnectionState() { @UsedByGodot public void queryPurchases(String type) { - billingClient.queryPurchasesAsync(type, new PurchasesResponseListener() { + + QueryPurchasesParams qp = QueryPurchasesParams.newBuilder().setProductType(type).build(); + billingClient.queryPurchasesAsync(qp, new PurchasesResponseListener() { @Override - public void onQueryPurchasesResponse(BillingResult billingResult, - List purchaseList) { + public void onQueryPurchasesResponse(@NonNull BillingResult billingResult, + @NonNull List purchaseList) { Dictionary returnValue = new Dictionary(); if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { returnValue.put("status", 0); // OK = 0 @@ -124,25 +136,23 @@ public void onQueryPurchasesResponse(BillingResult billingResult, } @UsedByGodot public void querySkuDetails(final String[] list, String type) { - List skuList = Arrays.asList(list); - SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder() - .setSkusList(skuList) - .setType(type); + List skuList = Arrays.asList(list); - billingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() { + billingClient.queryProductDetailsAsync(createQueryParams(skuList,type), new ProductDetailsResponseListener() { @Override - public void onSkuDetailsResponse(BillingResult billingResult, - List skuDetailsList) { + public void onProductDetailsResponse(@NonNull BillingResult billingResult, @NonNull QueryProductDetailsResult queryProductDetailsResult) { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { - for (SkuDetails skuDetails : skuDetailsList) { - skuDetailsCache.put(skuDetails.getSku(), skuDetails); + List skuDetailsList = queryProductDetailsResult.getProductDetailsList(); + for (ProductDetails skuDetails : skuDetailsList) { + skuDetailsCache.put(skuDetails.getProductId(), skuDetails); } emitSignal("sku_details_query_completed", (Object)GooglePlayBillingUtils.convertSkuDetailsListToDictionaryObjectArray(skuDetailsList)); } else { emitSignal("sku_details_query_error", billingResult.getResponseCode(), billingResult.getDebugMessage(), list); } } + }); } @UsedByGodot @@ -153,7 +163,7 @@ public void acknowledgePurchase(final String purchaseToken) { .build(); billingClient.acknowledgePurchase(acknowledgePurchaseParams, new AcknowledgePurchaseResponseListener() { @Override - public void onAcknowledgePurchaseResponse(BillingResult billingResult) { + public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { emitSignal("purchase_acknowledged", purchaseToken); } else { @@ -171,7 +181,7 @@ public void consumePurchase(String purchaseToken) { billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() { @Override - public void onConsumeResponse(BillingResult billingResult, String purchaseToken) { + public void onConsumeResponse(@NonNull BillingResult billingResult, @NonNull String purchaseToken) { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { emitSignal("purchase_consumed", purchaseToken); } else { @@ -204,7 +214,7 @@ public Dictionary confirmPriceChange(String sku) { return returnValue; } - SkuDetails skuDetails = skuDetailsCache.get(sku); + ProductDetails skuDetails = skuDetailsCache.get(sku); Dictionary returnValue = new Dictionary(); returnValue.put("status", 0); // OK = 0 @@ -228,10 +238,46 @@ private Dictionary purchaseInternal(String oldToken, String sku, int replacement returnValue.put("debug_message", "You must query the sku details and wait for the result before purchasing!"); return returnValue; } + ProductDetails productDetails= skuDetailsCache.get(sku); + String offerToken = ""; + if(productDetails != null) + { + if(productDetails.getProductType().equals(BillingClient.ProductType.INAPP)) + { + offerToken = Objects.requireNonNull(productDetails.getOneTimePurchaseOfferDetails()).getOfferToken(); + } + else if(productDetails.getProductType().equals(BillingClient.ProductType.SUBS)) + { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + if(productDetails.getSubscriptionOfferDetails() != null) + offerToken = productDetails.getSubscriptionOfferDetails().getFirst().getOfferToken(); + } + } + } + + + assert productDetails != null; + assert offerToken != null; + List productDetailsParamsList = + List.of( + ProductDetailsParams.newBuilder() - SkuDetails skuDetails = skuDetailsCache.get(sku); - BillingFlowParams.Builder purchaseParamsBuilder = BillingFlowParams.newBuilder(); - purchaseParamsBuilder.setSkuDetails(skuDetails); + // retrieve a value for "productDetails" by calling queryProductDetailsAsync() + .setProductDetails(productDetails) + // Get the offer token: + // a. For one-time products, call ProductDetails.getOneTimePurchaseOfferDetailsList() + // for a list of offers that are available to the user. + // b. For subscriptions, call ProductDetails.subscriptionOfferDetails() + // for a list of offers that are available to the user. + .setOfferToken(offerToken) + .build() + ); + +// BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder() +// .setProductDetailsParamsList(productDetailsParamsList) +// .build(); + BillingFlowParams.Builder purchaseParamsBuilder = BillingFlowParams.newBuilder(); + purchaseParamsBuilder.setProductDetailsParamsList(productDetailsParamsList); if (!obfuscatedAccountId.isEmpty()) { purchaseParamsBuilder.setObfuscatedAccountId(obfuscatedAccountId); } @@ -246,7 +292,7 @@ private Dictionary purchaseInternal(String oldToken, String sku, int replacement .build(); purchaseParamsBuilder.setSubscriptionUpdateParams(updateParams); } - BillingResult result = billingClient.launchBillingFlow(getActivity(), purchaseParamsBuilder.build()); + BillingResult result = billingClient.launchBillingFlow(Objects.requireNonNull(getActivity()), purchaseParamsBuilder.build()); Dictionary returnValue = new Dictionary(); if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) { @@ -290,11 +336,11 @@ public String getPluginName() { return "GodotGooglePlayBilling"; } - @NonNull - @Override - public List getPluginMethods() { - return Arrays.asList("startConnection", "endConnection", "confirmPriceChange", "purchase", "updateSubscription", "querySkuDetails", "isReady", "getConnectionState", "queryPurchases", "acknowledgePurchase", "consumePurchase", "setObfuscatedAccountId", "setObfuscatedProfileId"); - } +// @NonNull +// @Override +// public List getPluginMethods() { +// return Arrays.asList("startConnection", "endConnection", "confirmPriceChange", "purchase", "updateSubscription", "querySkuDetails", "isReady", "getConnectionState", "queryPurchases", "acknowledgePurchase", "consumePurchase", "setObfuscatedAccountId", "setObfuscatedProfileId"); +// } @NonNull @Override 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 1c1cb28..b68785c 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 @@ -32,11 +32,13 @@ import org.godotengine.godot.Dictionary; +import com.android.billingclient.api.ProductDetails; import com.android.billingclient.api.Purchase; -import com.android.billingclient.api.SkuDetails; +import com.android.billingclient.api.QueryProductDetailsParams; import java.util.ArrayList; import java.util.List; +import java.util.Objects; public class GooglePlayBillingUtils { public static Dictionary convertPurchaseToDictionary(Purchase purchase) { @@ -60,24 +62,24 @@ public static Dictionary convertPurchaseToDictionary(Purchase purchase) { return dictionary; } - public static Dictionary convertSkuDetailsToDictionary(SkuDetails details) { + public static Dictionary convertSkuDetailsToDictionary(ProductDetails details) { Dictionary dictionary = new Dictionary(); - dictionary.put("sku", details.getSku()); + dictionary.put("sku", details.getProductId()); dictionary.put("title", details.getTitle()); dictionary.put("description", details.getDescription()); - dictionary.put("price", details.getPrice()); - dictionary.put("price_currency_code", details.getPriceCurrencyCode()); - dictionary.put("price_amount_micros", details.getPriceAmountMicros()); - dictionary.put("free_trial_period", details.getFreeTrialPeriod()); - dictionary.put("icon_url", details.getIconUrl()); - dictionary.put("introductory_price", details.getIntroductoryPrice()); - dictionary.put("introductory_price_amount_micros", details.getIntroductoryPriceAmountMicros()); - dictionary.put("introductory_price_cycles", details.getIntroductoryPriceCycles()); - dictionary.put("introductory_price_period", details.getIntroductoryPricePeriod()); - dictionary.put("original_price", details.getOriginalPrice()); - dictionary.put("original_price_amount_micros", details.getOriginalPriceAmountMicros()); - dictionary.put("subscription_period", details.getSubscriptionPeriod()); - dictionary.put("type", details.getType()); + dictionary.put("price", Objects.requireNonNull(details.getOneTimePurchaseOfferDetails()).getFormattedPrice()); + dictionary.put("price_currency_code", details.getOneTimePurchaseOfferDetails().getPriceCurrencyCode()); + dictionary.put("price_amount_micros", details.getOneTimePurchaseOfferDetails().getPriceAmountMicros()); +// dictionary.put("free_trial_period", details.getOneTimePurchaseOfferDetails().getDiscountDisplayInfo().); +// dictionary.put("icon_url", details.getIconUrl()); +// dictionary.put("introductory_price", details.getIntroductoryPrice()); +// dictionary.put("introductory_price_amount_micros", details.getIntroductoryPriceAmountMicros()); +// dictionary.put("introductory_price_cycles", details.getIntroductoryPriceCycles()); +// dictionary.put("introductory_price_period", details.getIntroductoryPricePeriod()); +// dictionary.put("original_price", details.getOriginalPrice()); +// dictionary.put("original_price_amount_micros", details.getOriginalPriceAmountMicros()); +// dictionary.put("subscription_period", details.getSubscriptionPeriod()); +// dictionary.put("type", details.getType()); return dictionary; } @@ -91,7 +93,7 @@ public static Object[] convertPurchaseListToDictionaryObjectArray(List return purchaseDictionaries; } - public static Object[] convertSkuDetailsListToDictionaryObjectArray(List skuDetails) { + public static Object[] convertSkuDetailsListToDictionaryObjectArray(List skuDetails) { Object[] skuDetailsDictionaries = new Object[skuDetails.size()]; for (int i = 0; i < skuDetails.size(); i++) { @@ -100,4 +102,36 @@ public static Object[] convertSkuDetailsListToDictionaryObjectArray(List convertSkuDetailsStringToProductList(List skuList){ +// QueryProductDetailsParams queryProductDetailsParams = +// QueryProductDetailsParams.newBuilder() +// .setProductList( +// List.of( +// QueryProductDetailsParams.Product.newBuilder() +// .setProductId("product_id_example") +// .setProductId("") +// .setProductType(BillingClient.ProductType.SUBS) +// .build())) +// .build(); +// return new QueryProductDetailsParams.Product[0]; +// } + public static QueryProductDetailsParams createQueryParams(List skuList,String type) { + List productList = new ArrayList<>(); + + // Add an in-app product + for(String sku : skuList ) + { + productList.add( + QueryProductDetailsParams.Product.newBuilder() + .setProductId(sku) // Replace with your actual in-app product ID + .setProductType(type) + .build() + ); + } + + return QueryProductDetailsParams.newBuilder() + .setProductList(productList) + .build(); + } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9fadb7f..c033438 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Jun 15 13:39:16 CEST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle b/settings.gradle index 4242cad..436ce16 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,17 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + maven { url "https://www.jitpack.io" } + maven { url 'https://maven.google.com' } + } +} + +plugins { + id "com.android.application" version "8.12.0" apply false +} + include ':godot-google-play-billing' rootProject.name = "Godot Google Play Billing" +