From 05e900140403a6921438c2e5d3a4c1737f48c547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ritzl?= Date: Wed, 7 Sep 2022 11:58:19 +0200 Subject: [PATCH] Fixed issues with subscriptions --- extension-iap/src/iap_android.cpp | 44 +++++++---- extension-iap/src/iap_private.h | 2 +- .../src/java/com/defold/iap/IapAmazon.java | 2 +- .../java/com/defold/iap/IapGooglePlay.java | 78 ++++++++++++++----- main/main.gui_script | 3 +- 5 files changed, 90 insertions(+), 39 deletions(-) diff --git a/extension-iap/src/iap_android.cpp b/extension-iap/src/iap_android.cpp index 32c0b38..caac8af 100644 --- a/extension-iap/src/iap_android.cpp +++ b/extension-iap/src/iap_android.cpp @@ -40,6 +40,8 @@ static IAP g_IAP; static int IAP_ProcessPendingTransactions(lua_State* L) { + DM_LUA_STACK_CHECK(L, 0); + dmAndroid::ThreadAttacher threadAttacher; JNIEnv* env = threadAttacher.GetEnv(); env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_ProcessPendingConsumables, g_IAP.m_IAPJNI); @@ -49,11 +51,11 @@ static int IAP_ProcessPendingTransactions(lua_State* L) static int IAP_List(lua_State* L) { - int top = lua_gettop(L); + DM_LUA_STACK_CHECK(L, 0); + char* buf = IAP_List_CreateBuffer(L); if( buf == 0 ) { - assert(top == lua_gettop(L)); return 0; } @@ -68,36 +70,46 @@ static int IAP_List(lua_State* L) env->DeleteLocalRef(products); free(buf); - assert(top == lua_gettop(L)); return 0; } static int IAP_Buy(lua_State* L) { - int top = lua_gettop(L); + DM_LUA_STACK_CHECK(L, 0); + int top = lua_gettop(L); const char* id = luaL_checkstring(L, 1); + const char* token = ""; + + if (top >= 2 && lua_istable(L, 2)) { + luaL_checktype(L, 2, LUA_TTABLE); + lua_pushvalue(L, 2); + lua_getfield(L, -1, "token"); + token = lua_isnil(L, -1) ? "" : luaL_checkstring(L, -1); + lua_pop(L, 2); + } dmAndroid::ThreadAttacher threadAttacher; JNIEnv* env = threadAttacher.GetEnv(); jstring ids = env->NewStringUTF(id); - env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_Buy, ids, g_IAP.m_IAPJNI); + jstring tokens = env->NewStringUTF(token); + env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_Buy, ids, tokens, g_IAP.m_IAPJNI); env->DeleteLocalRef(ids); + env->DeleteLocalRef(tokens); - assert(top == lua_gettop(L)); return 0; } static int IAP_Finish(lua_State* L) { + DM_LUA_STACK_CHECK(L, 0); + if(g_IAP.m_autoFinishTransactions) { dmLogWarning("Calling iap.finish when autofinish transactions is enabled. Ignored."); return 0; } - int top = lua_gettop(L); - luaL_checktype(L, 1, LUA_TTABLE); lua_getfield(L, -1, "state"); @@ -107,7 +119,6 @@ static int IAP_Finish(lua_State* L) { dmLogError("Invalid transaction state (must be iap.TRANS_STATE_PURCHASED)."); lua_pop(L, 1); - assert(top == lua_gettop(L)); return 0; } } @@ -130,13 +141,12 @@ static int IAP_Finish(lua_State* L) env->DeleteLocalRef(receiptUTF); } - assert(top == lua_gettop(L)); return 0; } static int IAP_Acknowledge(lua_State* L) { - int top = lua_gettop(L); + DM_LUA_STACK_CHECK(L, 0); luaL_checktype(L, 1, LUA_TTABLE); @@ -147,7 +157,6 @@ static int IAP_Acknowledge(lua_State* L) { dmLogError("Invalid transaction state (must be iap.TRANS_STATE_PURCHASED)."); lua_pop(L, 1); - assert(top == lua_gettop(L)); return 0; } } @@ -170,7 +179,6 @@ static int IAP_Acknowledge(lua_State* L) env->DeleteLocalRef(receiptUTF); } - assert(top == lua_gettop(L)); return 0; } @@ -178,20 +186,20 @@ static int IAP_Restore(lua_State* L) { // TODO: Missing callback here for completion/error // See iap_ios.mm + DM_LUA_STACK_CHECK(L, 1); - int top = lua_gettop(L); dmAndroid::ThreadAttacher threadAttacher; JNIEnv* env = threadAttacher.GetEnv(); env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_Restore, g_IAP.m_IAPJNI); - assert(top == lua_gettop(L)); - lua_pushboolean(L, 1); return 1; } static int IAP_SetListener(lua_State* L) { + DM_LUA_STACK_CHECK(L, 0); + IAP* iap = &g_IAP; bool had_previous = iap->m_Listener != 0; @@ -212,6 +220,8 @@ static int IAP_SetListener(lua_State* L) static int IAP_GetProviderId(lua_State* L) { + DM_LUA_STACK_CHECK(L, 1); + lua_pushinteger(L, g_IAP.m_ProviderId); return 1; } @@ -409,7 +419,7 @@ static dmExtension::Result InitializeIAP(dmExtension::Params* params) jclass iap_jni_class = dmAndroid::LoadClass(env, "com.defold.iap.IapJNI"); g_IAP.m_List = env->GetMethodID(iap_class, "listItems", "(Ljava/lang/String;Lcom/defold/iap/IListProductsListener;J)V"); - g_IAP.m_Buy = env->GetMethodID(iap_class, "buy", "(Ljava/lang/String;Lcom/defold/iap/IPurchaseListener;)V"); + g_IAP.m_Buy = env->GetMethodID(iap_class, "buy", "(Ljava/lang/String;Ljava/lang/String;Lcom/defold/iap/IPurchaseListener;)V"); g_IAP.m_Restore = env->GetMethodID(iap_class, "restore", "(Lcom/defold/iap/IPurchaseListener;)V"); g_IAP.m_Stop = env->GetMethodID(iap_class, "stop", "()V"); g_IAP.m_ProcessPendingConsumables = env->GetMethodID(iap_class, "processPendingConsumables", "(Lcom/defold/iap/IPurchaseListener;)V"); diff --git a/extension-iap/src/iap_private.h b/extension-iap/src/iap_private.h index e1c287e..eb8708a 100644 --- a/extension-iap/src/iap_private.h +++ b/extension-iap/src/iap_private.h @@ -21,7 +21,7 @@ struct DM_ALIGNED(16) IAPCommand // Used for storing eventual callback info (if needed) dmScript::LuaCallbackInfo* m_Callback; - // THe actual command payload + // The actual command payload int32_t m_Command; int32_t m_ResponseCode; void* m_Data; diff --git a/extension-iap/src/java/com/defold/iap/IapAmazon.java b/extension-iap/src/java/com/defold/iap/IapAmazon.java index 3b1da11..d975e35 100644 --- a/extension-iap/src/java/com/defold/iap/IapAmazon.java +++ b/extension-iap/src/java/com/defold/iap/IapAmazon.java @@ -81,7 +81,7 @@ public class IapAmazon implements PurchasingListener { } } - public void buy(final String product, final IPurchaseListener listener) { + public void buy(final String product, final String token, final IPurchaseListener listener) { synchronized (purchaseListeners) { RequestId req = PurchasingService.purchase(product); if (req != null) { diff --git a/extension-iap/src/java/com/defold/iap/IapGooglePlay.java b/extension-iap/src/java/com/defold/iap/IapGooglePlay.java index 8fdf335..c25871c 100644 --- a/extension-iap/src/java/com/defold/iap/IapGooglePlay.java +++ b/extension-iap/src/java/com/defold/iap/IapGooglePlay.java @@ -25,6 +25,8 @@ import com.android.billingclient.api.Purchase; import com.android.billingclient.api.Purchase.PurchaseState; import com.android.billingclient.api.ProductDetails; import com.android.billingclient.api.ProductDetails.OneTimePurchaseOfferDetails; +import com.android.billingclient.api.ProductDetails.PricingPhases; +import com.android.billingclient.api.ProductDetails.PricingPhase; import com.android.billingclient.api.ProductDetails.SubscriptionOfferDetails; import com.android.billingclient.api.ConsumeParams; import com.android.billingclient.api.BillingFlowParams; @@ -115,20 +117,50 @@ public class IapGooglePlay implements PurchasesUpdatedListener { return p.toString(); } + private JSONArray convertSubscriptionOfferPricingPhases(SubscriptionOfferDetails details) { + JSONArray a = new JSONArray(); + try { + List pricingPhases = details.getPricingPhases().getPricingPhaseList(); + for (PricingPhase pricingPhase : pricingPhases) { + JSONObject o = new JSONObject(); + o.put("price_string", pricingPhase.getFormattedPrice()); + o.put("price", pricingPhase.getPriceAmountMicros() * 0.000001); + o.put("currency_code", pricingPhase.getPriceCurrencyCode()); + o.put("billing_period", pricingPhase.getBillingPeriod()); + o.put("billing_cycle_count", pricingPhase.getBillingCycleCount()); + o.put("recurrence_mode", pricingPhase.getRecurrenceMode()); + a.put(o); + } + } + catch(JSONException e) { + Log.wtf(TAG, "Failed to convert subscription offer pricing phases", e); + } + return a; + } + private JSONObject convertProductDetails(ProductDetails productDetails) { JSONObject p = new JSONObject(); try { p.put("ident", productDetails.getProductId()); - - if (productDetails.getProductType() == ProductType.INAPP) { + if (productDetails.getProductType().equals(ProductType.INAPP)) { OneTimePurchaseOfferDetails offerDetails = productDetails.getOneTimePurchaseOfferDetails(); p.put("price_string", offerDetails.getFormattedPrice()); p.put("currency_code", offerDetails.getPriceCurrencyCode()); p.put("price", offerDetails.getPriceAmountMicros() * 0.000001); } - else if (productDetails.getProductType() == ProductType.INAPP) { - List subscriptionDetails = productDetails.getSubscriptionOfferDetails(); - + else if (productDetails.getProductType().equals(ProductType.SUBS)) { + List subscriptionOfferDetails = productDetails.getSubscriptionOfferDetails(); + JSONArray a = new JSONArray(); + for (SubscriptionOfferDetails offerDetails : subscriptionOfferDetails) { + JSONObject o = new JSONObject(); + o.put("token", offerDetails.getOfferToken()); + o.put("pricing", convertSubscriptionOfferPricingPhases(offerDetails)); + a.put(o); + } + p.put("subscriptions", a); + } + else { + Log.i(TAG, "convertProductDetails() unknown product type " + productDetails.getProductType()); } } catch(JSONException e) { @@ -229,8 +261,10 @@ public class IapGooglePlay implements PurchasesUpdatedListener { } }; - billingClient.queryPurchasesAsync(QueryPurchasesParams.newBuilder().setProductType(ProductType.INAPP).build(), purchasesListener); - billingClient.queryPurchasesAsync(QueryPurchasesParams.newBuilder().setProductType(ProductType.SUBS).build(), purchasesListener); + final QueryPurchasesParams inappParams = QueryPurchasesParams.newBuilder().setProductType(ProductType.INAPP).build(); + final QueryPurchasesParams subsParams = QueryPurchasesParams.newBuilder().setProductType(ProductType.SUBS).build(); + billingClient.queryPurchasesAsync(inappParams, purchasesListener); + billingClient.queryPurchasesAsync(subsParams, purchasesListener); } /** @@ -239,7 +273,7 @@ public class IapGooglePlay implements PurchasesUpdatedListener { */ private void consumePurchase(final String purchaseToken, final ConsumeResponseListener consumeListener) { Log.d(TAG, "consumePurchase() " + purchaseToken); - ConsumeParams consumeParams = ConsumeParams.newBuilder() + final ConsumeParams consumeParams = ConsumeParams.newBuilder() .setPurchaseToken(purchaseToken) .build(); @@ -291,7 +325,7 @@ public class IapGooglePlay implements PurchasesUpdatedListener { * Handle a purchase. If the extension is configured to automatically * finish transactions the purchase will be immediately consumed. Otherwise * the product will be returned via the listener without being consumed. - * NOTE: Billing 3.0 requires purchases to be acknowledged within 3 days of + * NOTE: Billing 3.0+ requires purchases to be acknowledged within 3 days of * purchase unless they are consumed. */ private void handlePurchase(final Purchase purchase, final IPurchaseListener purchaseListener) { @@ -328,15 +362,18 @@ public class IapGooglePlay implements PurchasesUpdatedListener { * Buy a product. This method stores the listener and uses it in the * onPurchasesUpdated() callback. */ - private void buyProduct(ProductDetails pd, final IPurchaseListener purchaseListener) { + private void buyProduct(ProductDetails pd, final String token, final IPurchaseListener purchaseListener) { this.purchaseListener = purchaseListener; List productDetailsParams = new ArrayList(); - productDetailsParams.add(ProductDetailsParams.newBuilder().setProductDetails(pd).build()); + if (pd.getProductType().equals(ProductType.SUBS)) { + productDetailsParams.add(ProductDetailsParams.newBuilder().setProductDetails(pd).setOfferToken(token).build()); + } + else { + productDetailsParams.add(ProductDetailsParams.newBuilder().setProductDetails(pd).build()); + } - BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder() - .setProductDetailsParamsList(productDetailsParams) - .build(); + final BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder().setProductDetailsParamsList(productDetailsParams).build(); BillingResult billingResult = billingClient.launchBillingFlow(this.activity, billingFlowParams); if (billingResult.getResponseCode() != BillingResponseCode.OK) { @@ -348,12 +385,12 @@ public class IapGooglePlay implements PurchasesUpdatedListener { /** * Called from Lua. */ - public void buy(final String product, final IPurchaseListener purchaseListener) { + public void buy(final String product, final String token, final IPurchaseListener purchaseListener) { Log.d(TAG, "buy()"); ProductDetails pd = this.products.get(product); if (pd != null) { - buyProduct(pd, purchaseListener); + buyProduct(pd, token, purchaseListener); } else { List productList = new ArrayList(); @@ -362,7 +399,7 @@ public class IapGooglePlay implements PurchasesUpdatedListener { @Override public void onProductDetailsResponse(BillingResult billingResult, List productDetailsList) { if (billingResult.getResponseCode() == BillingResponseCode.OK && (productDetailsList != null) && !productDetailsList.isEmpty()) { - buyProduct(productDetailsList.get(0), purchaseListener); + buyProduct(productDetailsList.get(0), token, purchaseListener); } else { Log.e(TAG, "Unable to get product details before buying: " + billingResult.getDebugMessage()); @@ -400,15 +437,18 @@ public class IapGooglePlay implements PurchasesUpdatedListener { } }; + // we don't know if a product is a subscription or inapp product type + // instread we create two product lists from the same set of products and use INAPP for one and SUBS for the other List inappProductList = new ArrayList(); List subsProductList = new ArrayList(); for (String productId : productList) { inappProductList.add(Product.newBuilder().setProductId(productId).setProductType(ProductType.INAPP).build()); subsProductList.add(Product.newBuilder().setProductId(productId).setProductType(ProductType.SUBS).build()); } - QueryProductDetailsParams inappParams = QueryProductDetailsParams.newBuilder().setProductList(inappProductList).build(); - QueryProductDetailsParams subsParams = QueryProductDetailsParams.newBuilder().setProductList(subsProductList).build(); + // do one query per product type + final QueryProductDetailsParams inappParams = QueryProductDetailsParams.newBuilder().setProductList(inappProductList).build(); + final QueryProductDetailsParams subsParams = QueryProductDetailsParams.newBuilder().setProductList(subsProductList).build(); billingClient.queryProductDetailsAsync(inappParams, detailsListener); billingClient.queryProductDetailsAsync(subsParams, detailsListener); } diff --git a/main/main.gui_script b/main/main.gui_script index dad867f..6252d52 100644 --- a/main/main.gui_script +++ b/main/main.gui_script @@ -3,7 +3,7 @@ local dirtylarry = require "dirtylarry/dirtylarry" local GOLDBARS_SMALL = "com.defold.iap.goldbar.small" local GOLDBARS_MEDIUM = "com.defold.iap.goldbar.medium" local GOLDBARS_LARGE = "com.defold.iap.goldbar.large" -local SUBSCRIPTION = "com.defold.iap.subscription" +local SUBSCRIPTION = "com.defold.iap.subscription.one" local NON_CONSUMABLE = "com.defold.iap.removeads" local items = { @@ -66,6 +66,7 @@ local function list() for k,p in pairs(products) do available_items[p.ident] = p log("Item %s", p.ident) + pprint(p) local button = item_buttons[p.ident] if button then gui.set_color(gui.get_node(button.."/larrylabel"), vmath.vector4(1,1,1,1))