diff --git a/docs/_data/api.yml b/docs/_data/api.yml index a74426a..5be1dc8 100644 --- a/docs/_data/api.yml +++ b/docs/_data/api.yml @@ -1,6 +1,6 @@ - name: iap - type: table - desc: Functions and constants for doing in-app purchases. Supported on iOS, Android (Google Play and Amazon) + type: table + desc: Functions and constants for doing in-app purchases. Supported on iOS, Android (Google Play and Amazon) and Facebook Canvas platforms. [icon:ios] [icon:googleplay] [icon:amazon] [icon:facebook] members: @@ -61,6 +61,19 @@ type: table desc: transaction table parameter as supplied in listener callback +#***************************************************************************************************** + + - name: acknowledge + type: function + desc: Acknowledges a product transaction but does not consume it. + [icon:attention] [icon:googleplay] Calling iap.acknowledge is required on a successful transaction + on Google Play unless iap.finish is called. + The `transaction.state` field must equal `iap.TRANS_STATE_PURCHASED`. + parameters: + - name: transaction + type: table + desc: transaction table parameter as supplied in listener callback + #***************************************************************************************************** - name: get_provider_id @@ -210,7 +223,7 @@ - name: trans_ident type: string - desc: The transaction identifier. This field is only set when `state` is + desc: The transaction identifier. This field is only set when `state` is `TRANS_STATE_RESTORED`, `TRANS_STATE_UNVERIFIED` or `TRANS_STATE_PURCHASED`. - name: receipt @@ -260,47 +273,45 @@ - name: PROVIDER_ID_AMAZON type: number desc: provider id for Amazon - + - name: PROVIDER_ID_APPLE type: number desc: provider id for Apple - + - name: PROVIDER_ID_FACEBOOK type: number desc: provider id for Facebook - + - name: PROVIDER_ID_GOOGLE type: number desc: iap provider id for Google - + - name: REASON_UNSPECIFIED type: number desc: unspecified error reason - + - name: REASON_USER_CANCELED type: number desc: user canceled reason - + - name: TRANS_STATE_FAILED type: number desc: transaction failed state - + - name: TRANS_STATE_PURCHASED type: number desc: transaction purchased state - + - name: TRANS_STATE_PURCHASING type: number desc: transaction purchasing state This is an intermediate mode followed by TRANS_STATE_PURCHASED. Store provider support dependent. - + - name: TRANS_STATE_RESTORED type: number desc: transaction restored state This is only available on store providers supporting restoring purchases. - + - name: TRANS_STATE_UNVERIFIED type: number desc: transaction unverified state, requires verification of purchase - - diff --git a/extension-iap/src/iap_android.cpp b/extension-iap/src/iap_android.cpp index 28757e9..5a8e5a8 100644 --- a/extension-iap/src/iap_android.cpp +++ b/extension-iap/src/iap_android.cpp @@ -43,6 +43,7 @@ struct IAP jmethodID m_Buy; jmethodID m_Restore; jmethodID m_ProcessPendingConsumables; + jmethodID m_AcknowledgeTransaction; jmethodID m_FinishTransaction; IAPCommandQueue m_CommandQueue; @@ -146,6 +147,46 @@ static int IAP_Finish(lua_State* L) return 0; } +static int IAP_Acknowledge(lua_State* L) +{ + int top = lua_gettop(L); + + luaL_checktype(L, 1, LUA_TTABLE); + + lua_getfield(L, -1, "state"); + if (lua_isnumber(L, -1)) + { + if(lua_tointeger(L, -1) != TRANS_STATE_PURCHASED) + { + dmLogError("Invalid transaction state (must be iap.TRANS_STATE_PURCHASED)."); + lua_pop(L, 1); + assert(top == lua_gettop(L)); + return 0; + } + } + lua_pop(L, 1); + + lua_getfield(L, -1, "receipt"); + if (!lua_isstring(L, -1)) { + dmLogError("Transaction error. Invalid transaction data, does not contain 'receipt' key."); + lua_pop(L, 1); + } + else + { + const char * receipt = lua_tostring(L, -1); + lua_pop(L, 1); + + JNIEnv* env = Attach(); + jstring receiptUTF = env->NewStringUTF(receipt); + env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_AcknowledgeTransaction, receiptUTF, g_IAP.m_IAPJNI); + env->DeleteLocalRef(receiptUTF); + Detach(); + } + + assert(top == lua_gettop(L)); + return 0; +} + static int IAP_Restore(lua_State* L) { // TODO: Missing callback here for completion/error @@ -193,6 +234,7 @@ static const luaL_reg IAP_methods[] = {"list", IAP_List}, {"buy", IAP_Buy}, {"finish", IAP_Finish}, + {"acknowledge", IAP_Acknowledge}, {"restore", IAP_Restore}, {"set_listener", IAP_SetListener}, {"get_provider_id", IAP_GetProviderId}, @@ -226,6 +268,7 @@ JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onProductsResult(JNIEnv* env, JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onPurchaseResult__ILjava_lang_String_2(JNIEnv* env, jobject, jint responseCode, jstring purchaseData) { + dmLogInfo("Java_com_defold_iap_IapJNI_onPurchaseResult__ILjava_lang_String_2 %d", (int)responseCode); const char* pd = 0; if (purchaseData) { @@ -398,6 +441,7 @@ static dmExtension::Result InitializeIAP(dmExtension::Params* params) g_IAP.m_Stop = env->GetMethodID(iap_class, "stop", "()V"); g_IAP.m_ProcessPendingConsumables = env->GetMethodID(iap_class, "processPendingConsumables", "(Lcom/defold/iap/IPurchaseListener;)V"); g_IAP.m_FinishTransaction = env->GetMethodID(iap_class, "finishTransaction", "(Ljava/lang/String;Lcom/defold/iap/IPurchaseListener;)V"); + g_IAP.m_AcknowledgeTransaction = env->GetMethodID(iap_class, "acknowledgeTransaction", "(Ljava/lang/String;Lcom/defold/iap/IPurchaseListener;)V"); jmethodID jni_constructor = env->GetMethodID(iap_class, "", "(Landroid/app/Activity;Z)V"); g_IAP.m_IAP = env->NewGlobalRef(env->NewObject(iap_class, jni_constructor, dmGraphics::GetNativeAndroidActivity(), g_IAP.m_autoFinishTransactions)); diff --git a/extension-iap/src/iap_emscripten.cpp b/extension-iap/src/iap_emscripten.cpp index 6fb61d3..bb691af 100644 --- a/extension-iap/src/iap_emscripten.cpp +++ b/extension-iap/src/iap_emscripten.cpp @@ -193,6 +193,11 @@ static int IAP_Finish(lua_State* L) return 0; } +static int IAP_Acknowledge(lua_State* L) +{ + return 0; +} + static int IAP_Restore(lua_State* L) { DM_LUA_STACK_CHECK(L, 1); @@ -212,6 +217,7 @@ static const luaL_reg IAP_methods[] = {"list", IAP_List}, {"buy", IAP_Buy}, {"finish", IAP_Finish}, + {"acknowledge", IAP_Acknowledge}, {"restore", IAP_Restore}, {"set_listener", IAP_SetListener}, {"get_provider_id", IAP_GetProviderId}, diff --git a/extension-iap/src/iap_ios.mm b/extension-iap/src/iap_ios.mm index 6c4a0d2..ee30346 100644 --- a/extension-iap/src/iap_ios.mm +++ b/extension-iap/src/iap_ios.mm @@ -503,6 +503,11 @@ static int IAP_SetListener(lua_State* L) return 0; } +static int IAP_Acknowledge(lua_State* L) +{ + return 0; +} + static int IAP_GetProviderId(lua_State* L) { lua_pushinteger(L, PROVIDER_ID_APPLE); @@ -514,6 +519,7 @@ static const luaL_reg IAP_methods[] = {"list", IAP_List}, {"buy", IAP_Buy}, {"finish", IAP_Finish}, + {"acknowledge", IAP_Acknowledge}, {"restore", IAP_Restore}, {"set_listener", IAP_SetListener}, {"get_provider_id", IAP_GetProviderId}, diff --git a/extension-iap/src/java/com/defold/iap/IapGooglePlay.java b/extension-iap/src/java/com/defold/iap/IapGooglePlay.java index c978884..dded5eb 100644 --- a/extension-iap/src/java/com/defold/iap/IapGooglePlay.java +++ b/extension-iap/src/java/com/defold/iap/IapGooglePlay.java @@ -28,11 +28,12 @@ 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.AcknowledgePurchaseParams; import com.android.billingclient.api.PurchasesUpdatedListener; import com.android.billingclient.api.BillingClientStateListener; import com.android.billingclient.api.ConsumeResponseListener; import com.android.billingclient.api.SkuDetailsResponseListener; - +import com.android.billingclient.api.AcknowledgePurchaseResponseListener; public class IapGooglePlay implements PurchasesUpdatedListener { public static final String TAG = "IapGooglePlay"; @@ -70,7 +71,14 @@ public class IapGooglePlay implements PurchasesUpdatedListener { public void stop() { Log.d(TAG, "stop()"); - billingClient.endConnection(); + this.activity.runOnUiThread(new Runnable() { + @Override + public void run() { + if (billingClient.isReady()) { + billingClient.endConnection(); + } + } + }); } public String toISO8601(final Date date) { @@ -172,6 +180,7 @@ public class IapGooglePlay implements PurchasesUpdatedListener { defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_ERROR; break; } + Log.d(TAG, "billingResponseCodeToDefoldResponse: " + responseCode + " defoldResponse: " + defoldResponse); return defoldResponse; } @@ -241,6 +250,29 @@ public class IapGooglePlay implements PurchasesUpdatedListener { }); } + /** + * Called from Lua. This method will try to acknowledge a purchase (but not finish/consume it). + */ + public void acknowledgeTransaction(final String purchaseToken, final IPurchaseListener purchaseListener) { + Log.d(TAG, "acknowledgeTransaction() " + purchaseToken); + + AcknowledgePurchaseParams acknowledgeParams = AcknowledgePurchaseParams.newBuilder() + .setPurchaseToken(purchaseToken) + .build(); + + billingClient.acknowledgePurchase(acknowledgeParams, new AcknowledgePurchaseResponseListener() { + @Override + public void onAcknowledgePurchaseResponse(BillingResult billingResult) { + Log.d(TAG, "acknowledgeTransaction() response code " + billingResult.getResponseCode()); + // note: we only call the purchase listener if an error happens + if (billingResult.getResponseCode() != BillingResponseCode.OK) { + Log.e(TAG, "Unable to acknowledge purchase: " + billingResult.getDebugMessage()); + purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), ""); + } + } + }); + } + /** * Handle a purchase. If the extension is configured to automatically * finish transactions the purchase will be immediately consumed. Otherwise @@ -385,7 +417,11 @@ public class IapGooglePlay implements PurchasesUpdatedListener { }); } + /** + * Called from Lua. + */ public void restore(final IPurchaseListener listener) { Log.d(TAG, "restore()"); + processPendingConsumables(listener); } } diff --git a/main/main.gui b/main/main.gui index 3500faa..3b59b8d 100644 --- a/main/main.gui +++ b/main/main.gui @@ -1180,6 +1180,322 @@ nodes { text_leading: 1.0 text_tracking: 0.0 } +nodes { + position { + x: 40.0 + y: 153.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: "chk_finish" + layer: "" + inherit_alpha: true + alpha: 1.0 + template: "/dirtylarry/checkbox_label.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: 64.0 + y: 68.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: "checkbox/checkbox_normal" + id: "chk_finish/larrycheckbox" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + adjust_mode: ADJUST_MODE_FIT + parent: "chk_finish" + layer: "" + inherit_alpha: true + slice9 { + x: 0.0 + y: 0.0 + z: 0.0 + w: 0.0 + } + clipping_mode: CLIPPING_MODE_NONE + clipping_visible: true + clipping_inverted: false + alpha: 1.0 + template_node_child: true + size_mode: SIZE_MODE_MANUAL +} +nodes { + position { + x: 46.0 + y: 3.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: 50.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: "Finish" + font: "larryfont" + id: "chk_finish/larrylabel" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_W + outline { + x: 1.0 + y: 1.0 + z: 1.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: "chk_finish" + 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 +} +nodes { + position { + x: 362.0 + y: 153.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: "chk_acknowledge" + layer: "" + inherit_alpha: true + alpha: 1.0 + template: "/dirtylarry/checkbox_label.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: 64.0 + y: 68.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: "checkbox/checkbox_normal" + id: "chk_acknowledge/larrycheckbox" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + adjust_mode: ADJUST_MODE_FIT + parent: "chk_acknowledge" + layer: "" + inherit_alpha: true + slice9 { + x: 0.0 + y: 0.0 + z: 0.0 + w: 0.0 + } + clipping_mode: CLIPPING_MODE_NONE + clipping_visible: true + clipping_inverted: false + alpha: 1.0 + template_node_child: true + size_mode: SIZE_MODE_MANUAL +} +nodes { + position { + x: 46.0 + y: 3.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: 50.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: "Acknowledge" + font: "larryfont" + id: "chk_acknowledge/larrylabel" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_W + outline { + x: 1.0 + y: 1.0 + z: 1.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: "chk_acknowledge" + 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 241f388..f9afd34 100644 --- a/main/main.gui_script +++ b/main/main.gui_script @@ -89,13 +89,20 @@ local function buy_listener(self, transaction, error) product_items["reset"] = transaction else log("iap.buy() ok %s", transaction.ident) - log("iap.finish() %s", transaction.ident) - iap.finish(transaction) + if self.finish then + log("iap.finish() %s", transaction.ident) + iap.finish(transaction) + elseif self.acknowledge then + log("iap.acknowledge() %s", transaction.ident) + iap.acknowledge(transaction) + end end end function init(self) self.log = {} + self.finish = false + self.acknowledge = false log("init()") msg.post(".", "acquire_input_focus") if not iap then @@ -125,5 +132,7 @@ function on_input(self, action_id, action) dirtylarry:button("pending", action_id, action, function() process_pending_transactions() end) + self.finish = dirtylarry:checkbox("chk_finish", action_id, action, self.finish) + self.acknowledge = dirtylarry:checkbox("chk_acknowledge", action_id, action, self.acknowledge) end end