diff --git a/extension-iap/manifests/android/AndroidManifest.xml b/extension-iap/manifests/android/AndroidManifest.xml
index c6b0a3c..158ba0a 100644
--- a/extension-iap/manifests/android/AndroidManifest.xml
+++ b/extension-iap/manifests/android/AndroidManifest.xml
@@ -3,7 +3,7 @@
package="{{android.package}}">
-
+
-
-
diff --git a/extension-iap/manifests/android/build.gradle b/extension-iap/manifests/android/build.gradle
new file mode 100644
index 0000000..8ad2663
--- /dev/null
+++ b/extension-iap/manifests/android/build.gradle
@@ -0,0 +1,3 @@
+dependencies {
+ implementation 'com.android.billingclient:billing:3.0.0'
+}
diff --git a/extension-iap/src/iap_android.cpp b/extension-iap/src/iap_android.cpp
index fa6f5a1..ccd4a27 100644
--- a/extension-iap/src/iap_android.cpp
+++ b/extension-iap/src/iap_android.cpp
@@ -52,7 +52,12 @@ static IAP g_IAP;
static int IAP_ProcessPendingTransactions(lua_State* L)
{
- //todo handle pending transactions if there is such thing on Android
+ IAP* iap = &g_IAP;
+
+ JNIEnv* env = Attach();
+ env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_ProcessPendingConsumables, g_IAP.m_IAPJNI);
+ Detach();
+
return 0;
}
@@ -468,4 +473,3 @@ static dmExtension::Result FinalizeIAP(dmExtension::Params* params)
DM_DECLARE_EXTENSION(IAPExt, "IAP", 0, 0, InitializeIAP, UpdateIAP, 0, FinalizeIAP)
#endif //DM_PLATFORM_ANDROID
-
diff --git a/extension-iap/src/java/com/android/vending/billing/IInAppBillingService.java b/extension-iap/src/java/com/android/vending/billing/IInAppBillingService.java
deleted file mode 100644
index 0d728e5..0000000
--- a/extension-iap/src/java/com/android/vending/billing/IInAppBillingService.java
+++ /dev/null
@@ -1,501 +0,0 @@
-/*
- * This file is auto-generated. DO NOT MODIFY.
- * Original file: /Users/chmu/android_workspace/TestBilling/src/com/android/vending/billing/IInAppBillingService.aidl
- */
-package com.android.vending.billing;
-/**
- * InAppBillingService is the service that provides in-app billing version 3 and beyond.
- * This service provides the following features:
- * 1. Provides a new API to get details of in-app items published for the app including
- * price, type, title and description.
- * 2. The purchase flow is synchronous and purchase information is available immediately
- * after it completes.
- * 3. Purchase information of in-app purchases is maintained within the Google Play system
- * till the purchase is consumed.
- * 4. An API to consume a purchase of an inapp item. All purchases of one-time
- * in-app items are consumable and thereafter can be purchased again.
- * 5. An API to get current purchases of the user immediately. This will not contain any
- * consumed purchases.
- *
- * All calls will give a response code with the following possible values
- * RESULT_OK = 0 - success
- * RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog
- * RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested
- * RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase
- * RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API
- * RESULT_ERROR = 6 - Fatal error during the API action
- * RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
- * RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
- */
-public interface IInAppBillingService extends android.os.IInterface
-{
-/** Local-side IPC implementation stub class. */
-public static abstract class Stub extends android.os.Binder implements com.android.vending.billing.IInAppBillingService
-{
-private static final java.lang.String DESCRIPTOR = "com.android.vending.billing.IInAppBillingService";
-/** Construct the stub at attach it to the interface. */
-public Stub()
-{
-this.attachInterface(this, DESCRIPTOR);
-}
-/**
- * Cast an IBinder object into an com.android.vending.billing.IInAppBillingService interface,
- * generating a proxy if needed.
- */
-public static com.android.vending.billing.IInAppBillingService asInterface(android.os.IBinder obj)
-{
-if ((obj==null)) {
-return null;
-}
-android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
-if (((iin!=null)&&(iin instanceof com.android.vending.billing.IInAppBillingService))) {
-return ((com.android.vending.billing.IInAppBillingService)iin);
-}
-return new com.android.vending.billing.IInAppBillingService.Stub.Proxy(obj);
-}
-@Override public android.os.IBinder asBinder()
-{
-return this;
-}
-@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
-{
-switch (code)
-{
-case INTERFACE_TRANSACTION:
-{
-reply.writeString(DESCRIPTOR);
-return true;
-}
-case TRANSACTION_isBillingSupported:
-{
-data.enforceInterface(DESCRIPTOR);
-int _arg0;
-_arg0 = data.readInt();
-java.lang.String _arg1;
-_arg1 = data.readString();
-java.lang.String _arg2;
-_arg2 = data.readString();
-int _result = this.isBillingSupported(_arg0, _arg1, _arg2);
-reply.writeNoException();
-reply.writeInt(_result);
-return true;
-}
-case TRANSACTION_getSkuDetails:
-{
-data.enforceInterface(DESCRIPTOR);
-int _arg0;
-_arg0 = data.readInt();
-java.lang.String _arg1;
-_arg1 = data.readString();
-java.lang.String _arg2;
-_arg2 = data.readString();
-android.os.Bundle _arg3;
-if ((0!=data.readInt())) {
-_arg3 = android.os.Bundle.CREATOR.createFromParcel(data);
-}
-else {
-_arg3 = null;
-}
-android.os.Bundle _result = this.getSkuDetails(_arg0, _arg1, _arg2, _arg3);
-reply.writeNoException();
-if ((_result!=null)) {
-reply.writeInt(1);
-_result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
-}
-else {
-reply.writeInt(0);
-}
-return true;
-}
-case TRANSACTION_getBuyIntent:
-{
-data.enforceInterface(DESCRIPTOR);
-int _arg0;
-_arg0 = data.readInt();
-java.lang.String _arg1;
-_arg1 = data.readString();
-java.lang.String _arg2;
-_arg2 = data.readString();
-java.lang.String _arg3;
-_arg3 = data.readString();
-java.lang.String _arg4;
-_arg4 = data.readString();
-android.os.Bundle _result = this.getBuyIntent(_arg0, _arg1, _arg2, _arg3, _arg4);
-reply.writeNoException();
-if ((_result!=null)) {
-reply.writeInt(1);
-_result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
-}
-else {
-reply.writeInt(0);
-}
-return true;
-}
-case TRANSACTION_getPurchases:
-{
-data.enforceInterface(DESCRIPTOR);
-int _arg0;
-_arg0 = data.readInt();
-java.lang.String _arg1;
-_arg1 = data.readString();
-java.lang.String _arg2;
-_arg2 = data.readString();
-java.lang.String _arg3;
-_arg3 = data.readString();
-android.os.Bundle _result = this.getPurchases(_arg0, _arg1, _arg2, _arg3);
-reply.writeNoException();
-if ((_result!=null)) {
-reply.writeInt(1);
-_result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
-}
-else {
-reply.writeInt(0);
-}
-return true;
-}
-case TRANSACTION_consumePurchase:
-{
-data.enforceInterface(DESCRIPTOR);
-int _arg0;
-_arg0 = data.readInt();
-java.lang.String _arg1;
-_arg1 = data.readString();
-java.lang.String _arg2;
-_arg2 = data.readString();
-int _result = this.consumePurchase(_arg0, _arg1, _arg2);
-reply.writeNoException();
-reply.writeInt(_result);
-return true;
-}
-}
-return super.onTransact(code, data, reply, flags);
-}
-private static class Proxy implements com.android.vending.billing.IInAppBillingService
-{
-private android.os.IBinder mRemote;
-Proxy(android.os.IBinder remote)
-{
-mRemote = remote;
-}
-@Override public android.os.IBinder asBinder()
-{
-return mRemote;
-}
-public java.lang.String getInterfaceDescriptor()
-{
-return DESCRIPTOR;
-}
-/**
- * Checks support for the requested billing API version, package and in-app type.
- * Minimum API version supported by this interface is 3.
- * @param apiVersion the billing version which the app is using
- * @param packageName the package name of the calling app
- * @param type type of the in-app item being purchased "inapp" for one-time purchases
- * and "subs" for subscription.
- * @return RESULT_OK(0) on success, corresponding result code on failures
- */
-@Override public int isBillingSupported(int apiVersion, java.lang.String packageName, java.lang.String type) throws android.os.RemoteException
-{
-android.os.Parcel _data = android.os.Parcel.obtain();
-android.os.Parcel _reply = android.os.Parcel.obtain();
-int _result;
-try {
-_data.writeInterfaceToken(DESCRIPTOR);
-_data.writeInt(apiVersion);
-_data.writeString(packageName);
-_data.writeString(type);
-mRemote.transact(Stub.TRANSACTION_isBillingSupported, _data, _reply, 0);
-_reply.readException();
-_result = _reply.readInt();
-}
-finally {
-_reply.recycle();
-_data.recycle();
-}
-return _result;
-}
-/**
- * Provides details of a list of SKUs
- * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
- * with a list JSON strings containing the productId, price, title and description.
- * This API can be called with a maximum of 20 SKUs.
- * @param apiVersion billing API version that the Third-party is using
- * @param packageName the package name of the calling app
- * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
- * @return Bundle containing the following key-value pairs
- * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
- * failure as listed above.
- * "DETAILS_LIST" with a StringArrayList containing purchase information
- * in JSON format similar to:
- * '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
- * "title : "Example Title", "description" : "This is an example description" }'
- */
-@Override public android.os.Bundle getSkuDetails(int apiVersion, java.lang.String packageName, java.lang.String type, android.os.Bundle skusBundle) throws android.os.RemoteException
-{
-android.os.Parcel _data = android.os.Parcel.obtain();
-android.os.Parcel _reply = android.os.Parcel.obtain();
-android.os.Bundle _result;
-try {
-_data.writeInterfaceToken(DESCRIPTOR);
-_data.writeInt(apiVersion);
-_data.writeString(packageName);
-_data.writeString(type);
-if ((skusBundle!=null)) {
-_data.writeInt(1);
-skusBundle.writeToParcel(_data, 0);
-}
-else {
-_data.writeInt(0);
-}
-mRemote.transact(Stub.TRANSACTION_getSkuDetails, _data, _reply, 0);
-_reply.readException();
-if ((0!=_reply.readInt())) {
-_result = android.os.Bundle.CREATOR.createFromParcel(_reply);
-}
-else {
-_result = null;
-}
-}
-finally {
-_reply.recycle();
-_data.recycle();
-}
-return _result;
-}
-/**
- * Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
- * the type, a unique purchase token and an optional developer payload.
- * @param apiVersion billing API version that the app is using
- * @param packageName package name of the calling app
- * @param sku the SKU of the in-app item as published in the developer console
- * @param type the type of the in-app item ("inapp" for one-time purchases
- * and "subs" for subscription).
- * @param developerPayload optional argument to be sent back with the purchase information
- * @return Bundle containing the following key-value pairs
- * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
- * failure as listed above.
- * "BUY_INTENT" - PendingIntent to start the purchase flow
- *
- * The Pending intent should be launched with startIntentSenderForResult. When purchase flow
- * has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
- * If the purchase is successful, the result data will contain the following key-value pairs
- * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
- * failure as listed above.
- * "INAPP_PURCHASE_DATA" - String in JSON format similar to
- * '{"orderId":"12999763169054705758.1371079406387615",
- * "packageName":"com.example.app",
- * "productId":"exampleSku",
- * "purchaseTime":1345678900000,
- * "purchaseToken" : "122333444455555",
- * "developerPayload":"example developer payload" }'
- * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
- * was signed with the private key of the developer
- * TODO: change this to app-specific keys.
- */
-@Override public android.os.Bundle getBuyIntent(int apiVersion, java.lang.String packageName, java.lang.String sku, java.lang.String type, java.lang.String developerPayload) throws android.os.RemoteException
-{
-android.os.Parcel _data = android.os.Parcel.obtain();
-android.os.Parcel _reply = android.os.Parcel.obtain();
-android.os.Bundle _result;
-try {
-_data.writeInterfaceToken(DESCRIPTOR);
-_data.writeInt(apiVersion);
-_data.writeString(packageName);
-_data.writeString(sku);
-_data.writeString(type);
-_data.writeString(developerPayload);
-mRemote.transact(Stub.TRANSACTION_getBuyIntent, _data, _reply, 0);
-_reply.readException();
-if ((0!=_reply.readInt())) {
-_result = android.os.Bundle.CREATOR.createFromParcel(_reply);
-}
-else {
-_result = null;
-}
-}
-finally {
-_reply.recycle();
-_data.recycle();
-}
-return _result;
-}
-/**
- * Returns the current SKUs owned by the user of the type and package name specified along with
- * purchase information and a signature of the data to be validated.
- * This will return all SKUs that have been purchased in V3 and managed items purchased using
- * V1 and V2 that have not been consumed.
- * @param apiVersion billing API version that the app is using
- * @param packageName package name of the calling app
- * @param type the type of the in-app items being requested
- * ("inapp" for one-time purchases and "subs" for subscription).
- * @param continuationToken to be set as null for the first call, if the number of owned
- * skus are too many, a continuationToken is returned in the response bundle.
- * This method can be called again with the continuation token to get the next set of
- * owned skus.
- * @return Bundle containing the following key-value pairs
- * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
- * failure as listed above.
- * "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
- * "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
- * "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
- * of the purchase information
- * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
- * next set of in-app purchases. Only set if the
- * user has more owned skus than the current list.
- */
-@Override public android.os.Bundle getPurchases(int apiVersion, java.lang.String packageName, java.lang.String type, java.lang.String continuationToken) throws android.os.RemoteException
-{
-android.os.Parcel _data = android.os.Parcel.obtain();
-android.os.Parcel _reply = android.os.Parcel.obtain();
-android.os.Bundle _result;
-try {
-_data.writeInterfaceToken(DESCRIPTOR);
-_data.writeInt(apiVersion);
-_data.writeString(packageName);
-_data.writeString(type);
-_data.writeString(continuationToken);
-mRemote.transact(Stub.TRANSACTION_getPurchases, _data, _reply, 0);
-_reply.readException();
-if ((0!=_reply.readInt())) {
-_result = android.os.Bundle.CREATOR.createFromParcel(_reply);
-}
-else {
-_result = null;
-}
-}
-finally {
-_reply.recycle();
-_data.recycle();
-}
-return _result;
-}
-/**
- * Consume the last purchase of the given SKU. This will result in this item being removed
- * from all subsequent responses to getPurchases() and allow re-purchase of this item.
- * @param apiVersion billing API version that the app is using
- * @param packageName package name of the calling app
- * @param purchaseToken token in the purchase information JSON that identifies the purchase
- * to be consumed
- * @return 0 if consumption succeeded. Appropriate error values for failures.
- */
-@Override public int consumePurchase(int apiVersion, java.lang.String packageName, java.lang.String purchaseToken) throws android.os.RemoteException
-{
-android.os.Parcel _data = android.os.Parcel.obtain();
-android.os.Parcel _reply = android.os.Parcel.obtain();
-int _result;
-try {
-_data.writeInterfaceToken(DESCRIPTOR);
-_data.writeInt(apiVersion);
-_data.writeString(packageName);
-_data.writeString(purchaseToken);
-mRemote.transact(Stub.TRANSACTION_consumePurchase, _data, _reply, 0);
-_reply.readException();
-_result = _reply.readInt();
-}
-finally {
-_reply.recycle();
-_data.recycle();
-}
-return _result;
-}
-}
-static final int TRANSACTION_isBillingSupported = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
-static final int TRANSACTION_getSkuDetails = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
-static final int TRANSACTION_getBuyIntent = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
-static final int TRANSACTION_getPurchases = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
-static final int TRANSACTION_consumePurchase = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4);
-}
-/**
- * Checks support for the requested billing API version, package and in-app type.
- * Minimum API version supported by this interface is 3.
- * @param apiVersion the billing version which the app is using
- * @param packageName the package name of the calling app
- * @param type type of the in-app item being purchased "inapp" for one-time purchases
- * and "subs" for subscription.
- * @return RESULT_OK(0) on success, corresponding result code on failures
- */
-public int isBillingSupported(int apiVersion, java.lang.String packageName, java.lang.String type) throws android.os.RemoteException;
-/**
- * Provides details of a list of SKUs
- * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
- * with a list JSON strings containing the productId, price, title and description.
- * This API can be called with a maximum of 20 SKUs.
- * @param apiVersion billing API version that the Third-party is using
- * @param packageName the package name of the calling app
- * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
- * @return Bundle containing the following key-value pairs
- * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
- * failure as listed above.
- * "DETAILS_LIST" with a StringArrayList containing purchase information
- * in JSON format similar to:
- * '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
- * "title : "Example Title", "description" : "This is an example description" }'
- */
-public android.os.Bundle getSkuDetails(int apiVersion, java.lang.String packageName, java.lang.String type, android.os.Bundle skusBundle) throws android.os.RemoteException;
-/**
- * Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
- * the type, a unique purchase token and an optional developer payload.
- * @param apiVersion billing API version that the app is using
- * @param packageName package name of the calling app
- * @param sku the SKU of the in-app item as published in the developer console
- * @param type the type of the in-app item ("inapp" for one-time purchases
- * and "subs" for subscription).
- * @param developerPayload optional argument to be sent back with the purchase information
- * @return Bundle containing the following key-value pairs
- * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
- * failure as listed above.
- * "BUY_INTENT" - PendingIntent to start the purchase flow
- *
- * The Pending intent should be launched with startIntentSenderForResult. When purchase flow
- * has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
- * If the purchase is successful, the result data will contain the following key-value pairs
- * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
- * failure as listed above.
- * "INAPP_PURCHASE_DATA" - String in JSON format similar to
- * '{"orderId":"12999763169054705758.1371079406387615",
- * "packageName":"com.example.app",
- * "productId":"exampleSku",
- * "purchaseTime":1345678900000,
- * "purchaseToken" : "122333444455555",
- * "developerPayload":"example developer payload" }'
- * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
- * was signed with the private key of the developer
- * TODO: change this to app-specific keys.
- */
-public android.os.Bundle getBuyIntent(int apiVersion, java.lang.String packageName, java.lang.String sku, java.lang.String type, java.lang.String developerPayload) throws android.os.RemoteException;
-/**
- * Returns the current SKUs owned by the user of the type and package name specified along with
- * purchase information and a signature of the data to be validated.
- * This will return all SKUs that have been purchased in V3 and managed items purchased using
- * V1 and V2 that have not been consumed.
- * @param apiVersion billing API version that the app is using
- * @param packageName package name of the calling app
- * @param type the type of the in-app items being requested
- * ("inapp" for one-time purchases and "subs" for subscription).
- * @param continuationToken to be set as null for the first call, if the number of owned
- * skus are too many, a continuationToken is returned in the response bundle.
- * This method can be called again with the continuation token to get the next set of
- * owned skus.
- * @return Bundle containing the following key-value pairs
- * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
- * failure as listed above.
- * "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
- * "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
- * "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
- * of the purchase information
- * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
- * next set of in-app purchases. Only set if the
- * user has more owned skus than the current list.
- */
-public android.os.Bundle getPurchases(int apiVersion, java.lang.String packageName, java.lang.String type, java.lang.String continuationToken) throws android.os.RemoteException;
-/**
- * Consume the last purchase of the given SKU. This will result in this item being removed
- * from all subsequent responses to getPurchases() and allow re-purchase of this item.
- * @param apiVersion billing API version that the app is using
- * @param packageName package name of the calling app
- * @param purchaseToken token in the purchase information JSON that identifies the purchase
- * to be consumed
- * @return 0 if consumption succeeded. Appropriate error values for failures.
- */
-public int consumePurchase(int apiVersion, java.lang.String packageName, java.lang.String purchaseToken) throws android.os.RemoteException;
-}
diff --git a/extension-iap/src/java/com/defold/iap/IapGooglePlay.java b/extension-iap/src/java/com/defold/iap/IapGooglePlay.java
index fce6853..67f6be9 100644
--- a/extension-iap/src/java/com/defold/iap/IapGooglePlay.java
+++ b/extension-iap/src/java/com/defold/iap/IapGooglePlay.java
@@ -5,507 +5,366 @@ import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
+import java.util.HashMap;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import org.json.JSONException;
import org.json.JSONObject;
+import org.json.JSONArray;
import android.app.Activity;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.content.pm.ResolveInfo;
-import android.content.pm.PackageManager;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.Message;
-import android.os.Messenger;
-import android.os.RemoteException;
import android.util.Log;
-import com.android.vending.billing.IInAppBillingService;
+import com.android.billingclient.api.BillingClient;
+import com.android.billingclient.api.BillingClient.BillingResponseCode;
+import com.android.billingclient.api.BillingClient.SkuType;
+import com.android.billingclient.api.BillingResult;
+import com.android.billingclient.api.Purchase;
+import com.android.billingclient.api.Purchase.PurchasesResult;
+import com.android.billingclient.api.Purchase.PurchaseState;
+import com.android.billingclient.api.SkuDetails;
+import com.android.billingclient.api.ConsumeParams;
+import com.android.billingclient.api.BillingFlowParams;
+import com.android.billingclient.api.SkuDetailsParams;
+import com.android.billingclient.api.PurchasesUpdatedListener;
+import com.android.billingclient.api.BillingClientStateListener;
+import com.android.billingclient.api.ConsumeResponseListener;
+import com.android.billingclient.api.SkuDetailsResponseListener;
-public class IapGooglePlay implements Handler.Callback {
- public static final String PARAM_PRODUCT = "product";
- public static final String PARAM_PRODUCT_TYPE = "product_type";
- public static final String PARAM_PURCHASE_DATA = "purchase_data";
- public static final String PARAM_AUTOFINISH_TRANSACTIONS = "auto_finish_transactions";
- public static final String PARAM_MESSENGER = "com.defold.iap.messenger";
- public static final String RESPONSE_CODE = "RESPONSE_CODE";
- public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST";
- public static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
- public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA";
- public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE";
- public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST";
- public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
- public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
- public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
+public class IapGooglePlay implements PurchasesUpdatedListener {
+ public static final String TAG = "IapGooglePlay";
- public static enum Action {
- BUY,
- RESTORE,
- PROCESS_PENDING_CONSUMABLES,
- FINISH_TRANSACTION
- }
+ private BillingClient billingClient;
- public static final String TAG = "iap";
-
- private Activity activity;
- private Handler handler;
- private Messenger messenger;
- private ServiceConnection serviceConn;
- private IInAppBillingService service;
-
- private SkuDetailsThread skuDetailsThread;
- private BlockingQueue skuRequestQueue = new ArrayBlockingQueue(16);
+ private Map products;
private IPurchaseListener purchaseListener;
- private boolean initialized;
private boolean autoFinishTransactions;
-
- private static interface ISkuRequestListener {
- public void onProducts(int resultCode, JSONObject products);
- }
-
- private static class SkuRequest {
- private ArrayList skuList;
- private ISkuRequestListener listener;
-
- public SkuRequest(ArrayList skuList, ISkuRequestListener listener) {
- this.skuList = skuList;
- this.listener = listener;
- }
- }
-
- private class SkuDetailsThread extends Thread {
- public boolean stop = false;
-
- private void addProductsFromBundle(Bundle skuDetails, JSONObject products) throws JSONException {
- int response = skuDetails.getInt("RESPONSE_CODE");
- if (response == IapJNI.BILLING_RESPONSE_RESULT_OK) {
- ArrayList responseList = skuDetails.getStringArrayList("DETAILS_LIST");
-
- for (String r : responseList) {
- JSONObject product = new JSONObject(r);
- products.put(product.getString("productId"), product);
- }
- }
- else {
- Log.e(TAG, "Failed to fetch product list: " + response);
- }
- }
-
- @Override
- public void run() {
- while (!stop) {
- try {
- SkuRequest sr = skuRequestQueue.take();
- if (service == null) {
- Log.wtf(TAG, "service is null");
- sr.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
- continue;
- }
- if (activity == null) {
- Log.wtf(TAG, "activity is null");
- sr.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
- continue;
- }
-
- String packageName = activity.getPackageName();
- if (packageName == null)
- {
- Log.wtf(TAG, "activity packageName is null");
- sr.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
- continue;
- }
-
- try {
- Bundle querySkus = new Bundle();
- querySkus.putStringArrayList("ITEM_ID_LIST", sr.skuList);
-
- JSONObject products = new JSONObject();
-
- Bundle inappSkuDetails = service.getSkuDetails(3, packageName, "inapp", querySkus);
- addProductsFromBundle(inappSkuDetails, products);
-
- Bundle subscriptionSkuDetails = service.getSkuDetails(3, packageName, "subs", querySkus);
- addProductsFromBundle(subscriptionSkuDetails, products);
-
- sr.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_OK, products);
-
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to fetch product list", e);
- sr.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
- } catch (JSONException e) {
- Log.e(TAG, "Failed to fetch product list", e);
- sr.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
- }
- } catch (InterruptedException e) {
- continue;
- }
- }
- }
- }
+ private Activity activity;
public IapGooglePlay(Activity activity, boolean autoFinishTransactions) {
this.activity = activity;
this.autoFinishTransactions = autoFinishTransactions;
- }
-
- private static boolean isPlayStoreInstalled(Context context){
- try {
- context.getPackageManager().getPackageInfo("com.android.vending", 0);
- return true;
- } catch (PackageManager.NameNotFoundException e) {
- return false;
- }
- }
-
- private void init() {
- // NOTE: We must create Handler lazily as construction of
- // handlers must be in the context of a "looper" on Android
-
- if (!isPlayStoreInstalled(activity)) {
- Log.e(TAG, "Unable to find Google Play Store (com.android.vending)");
- return;
- }
-
- if (this.initialized)
- return;
-
- this.initialized = true;
- this.handler = new Handler(this);
- this.messenger = new Messenger(this.handler);
-
- serviceConn = new ServiceConnection() {
+ products = new HashMap();
+ billingClient = BillingClient.newBuilder(activity).setListener(this).enablePendingPurchases().build();
+ billingClient.startConnection(new BillingClientStateListener() {
@Override
- public void onServiceDisconnected(ComponentName name) {
- Log.v(TAG, "IAP disconnected");
- service = null;
- }
-
- @Override
- public void onServiceConnected(ComponentName name, IBinder binderService) {
- Log.v(TAG, "IAP connected");
- service = IInAppBillingService.Stub.asInterface(binderService);
- skuDetailsThread = new SkuDetailsThread();
- skuDetailsThread.start();
- }
- };
-
- Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
- // Limit intent to vending package
- serviceIntent.setPackage("com.android.vending");
- List intentServices = activity.getPackageManager().queryIntentServices(serviceIntent, 0);
- if (intentServices != null && !intentServices.isEmpty()) {
- // service available to handle that Intent
- activity.bindService(serviceIntent, serviceConn, Context.BIND_AUTO_CREATE);
- } else {
- serviceConn = null;
- Log.e(TAG, "Billing service unavailable on device.");
- }
- }
-
- public void stop() {
- this.activity.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- if (serviceConn != null) {
- activity.unbindService(serviceConn);
- serviceConn = null;
- }
- if (skuDetailsThread != null) {
- skuDetailsThread.stop = true;
- skuDetailsThread.interrupt();
- try {
- skuDetailsThread.join();
- } catch (InterruptedException e) {
- Log.wtf(TAG, "Failed to join thread", e);
- }
- }
- }
- });
- }
-
-
- private void queueSkuRequest(final SkuRequest request) {
- this.activity.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- init();
-
- if (serviceConn != null) {
- try {
- skuRequestQueue.put(request);
- } catch (InterruptedException e) {
- Log.wtf(TAG, "Failed to add sku request", e);
- request.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE, null);
- }
- } else {
- request.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE, null);
- }
- }
- });
- }
-
- public void listItems(final String skus, final IListProductsListener listener, final long commandPtr) {
- ArrayList skuList = new ArrayList();
- for (String x : skus.split(",")) {
- if (x.trim().length() > 0) {
- skuList.add(x);
- }
- }
-
- queueSkuRequest(new SkuRequest(skuList, new ISkuRequestListener() {
- @Override
- public void onProducts(int resultCode, JSONObject products) {
- if (products != null && products.length() > 0) {
- try {
- // go through all of the products and convert them into
- // the generic product format used for all IAP implementations
- Iterator keys = products.keys();
- while(keys.hasNext()) {
- String key = keys.next();
- if (products.get(key) instanceof JSONObject ) {
- JSONObject product = products.getJSONObject(key);
- products.put(key, convertProduct(product));
- }
- }
- listener.onProductsResult(resultCode, products.toString(), commandPtr);
- }
- catch(JSONException e) {
- Log.wtf(TAG, "Failed to convert products", e);
- listener.onProductsResult(resultCode, null, commandPtr);
- }
+ public void onBillingSetupFinished(BillingResult billingResult) {
+ if (billingResult.getResponseCode() == BillingResponseCode.OK) {
+ Log.v(TAG, "Setup finished");
+ // TODO BillingClient.queryPurchases()
}
else {
- listener.onProductsResult(resultCode, null, commandPtr);
+ Log.wtf(TAG, "Setup error: " + billingResult.getDebugMessage());
}
}
- }));
- }
- // Convert the product data into the generic format shared between all Defold IAP implementations
- private static JSONObject convertProduct(JSONObject product) {
- try {
- // Deep copy and modify
- JSONObject p = new JSONObject(product.toString());
- p.put("price_string", p.get("price"));
- p.put("ident", p.get("productId"));
- // It is not yet possible to obtain the price (num) and currency code on Android for the correct locale/region.
- // They have a currency code (price_currency_code), which reflects the merchant's locale, instead of the user's
- // https://code.google.com/p/marketbilling/issues/detail?id=93&q=currency%20code&colspec=ID%20Type%20Status%20Google%20Priority%20Milestone%20Owner%20Summary
- double price = 0.0;
- if (p.has("price_amount_micros")) {
- price = p.getLong("price_amount_micros") * 0.000001;
- }
- String currency_code = "Unknown";
- if (p.has("price_currency_code")) {
- currency_code = (String)p.get("price_currency_code");
- }
- p.put("currency_code", currency_code);
- p.put("price", price);
-
- p.remove("productId");
- p.remove("type");
- p.remove("price_amount_micros");
- p.remove("price_currency_code");
- return p;
- } catch (JSONException e) {
- Log.wtf(TAG, "Failed to convert product json", e);
- }
-
- return null;
- }
-
- private void buyProduct(final String product, final String type, final IPurchaseListener listener) {
- this.activity.runOnUiThread(new Runnable() {
@Override
- public void run() {
- init();
- IapGooglePlay.this.purchaseListener = listener;
- Intent intent = new Intent(activity, IapGooglePlayActivity.class);
- intent.putExtra(PARAM_MESSENGER, messenger);
- intent.putExtra(PARAM_AUTOFINISH_TRANSACTIONS, IapGooglePlay.this.autoFinishTransactions);
- intent.putExtra(PARAM_PRODUCT, product);
- intent.putExtra(PARAM_PRODUCT_TYPE, type);
- intent.setAction(Action.BUY.toString());
- activity.startActivity(intent);
+ public void onBillingServiceDisconnected() {
+ Log.v(TAG, "Service disconnected");
}
});
}
-
- public void buy(final String product, final IPurchaseListener listener) {
- ArrayList skuList = new ArrayList();
- skuList.add(product);
- queueSkuRequest(new SkuRequest(skuList, new ISkuRequestListener() {
- @Override
- public void onProducts(int resultCode, JSONObject products) {
- String type = "inapp";
- if (resultCode == IapJNI.BILLING_RESPONSE_RESULT_OK && products != null) {
- try {
- JSONObject productData = products.getJSONObject(product);
- type = productData.getString("type");
- }
- catch(JSONException e) {
- Log.wtf(TAG, "Failed to get product type before buying, assuming type 'inapp'", e);
- }
- }
- else {
- Log.wtf(TAG, "Failed to list product before buying, assuming type 'inapp'");
- }
- buyProduct(product, type, listener);
- }
- }));
- }
-
- public void finishTransaction(final String receipt, final IPurchaseListener listener) {
- if(IapGooglePlay.this.autoFinishTransactions) {
- return;
- }
- this.activity.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- init();
- IapGooglePlay.this.purchaseListener = listener;
- Intent intent = new Intent(activity, IapGooglePlayActivity.class);
- intent.putExtra(PARAM_MESSENGER, messenger);
- intent.putExtra(PARAM_AUTOFINISH_TRANSACTIONS, false);
- intent.putExtra(PARAM_PURCHASE_DATA, receipt);
- intent.setAction(Action.FINISH_TRANSACTION.toString());
- activity.startActivity(intent);
- }
- });
- }
-
- public void processPendingConsumables(final IPurchaseListener listener) {
- this.activity.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- init();
- IapGooglePlay.this.purchaseListener = listener;
- Intent intent = new Intent(activity, IapGooglePlayActivity.class);
- intent.putExtra(PARAM_MESSENGER, messenger);
- intent.putExtra(PARAM_AUTOFINISH_TRANSACTIONS, IapGooglePlay.this.autoFinishTransactions);
- intent.setAction(Action.PROCESS_PENDING_CONSUMABLES.toString());
- activity.startActivity(intent);
- }
- });
- }
-
- public void restore(final IPurchaseListener listener) {
- this.activity.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- init();
- IapGooglePlay.this.purchaseListener = listener;
- Intent intent = new Intent(activity, IapGooglePlayActivity.class);
- intent.putExtra(PARAM_MESSENGER, messenger);
- intent.putExtra(PARAM_AUTOFINISH_TRANSACTIONS, IapGooglePlay.this.autoFinishTransactions);
- intent.setAction(Action.RESTORE.toString());
- activity.startActivity(intent);
- }
- });
- }
-
- public static String toISO8601(final Date date) {
+ public String toISO8601(final Date date) {
String formatted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(date);
return formatted.substring(0, 22) + ":" + formatted.substring(22);
}
- private static String convertPurchase(String purchase, String signature) {
+ private String convertPurchase(Purchase purchase) {
+ // original JSON:
+ // {
+ // "orderId":"GPA.3301-1670-7033-37542",
+ // "packageName":"com.defold.extension.iap",
+ // "productId":"com.defold.iap.goldbar.medium",
+ // "purchaseTime":1595455967875,
+ // "purchaseState":0,
+ // "purchaseToken":"kacckamkehbbammphdcnhbme.AO-J1OxznnK6E8ILqaAgrPa-3sfaHny424R1e_ZJ2LkaJVsy-5aEOmHizw0vUp-017m8OUvw1rSvfAHbOog1fIvDGJmjaze3MEVFOh1ayJsNFfPDUGwMA_u_9rlV7OqX_nnIyDShH2KE5WrnMC0yQyw7sg5hfgeW6A",
+ // "acknowledged":false
+ // }
+ Log.d(TAG, "convertPurchase() original json: " + purchase.getOriginalJson());
+ JSONObject p = new JSONObject();
try {
- JSONObject p = new JSONObject(purchase);
- p.put("ident", p.get("productId"));
- p.put("state", IapJNI.TRANS_STATE_PURCHASED);
-
- // We check if orderId is actually set here, otherwise we return a blank string.
- // This is what Google used to do, but after some updates around June/May 2016
- // they stopped to include the orderId key at all for test purchases. See: DEF-1940
- if (p.has("orderId")) {
- p.put("trans_ident", p.get("orderId"));
- } else {
- p.put("trans_ident", "");
- }
-
- p.put("date", toISO8601(new Date(p.getLong("purchaseTime"))));
- // Receipt is the complete json data
- // http://robertomurray.co.uk/blog/2013/server-side-google-play-in-app-billing-receipt-validation-and-testing/
- p.put("receipt", purchase);
- p.put("signature", signature);
- // TODO: How to simulate original_trans on iOS?
-
- p.remove("packageName");
- p.remove("orderId");
- p.remove("productId");
- p.remove("developerPayload");
- p.remove("purchaseTime");
- p.remove("purchaseState");
- p.remove("purchaseToken");
-
- return p.toString();
-
- } catch (JSONException e) {
- Log.wtf(TAG, "Failed to convert purchase json", e);
+ JSONObject original = new JSONObject(purchase.getOriginalJson());
+ p.put("ident", original.get("productId"));
+ p.put("state", purchaseStateToDefoldState(purchase.getPurchaseState()));
+ p.put("trans_ident", purchase.getOrderId());
+ p.put("date", toISO8601(new Date(purchase.getPurchaseTime())));
+ p.put("receipt", purchase.getPurchaseToken());
+ p.put("signature", purchase.getSignature());
}
+ catch (JSONException e) {
+ Log.wtf(TAG, "Failed to convert purchase", e);
+ }
+ return p.toString();
+ }
- return null;
+ private JSONObject convertSkuDetails(SkuDetails skuDetails) {
+ JSONObject p = new JSONObject();
+ try {
+ p.put("price_string", skuDetails.getPrice());
+ p.put("ident", skuDetails.getSku());
+ p.put("currency_code", skuDetails.getPriceCurrencyCode());
+ p.put("price", skuDetails.getPriceAmountMicros() * 0.000001);
+ }
+ catch(JSONException e) {
+ Log.wtf(TAG, "Failed to convert sku details", e);
+ }
+ return p;
+ }
+
+ private int purchaseStateToDefoldState(int purchaseState) {
+ int defoldState;
+ switch(purchaseState) {
+ case PurchaseState.PENDING:
+ defoldState = IapJNI.TRANS_STATE_PURCHASING;
+ break;
+ case PurchaseState.PURCHASED:
+ defoldState = IapJNI.TRANS_STATE_PURCHASED;
+ break;
+ default:
+ case PurchaseState.UNSPECIFIED_STATE:
+ defoldState = IapJNI.TRANS_STATE_UNVERIFIED;
+ break;
+ }
+ return defoldState;
+ }
+
+ private int billingResponseCodeToDefoldResponse(int responseCode)
+ {
+ int defoldResponse;
+ switch(responseCode) {
+ case BillingResponseCode.BILLING_UNAVAILABLE:
+ defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE;
+ break;
+ case BillingResponseCode.DEVELOPER_ERROR:
+ defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_DEVELOPER_ERROR;
+ break;
+ case BillingResponseCode.ITEM_ALREADY_OWNED:
+ defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED;
+ break;
+ case BillingResponseCode.ITEM_NOT_OWNED:
+ defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED;
+ break;
+ case BillingResponseCode.ITEM_UNAVAILABLE:
+ defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE;
+ break;
+ case BillingResponseCode.OK:
+ defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_OK;
+ break;
+ case BillingResponseCode.SERVICE_TIMEOUT:
+ case BillingResponseCode.SERVICE_UNAVAILABLE:
+ case BillingResponseCode.SERVICE_DISCONNECTED:
+ defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_SERVICE_UNAVAILABLE;
+ break;
+ case BillingResponseCode.USER_CANCELED:
+ defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_USER_CANCELED;
+ break;
+ case BillingResponseCode.FEATURE_NOT_SUPPORTED:
+ case BillingResponseCode.ERROR:
+ default:
+ defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_ERROR;
+ break;
+ }
+ return defoldResponse;
+ }
+
+ private int billingResultToDefoldResponse(BillingResult result) {
+ return billingResponseCodeToDefoldResponse(result.getResponseCode());
+ }
+
+ // -------------------------------------------
+
+ private List queryPurchases(final String type) {
+ PurchasesResult result = billingClient.queryPurchases(type);
+ if (result.getBillingResult().getResponseCode() != BillingResponseCode.OK) {
+ Log.e(TAG, "Unable to query pending purchases: " + result.getBillingResult().getDebugMessage());
+ return new ArrayList();
+ }
+ return result.getPurchasesList();
+ }
+
+ public void processPendingConsumables(final IPurchaseListener purchaseListener)
+ {
+ Log.d(TAG, "processPendingConsumables()");
+ List purchasesList = new ArrayList();
+ purchasesList.addAll(queryPurchases(SkuType.INAPP));
+ purchasesList.addAll(queryPurchases(SkuType.SUBS));
+ for (Purchase purchase : purchasesList) {
+ handlePurchase(purchase, purchaseListener);
+ }
+ }
+
+ // -------------------------------------------
+
+ private void consumePurchase(final Purchase purchase, final IPurchaseListener purchaseListener)
+ {
+ Log.d(TAG, "consumePurchase()");
+ ConsumeParams consumeParams = ConsumeParams.newBuilder()
+ .setPurchaseToken(purchase.getPurchaseToken())
+ .build();
+
+ billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
+ @Override
+ public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
+ Log.d(TAG, "consumePurchase() onConsumeResponse " + billingResult.getResponseCode() + " purchaseToken: " + purchaseToken);
+ if (billingResult.getResponseCode() != BillingResponseCode.OK) {
+ Log.e(TAG, "Unable to consume purchase: " + billingResult.getDebugMessage());
+ purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
+ return;
+ }
+ }
+ });
+ }
+
+ public void finishTransaction(final String purchaseToken, final IPurchaseListener purchaseListener) {
+ Log.d(TAG, "finishTransaction() " + purchaseToken);
+ List purchasesList = new ArrayList();
+ purchasesList.addAll(queryPurchases(SkuType.INAPP));
+ purchasesList.addAll(queryPurchases(SkuType.SUBS));
+
+ for(Purchase p : purchasesList) {
+ Log.d(TAG, "finishTransaction() purchase: " + p.getOriginalJson());
+ if (p.getPurchaseToken().equals(purchaseToken)) {
+ consumePurchase(p, purchaseListener);
+ return;
+ }
+ }
+ Log.e(TAG, "Unable to find purchase for token: " + purchaseToken);
+ purchaseListener.onPurchaseResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, "");
+ }
+
+ // -------------------------------------------
+
+ private void handlePurchase(final Purchase purchase, final IPurchaseListener purchaseListener) {
+ if (this.autoFinishTransactions) {
+ consumePurchase(purchase, purchaseListener);
+ }
+ else {
+ purchaseListener.onPurchaseResult(billingResponseCodeToDefoldResponse(BillingResponseCode.OK), convertPurchase(purchase));
+ }
}
@Override
- public boolean handleMessage(Message msg) {
- Bundle bundle = msg.getData();
-
- String actionString = bundle.getString("action");
- if (actionString == null) {
- return false;
- }
-
- if (purchaseListener == null) {
- Log.wtf(TAG, "No purchase listener set");
- return false;
- }
-
- Action action = Action.valueOf(actionString);
-
- if (action == Action.BUY) {
- int responseCode = bundle.getInt(RESPONSE_CODE);
- String purchaseData = bundle.getString(RESPONSE_INAPP_PURCHASE_DATA);
- String dataSignature = bundle.getString(RESPONSE_INAPP_SIGNATURE);
-
- if (purchaseData != null && dataSignature != null) {
- purchaseData = convertPurchase(purchaseData, dataSignature);
- } else {
- purchaseData = "";
+ public void onPurchasesUpdated(BillingResult billingResult, List purchases) {
+ if (billingResult.getResponseCode() == BillingResponseCode.OK && purchases != null) {
+ for (Purchase purchase : purchases) {
+ handlePurchase(purchase, this.purchaseListener);
}
+ } else {
+ this.purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
+ }
+ }
- purchaseListener.onPurchaseResult(responseCode, purchaseData);
- } else if (action == Action.RESTORE) {
- Bundle items = bundle.getBundle("items");
+ private void buyProduct(SkuDetails sku, final IPurchaseListener purchaseListener) {
+ this.purchaseListener = purchaseListener;
- if (!items.containsKey(RESPONSE_INAPP_ITEM_LIST)) {
- purchaseListener.onPurchaseResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, "");
- return true;
- }
+ BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
+ .setSkuDetails(sku)
+ .build();
- ArrayList ownedSkus = items.getStringArrayList(RESPONSE_INAPP_ITEM_LIST);
- ArrayList purchaseDataList = items.getStringArrayList(RESPONSE_INAPP_PURCHASE_DATA_LIST);
- ArrayList signatureList = items.getStringArrayList(RESPONSE_INAPP_SIGNATURE_LIST);
- for (int i = 0; i < ownedSkus.size(); ++i) {
- int c = IapJNI.BILLING_RESPONSE_RESULT_OK;
- String pd = convertPurchase(purchaseDataList.get(i), signatureList.get(i));
- if (pd == null) {
- pd = "";
- c = IapJNI.BILLING_RESPONSE_RESULT_ERROR;
+ BillingResult billingResult = billingClient.launchBillingFlow(this.activity, billingFlowParams);
+ if (billingResult.getResponseCode() != BillingResponseCode.OK) {
+ Log.e(TAG, "Purchase failed: " + billingResult.getDebugMessage());
+ purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
+ }
+ }
+
+ public void buy(final String product, final IPurchaseListener purchaseListener) {
+ Log.d(TAG, "buy()");
+
+ SkuDetails sku = this.products.get(product);
+ if (sku != null) {
+ buyProduct(sku, purchaseListener);
+ }
+ else {
+ List skuList = new ArrayList();
+ skuList.add(product);
+ querySkuDetailsAsync(skuList, new SkuDetailsResponseListener() {
+ @Override
+ public void onSkuDetailsResponse(BillingResult billingResult, List skuDetailsList) {
+ if (billingResult.getResponseCode() == BillingResponseCode.OK) {
+ buyProduct(skuDetailsList.get(0), purchaseListener);
+ }
+ else {
+ Log.e(TAG, "Unable to get product details before buying: " + billingResult.getDebugMessage());
+ purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
+ }
}
- purchaseListener.onPurchaseResult(c, pd);
+ });
+ }
+ }
+
+ // -------------------------------------------
+
+ private void querySkuDetailsAsync(final List skuList, final SkuDetailsResponseListener listener)
+ {
+ SkuDetailsResponseListener detailsListener = new SkuDetailsResponseListener() {
+ private List allSkuDetails = new ArrayList();
+ private int queries = 2;
+
+ @Override
+ public void onSkuDetailsResponse(BillingResult billingResult, List skuDetails) {
+ // cache skus (cache will be used to speed up buying)
+ for (SkuDetails sd : skuDetails)
+ {
+ IapGooglePlay.this.products.put(sd.getSku(), sd);
+ }
+ //
+ allSkuDetails.addAll(skuDetails);
+ queries--;
+ if (queries == 0)
+ {
+ listener.onSkuDetailsResponse(billingResult, allSkuDetails);
+ }
+ }
+ };
+ billingClient.querySkuDetailsAsync(SkuDetailsParams.newBuilder().setSkusList(skuList).setType(SkuType.INAPP).build(), detailsListener);
+ billingClient.querySkuDetailsAsync(SkuDetailsParams.newBuilder().setSkusList(skuList).setType(SkuType.SUBS).build(), detailsListener);
+ }
+
+ public void listItems(final String products, final IListProductsListener productsListener, final long commandPtr)
+ {
+ Log.d(TAG, "listItems()");
+
+ // create list of skus from comma separated string
+ List skuList = new ArrayList();
+ for (String p : products.split(",")) {
+ if (p.trim().length() > 0) {
+ skuList.add(p);
}
}
- return true;
+
+ querySkuDetailsAsync(skuList, new SkuDetailsResponseListener() {
+ @Override
+ public void onSkuDetailsResponse(BillingResult billingResult, List skuDetails) {
+ JSONArray a = new JSONArray();
+ if (billingResult.getResponseCode() == BillingResponseCode.OK) {
+ for (SkuDetails sd : skuDetails)
+ {
+ a.put(convertSkuDetails(sd));
+ }
+ }
+ else {
+ Log.e(TAG, "Unable to list products: " + billingResult.getDebugMessage());
+ }
+ productsListener.onProductsResult(billingResultToDefoldResponse(billingResult), a.toString(), commandPtr);
+ }
+ });
+ }
+
+ // -------------------------------------------
+
+ public void stop()
+ {
+ Log.d(TAG, "stop()");
+ }
+
+ public void restore(final IPurchaseListener listener)
+ {
+ Log.d(TAG, "restore()");
}
}
diff --git a/extension-iap/src/java/com/defold/iap/IapGooglePlayActivity.java b/extension-iap/src/java/com/defold/iap/IapGooglePlayActivity.java
deleted file mode 100644
index 54198b0..0000000
--- a/extension-iap/src/java/com/defold/iap/IapGooglePlayActivity.java
+++ /dev/null
@@ -1,371 +0,0 @@
-package com.defold.iap;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.app.Activity;
-import android.app.PendingIntent;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentSender.SendIntentException;
-import android.content.ServiceConnection;
-import android.content.pm.ResolveInfo;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.Message;
-import android.os.Messenger;
-import android.os.RemoteException;
-import android.util.Log;
-import android.view.View;
-import android.view.ViewGroup.LayoutParams;
-
-import com.android.vending.billing.IInAppBillingService;
-import com.defold.iap.IapGooglePlay.Action;
-
-public class IapGooglePlayActivity extends Activity {
-
- private boolean hasPendingPurchases = false;
- private boolean autoFinishTransactions = true;
- private boolean isDone = false;
- private Messenger messenger;
- ServiceConnection serviceConn;
- IInAppBillingService service;
-
- // NOTE: Code from "trivialdrivesample"
- int getResponseCodeFromBundle(Bundle b) {
- Object o = b.get(IapGooglePlay.RESPONSE_CODE);
- if (o == null) {
- Log.d(IapGooglePlay.TAG, "Bundle with null response code, assuming OK (known issue)");
- return IapJNI.BILLING_RESPONSE_RESULT_OK;
- } else if (o instanceof Integer)
- return ((Integer) o).intValue();
- else if (o instanceof Long)
- return (int) ((Long) o).longValue();
- else {
- Log.e(IapGooglePlay.TAG, "Unexpected type for bundle response code.");
- Log.e(IapGooglePlay.TAG, o.getClass().getName());
- throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
- }
- }
-
- private void sendBuyError(int error) {
- Bundle bundle = new Bundle();
- bundle.putString("action", Action.BUY.toString());
-
- bundle.putInt(IapGooglePlay.RESPONSE_CODE, error);
- Message msg = new Message();
- msg.setData(bundle);
-
- try {
- messenger.send(msg);
- } catch (RemoteException e) {
- Log.wtf(IapGooglePlay.TAG, "Unable to send message", e);
- }
- this.finish();
- }
-
- private void buy(String product, String productType) {
- // Flush any pending items, in order to be able to buy the same (new) product again
- processPendingConsumables();
-
- try {
- Bundle buyIntentBundle = service.getBuyIntent(3, getPackageName(), product, productType, "");
- int response = getResponseCodeFromBundle(buyIntentBundle);
- if (response == IapJNI.BILLING_RESPONSE_RESULT_OK) {
- hasPendingPurchases = true;
- PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT");
- startIntentSenderForResult(pendingIntent.getIntentSender(), 1001, new Intent(), Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0));
- } else if (response == IapJNI.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED) {
- sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED);
- } else {
- sendBuyError(response);
- }
-
- } catch (RemoteException e) {
- Log.e(IapGooglePlay.TAG, String.format("Failed to buy", e));
- sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ERROR);
- } catch (SendIntentException e) {
- Log.e(IapGooglePlay.TAG, String.format("Failed to buy", e));
- sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ERROR);
- }
- }
-
- private boolean consume(String purchaseData) {
- try {
- if (purchaseData == null) {
- Log.e(IapGooglePlay.TAG, String.format("Failed to consume purchase, purchaseData was null!"));
- return false;
- }
-
- JSONObject pd = new JSONObject(purchaseData);
- if (!pd.isNull("autoRenewing")) {
- Log.i(IapGooglePlay.TAG, "Will not consume purchase since it is a subscription.");
- return true;
- }
- String token = pd.getString("purchaseToken");
- int consumeResponse = service.consumePurchase(3, getPackageName(), token);
- if (consumeResponse == IapJNI.BILLING_RESPONSE_RESULT_OK) {
- return true;
- } else {
- Log.e(IapGooglePlay.TAG, String.format("Failed to consume purchase (%d)", consumeResponse));
- sendBuyError(consumeResponse);
- }
- } catch (RemoteException e) {
- Log.e(IapGooglePlay.TAG, "Failed to consume purchase", e);
- sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ERROR);
- } catch (JSONException e) {
- Log.e(IapGooglePlay.TAG, "Failed to consume purchase", e);
- sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ERROR);
- }
- return false;
- }
-
- private boolean processPurchase(String purchaseData, String signature)
- {
- if (this.autoFinishTransactions && !consume(purchaseData)) {
- Log.e(IapGooglePlay.TAG, "Failed to consume and send message");
- return false;
- }
-
- Bundle bundle = new Bundle();
- bundle.putString("action", Action.BUY.toString());
- bundle.putInt(IapGooglePlay.RESPONSE_CODE, IapJNI.BILLING_RESPONSE_RESULT_OK);
- bundle.putString(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA, purchaseData);
- bundle.putString(IapGooglePlay.RESPONSE_INAPP_SIGNATURE, signature);
-
- Message msg = new Message();
- msg.setData(bundle);
- try {
- messenger.send(msg);
- return true;
- } catch (RemoteException e) {
- Log.wtf(IapGooglePlay.TAG, "Unable to send message", e);
- return false;
- }
- }
-
- // Make buy response codes for all consumables not yet processed.
- private void processPendingConsumables() {
- try {
- // Note: subscriptions cannot be consumed
- // https://developer.android.com/google/play/billing/api.html#subs
- Bundle items = service.getPurchases(3, getPackageName(), "inapp", null);
- int response = getResponseCodeFromBundle(items);
- if (response == IapJNI.BILLING_RESPONSE_RESULT_OK) {
- ArrayList purchaseDataList = items.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA_LIST);
- ArrayList signatureList = items.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_SIGNATURE_LIST);
- for (int i = 0; i < purchaseDataList.size(); ++i) {
- String purchaseData = purchaseDataList.get(i);
- String signature = signatureList.get(i);
- if (!processPurchase(purchaseData, signature)) {
- // abort and retry some other time
- break;
- }
- }
- }
- } catch (RemoteException e) {
- Log.e(IapGooglePlay.TAG, "Failed to process purchase", e);
- }
- }
-
- private void restore() {
- int response = IapJNI.BILLING_RESPONSE_RESULT_ERROR;
- Bundle bundle = new Bundle();
- bundle.putString("action", Action.RESTORE.toString());
-
- Bundle items = new Bundle();
- try {
- ArrayList purchaseItemList = new ArrayList();
- ArrayList purchaseDataList = new ArrayList();
- ArrayList signatureList = new ArrayList();
-
- Bundle inapp = service.getPurchases(3, getPackageName(), "inapp", null);
- if (getResponseCodeFromBundle(inapp) == IapJNI.BILLING_RESPONSE_RESULT_OK) {
- purchaseItemList.addAll(inapp.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_ITEM_LIST));
- purchaseDataList.addAll(inapp.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA_LIST));
- signatureList.addAll(inapp.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_SIGNATURE_LIST));
- }
-
- Bundle subs = service.getPurchases(3, getPackageName(), "subs", null);
- if (getResponseCodeFromBundle(subs) == IapJNI.BILLING_RESPONSE_RESULT_OK) {
- purchaseItemList.addAll(subs.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_ITEM_LIST));
- purchaseDataList.addAll(subs.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA_LIST));
- signatureList.addAll(subs.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_SIGNATURE_LIST));
- }
-
- items.putStringArrayList(IapGooglePlay.RESPONSE_INAPP_ITEM_LIST, purchaseItemList);
- items.putStringArrayList(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA_LIST, purchaseDataList);
- items.putStringArrayList(IapGooglePlay.RESPONSE_INAPP_SIGNATURE_LIST, signatureList);
- } catch (RemoteException e) {
- Log.e(IapGooglePlay.TAG, "Failed to restore purchases", e);
- }
- bundle.putBundle("items", items);
-
- bundle.putInt(IapGooglePlay.RESPONSE_CODE, response);
- Message msg = new Message();
- msg.setData(bundle);
-
- try {
- messenger.send(msg);
- } catch (RemoteException e) {
- Log.wtf(IapGooglePlay.TAG, "Unable to send message", e);
- }
- this.finish();
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- View view = new View(this);
- view.setBackgroundColor(0x10ffffff);
- setContentView(view, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
-
- Intent intent = getIntent();
- final Bundle extras = intent.getExtras();
- this.messenger = (Messenger) extras.getParcelable(IapGooglePlay.PARAM_MESSENGER);
- final Action action = Action.valueOf(intent.getAction());
- this.autoFinishTransactions = extras.getBoolean(IapGooglePlay.PARAM_AUTOFINISH_TRANSACTIONS);
-
- Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
- serviceIntent.setPackage("com.android.vending");
- List intentServices = getPackageManager().queryIntentServices(serviceIntent, 0);
- if (intentServices != null && !intentServices.isEmpty()) {
- // service available to handle that Intent
- serviceConn = new ServiceConnection() {
- @Override
- public void onServiceDisconnected(ComponentName name) {
- service = null;
- }
-
- @Override
- public void onServiceConnected(ComponentName name, IBinder serviceBinder) {
- service = IInAppBillingService.Stub.asInterface(serviceBinder);
- if (action == Action.BUY) {
- buy(extras.getString(IapGooglePlay.PARAM_PRODUCT), extras.getString(IapGooglePlay.PARAM_PRODUCT_TYPE));
- } else if (action == Action.RESTORE) {
- restore();
- } else if (action == Action.PROCESS_PENDING_CONSUMABLES) {
- processPendingConsumables();
- finish();
- } else if (action == Action.FINISH_TRANSACTION) {
- consume(extras.getString(IapGooglePlay.PARAM_PURCHASE_DATA));
- finish();
- }
- }
- };
-
- bindService(serviceIntent, serviceConn, Context.BIND_AUTO_CREATE);
- } else {
- // Service will never be connected; just send unavailability message
- Bundle bundle = new Bundle();
- bundle.putString("action", intent.getAction());
- bundle.putInt(IapGooglePlay.RESPONSE_CODE, IapJNI.BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE);
- Message msg = new Message();
- msg.setData(bundle);
- try {
- messenger.send(msg);
- } catch (RemoteException e) {
- Log.wtf(IapGooglePlay.TAG, "Unable to send message", e);
- }
- this.finish();
- }
- }
-
- @Override
- public void finish() {
- super.finish();
- this.isDone = true;
- }
-
- @Override
- protected void onDestroy() {
- if (hasPendingPurchases) {
- // Not sure connection is up so need to check here.
- if (service != null) {
- if(autoFinishTransactions) {
- processPendingConsumables();
- }
- }
- hasPendingPurchases = false;
- }
-
- if( !isDone )
- {
- Intent intent = getIntent();
-
- if( intent != null && intent.getComponent().getClassName().equals( getClass().getName() ) )
- {
- Log.v(IapGooglePlay.TAG, "There's still an intent left: " + intent.getAction() );
- sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ERROR);
- }
- }
-
- if (serviceConn != null) {
- try
- {
- unbindService(serviceConn);
- } catch (IllegalArgumentException e) {
- Log.wtf(IapGooglePlay.TAG, "Unable to unbind service", e);
- }
- }
- super.onDestroy();
- }
-
- // NOTE: Code from "trivialdrivesample"
- int getResponseCodeFromIntent(Intent i) {
- Object o = i.getExtras().get(IapGooglePlay.RESPONSE_CODE);
- if (o == null) {
- Log.e(IapGooglePlay.TAG, "Intent with no response code, assuming OK (known issue)");
- return IapJNI.BILLING_RESPONSE_RESULT_OK;
- } else if (o instanceof Integer) {
- return ((Integer) o).intValue();
- } else if (o instanceof Long) {
- return (int) ((Long) o).longValue();
- } else {
- Log.e(IapGooglePlay.TAG, "Unexpected type for intent response code.");
- Log.e(IapGooglePlay.TAG, o.getClass().getName());
- throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName());
- }
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- Bundle bundle = null;
- if (data != null) {
- int responseCode = getResponseCodeFromIntent(data);
- String purchaseData = data.getStringExtra(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA);
- String dataSignature = data.getStringExtra(IapGooglePlay.RESPONSE_INAPP_SIGNATURE);
- if (responseCode == IapJNI.BILLING_RESPONSE_RESULT_OK) {
- processPurchase(purchaseData, dataSignature);
- } else {
- bundle = new Bundle();
- bundle.putString("action", Action.BUY.toString());
- bundle.putInt(IapGooglePlay.RESPONSE_CODE, responseCode);
- bundle.putString(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA, purchaseData);
- bundle.putString(IapGooglePlay.RESPONSE_INAPP_SIGNATURE, dataSignature);
- }
- } else {
- bundle = new Bundle();
- bundle.putString("action", Action.BUY.toString());
- bundle.putInt(IapGooglePlay.RESPONSE_CODE, IapJNI.BILLING_RESPONSE_RESULT_ERROR);
- }
-
- // Send message if generated above
- if (bundle != null) {
- Message msg = new Message();
- msg.setData(bundle);
- try {
- messenger.send(msg);
- } catch (RemoteException e) {
- Log.wtf(IapGooglePlay.TAG, "Unable to send message", e);
- }
- }
-
- this.finish();
- }
-}
diff --git a/main/main.gui b/main/main.gui
index 5775982..3500faa 100644
--- a/main/main.gui
+++ b/main/main.gui
@@ -169,7 +169,7 @@ nodes {
}
nodes {
position {
- x: 485.0
+ x: 530.0
y: 56.0
z: 0.0
w: 1.0
@@ -199,7 +199,7 @@ nodes {
w: 1.0
}
type: TYPE_TEMPLATE
- id: "reset"
+ id: "restore"
layer: ""
inherit_alpha: true
alpha: 1.0
@@ -226,7 +226,7 @@ nodes {
w: 1.0
}
size {
- x: 300.0
+ x: 200.0
y: 88.0
z: 0.0
w: 1.0
@@ -240,12 +240,12 @@ nodes {
type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA
texture: "button/button_normal"
- id: "reset/larrybutton"
+ id: "restore/larrybutton"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
adjust_mode: ADJUST_MODE_FIT
- parent: "reset"
+ parent: "restore"
layer: ""
inherit_alpha: true
slice9 {
@@ -258,6 +258,7 @@ nodes {
clipping_visible: true
clipping_inverted: false
alpha: 1.0
+ overridden_fields: 4
template_node_child: true
size_mode: SIZE_MODE_MANUAL
}
@@ -294,9 +295,9 @@ nodes {
}
type: TYPE_TEXT
blend_mode: BLEND_MODE_ALPHA
- text: "Reset"
+ text: "Restore"
font: "larryfont"
- id: "reset/larrylabel"
+ id: "restore/larrylabel"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
@@ -314,7 +315,7 @@ nodes {
}
adjust_mode: ADJUST_MODE_FIT
line_break: false
- parent: "reset/larrybutton"
+ parent: "restore/larrybutton"
layer: ""
inherit_alpha: true
alpha: 1.0
@@ -705,7 +706,7 @@ nodes {
}
nodes {
position {
- x: 159.0
+ x: 109.0
y: 56.0
z: 0.0
w: 1.0
@@ -762,7 +763,7 @@ nodes {
w: 1.0
}
size {
- x: 300.0
+ x: 200.0
y: 88.0
z: 0.0
w: 1.0
@@ -794,6 +795,7 @@ nodes {
clipping_visible: true
clipping_inverted: false
alpha: 1.0
+ overridden_fields: 4
template_node_child: true
size_mode: SIZE_MODE_MANUAL
}
@@ -1019,6 +1021,165 @@ nodes {
text_leading: 1.0
text_tracking: 0.0
}
+nodes {
+ position {
+ x: 320.0
+ y: 56.0
+ z: 0.0
+ w: 1.0
+ }
+ rotation {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ scale {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ size {
+ x: 200.0
+ y: 100.0
+ z: 0.0
+ w: 1.0
+ }
+ color {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ type: TYPE_TEMPLATE
+ id: "pending"
+ layer: ""
+ inherit_alpha: true
+ alpha: 1.0
+ template: "/dirtylarry/button.gui"
+ template_node_child: false
+}
+nodes {
+ position {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ rotation {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ scale {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ size {
+ x: 200.0
+ y: 88.0
+ z: 0.0
+ w: 1.0
+ }
+ color {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ type: TYPE_BOX
+ blend_mode: BLEND_MODE_ALPHA
+ texture: "button/button_normal"
+ id: "pending/larrybutton"
+ xanchor: XANCHOR_NONE
+ yanchor: YANCHOR_NONE
+ pivot: PIVOT_CENTER
+ adjust_mode: ADJUST_MODE_FIT
+ parent: "pending"
+ layer: ""
+ inherit_alpha: true
+ slice9 {
+ x: 32.0
+ y: 32.0
+ z: 32.0
+ w: 32.0
+ }
+ clipping_mode: CLIPPING_MODE_NONE
+ clipping_visible: true
+ clipping_inverted: false
+ alpha: 1.0
+ overridden_fields: 4
+ template_node_child: true
+ size_mode: SIZE_MODE_MANUAL
+}
+nodes {
+ position {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ rotation {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ scale {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ size {
+ x: 200.0
+ y: 100.0
+ z: 0.0
+ w: 1.0
+ }
+ color {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ type: TYPE_TEXT
+ blend_mode: BLEND_MODE_ALPHA
+ text: "Pending"
+ font: "larryfont"
+ id: "pending/larrylabel"
+ xanchor: XANCHOR_NONE
+ yanchor: YANCHOR_NONE
+ pivot: PIVOT_CENTER
+ outline {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ shadow {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ adjust_mode: ADJUST_MODE_FIT
+ line_break: false
+ parent: "pending/larrybutton"
+ layer: ""
+ inherit_alpha: true
+ alpha: 1.0
+ outline_alpha: 1.0
+ shadow_alpha: 1.0
+ overridden_fields: 8
+ template_node_child: true
+ text_leading: 1.0
+ text_tracking: 0.0
+}
material: "/builtins/materials/gui.material"
adjust_reference: ADJUST_REFERENCE_PARENT
max_nodes: 512
diff --git a/main/main.gui_script b/main/main.gui_script
index 29ff3fd..f5f7bba 100644
--- a/main/main.gui_script
+++ b/main/main.gui_script
@@ -36,11 +36,21 @@ local function log(fmt, ...)
gui.set_text(gui.get_node("log"), s)
end
+local function process_pending_transactions()
+ log("iap.process_pending_transactions()")
+ iap.process_pending_transactions()
+end
+
local function buy(id)
log("iap.buy() " .. id)
iap.buy(id)
end
+local function restore()
+ log("iap.restore()")
+ iap.restore()
+end
+
local function list()
log("iap.list()")
for item, button in pairs(item_buttons) do
@@ -67,12 +77,12 @@ end
local function buy_listener(self, transaction, error)
- pprint(transaction, error)
+ pprint(transaction)
if error then
log("iap.buy() error %s - %s", tostring(error.error), tostring(error.reason))
return
end
-
+
if iap.get_provider_id() == iap.PROVIDER_ID_GOOGLE and transaction.ident == NON_CONSUMABLE then
log("iap.buy() ok - google")
gui.set_color(gui.get_node("reset/larrylabel"), vmath.vector4(1,1,1,1))
@@ -109,5 +119,11 @@ function on_input(self, action_id, action)
dirtylarry:button("list", action_id, action, function()
list()
end)
+ dirtylarry:button("restore", action_id, action, function()
+ restore()
+ end)
+ dirtylarry:button("pending", action_id, action, function()
+ process_pending_transactions()
+ end)
end
-end
\ No newline at end of file
+end