Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,6 +19,6 @@ allprojects {
}
}

task clean(type: Delete) {
tasks.register('clean', Delete) {
delete rootProject.buildDir
}
7 changes: 4 additions & 3 deletions godot-google-play-billing/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -20,6 +20,7 @@ android {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
//buildToolsVersion '35.0.0'

libraryVariants.all { variant ->
variant.outputs.all { output ->
Expand All @@ -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'])
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String, SkuDetails> skuDetailsCache = new HashMap<>(); // sku → SkuDetails
private final HashMap<String, ProductDetails> skuDetailsCache = new HashMap<>(); // sku → SkuDetails
private boolean calledStartConnection;
private String obfuscatedAccountId;
private String obfuscatedProfileId;
Expand All @@ -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();
Expand All @@ -86,11 +94,13 @@ public GodotGooglePlayBilling(Godot godot) {
obfuscatedProfileId = "";
}

@UsedByGodot
public void startConnection() {
calledStartConnection = true;
billingClient.startConnection(this);
}

@UsedByGodot
public void endConnection() {
billingClient.endConnection();
}
Expand All @@ -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<Purchase> purchaseList) {
public void onQueryPurchasesResponse(@NonNull BillingResult billingResult,
@NonNull List<Purchase> purchaseList) {
Dictionary returnValue = new Dictionary();
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
returnValue.put("status", 0); // OK = 0
Expand All @@ -124,25 +136,23 @@ public void onQueryPurchasesResponse(BillingResult billingResult,
}
@UsedByGodot
public void querySkuDetails(final String[] list, String type) {
List<String> skuList = Arrays.asList(list);

SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder()
.setSkusList(skuList)
.setType(type);
List<String> skuList = Arrays.asList(list);

billingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() {
billingClient.queryProductDetailsAsync(createQueryParams(skuList,type), new ProductDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult,
List<SkuDetails> 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<ProductDetails> 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
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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<ProductDetailsParams> 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);
}
Expand All @@ -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) {
Expand Down Expand Up @@ -290,11 +336,11 @@ public String getPluginName() {
return "GodotGooglePlayBilling";
}

@NonNull
@Override
public List<String> getPluginMethods() {
return Arrays.asList("startConnection", "endConnection", "confirmPriceChange", "purchase", "updateSubscription", "querySkuDetails", "isReady", "getConnectionState", "queryPurchases", "acknowledgePurchase", "consumePurchase", "setObfuscatedAccountId", "setObfuscatedProfileId");
}
// @NonNull
// @Override
// public List<String> getPluginMethods() {
// return Arrays.asList("startConnection", "endConnection", "confirmPriceChange", "purchase", "updateSubscription", "querySkuDetails", "isReady", "getConnectionState", "queryPurchases", "acknowledgePurchase", "consumePurchase", "setObfuscatedAccountId", "setObfuscatedProfileId");
// }

@NonNull
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}

Expand All @@ -91,7 +93,7 @@ public static Object[] convertPurchaseListToDictionaryObjectArray(List<Purchase>
return purchaseDictionaries;
}

public static Object[] convertSkuDetailsListToDictionaryObjectArray(List<SkuDetails> skuDetails) {
public static Object[] convertSkuDetailsListToDictionaryObjectArray(List<ProductDetails> skuDetails) {
Object[] skuDetailsDictionaries = new Object[skuDetails.size()];

for (int i = 0; i < skuDetails.size(); i++) {
Expand All @@ -100,4 +102,36 @@ public static Object[] convertSkuDetailsListToDictionaryObjectArray(List<SkuDeta

return skuDetailsDictionaries;
}

// public static List<QueryProductDetailsParams.Product> convertSkuDetailsStringToProductList(List<String> 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<String> skuList,String type) {
List<QueryProductDetailsParams.Product> 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();
}
}
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
@@ -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"