From d6cc6f55f9d958ef2b0f778e85dbb630dadb947f Mon Sep 17 00:00:00 2001 From: Denis Dyukorev Date: Sat, 15 Feb 2020 15:00:15 +0300 Subject: [PATCH 1/4] IAP command pointer to products list function to avoid crash on multiple calls products list --- extension-iap/src/iap_android.cpp | 28 ++++++++----------- .../com/defold/iap/IListProductsListener.java | 2 +- .../src/java/com/defold/iap/IapAmazon.java | 16 +++++++---- .../java/com/defold/iap/IapGooglePlay.java | 8 +++--- .../src/java/com/defold/iap/IapJNI.java | 2 +- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/extension-iap/src/iap_android.cpp b/extension-iap/src/iap_android.cpp index 92adf32..136b305 100644 --- a/extension-iap/src/iap_android.cpp +++ b/extension-iap/src/iap_android.cpp @@ -34,7 +34,6 @@ struct IAP bool m_autoFinishTransactions; int m_ProviderId; - dmScript::LuaCallbackInfo* m_ProductCallback; dmScript::LuaCallbackInfo* m_Listener; jobject m_IAP; @@ -61,14 +60,13 @@ static int IAP_List(lua_State* L) return 0; } - if (g_IAP.m_ProductCallback) - dmScript::DestroyCallback(g_IAP.m_ProductCallback); - - g_IAP.m_ProductCallback = dmScript::CreateCallback(L, 2); - JNIEnv* env = Attach(); + IAPCommand* cmd = new IAPCommand; + cmd->m_Callback = dmScript::CreateCallback(L, 2); + cmd->m_Command = IAP_PRODUCT_RESULT; + jstring products = env->NewStringUTF(buf); - env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_List, products, g_IAP.m_IAPJNI); + env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_List, products, g_IAP.m_IAPJNI, (jlong)cmd); env->DeleteLocalRef(products); Detach(); @@ -198,7 +196,7 @@ extern "C" { #endif -JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onProductsResult__ILjava_lang_String_2(JNIEnv* env, jobject, jint responseCode, jstring productList) +JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onProductsResult(JNIEnv* env, jobject, jint responseCode, jstring productList, jlong cmdHandle) { const char* pl = 0; if (productList) @@ -206,16 +204,14 @@ JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onProductsResult__ILjava_lang_ pl = env->GetStringUTFChars(productList, 0); } - IAPCommand cmd; - cmd.m_Callback = g_IAP.m_ProductCallback; - cmd.m_Command = IAP_PRODUCT_RESULT; - cmd.m_ResponseCode = responseCode; + IAPCommand* cmd = (IAPCommand*)cmdHandle; + cmd->m_ResponseCode = responseCode; if (pl) { - cmd.m_Data = strdup(pl); + cmd->m_Data = strdup(pl); env->ReleaseStringUTFChars(productList, pl); } - IAP_Queue_Push(&g_IAP.m_CommandQueue, &cmd); + IAP_Queue_Push(&g_IAP.m_CommandQueue, cmd); } JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onPurchaseResult__ILjava_lang_String_2(JNIEnv* env, jobject, jint responseCode, jstring purchaseData) @@ -287,8 +283,6 @@ static void HandleProductResult(const IAPCommand* cmd) dmScript::TeardownCallback(cmd->m_Callback); dmScript::DestroyCallback(cmd->m_Callback); - assert(g_IAP.m_ProductCallback == cmd->m_Callback); - g_IAP.m_ProductCallback = 0; assert(top == lua_gettop(L)); } @@ -388,7 +382,7 @@ static dmExtension::Result InitializeIAP(dmExtension::Params* params) jclass iap_jni_class = (jclass)env->CallObjectMethod(cls, find_class, str_class_name); env->DeleteLocalRef(str_class_name); - g_IAP.m_List = env->GetMethodID(iap_class, "listItems", "(Ljava/lang/String;Lcom/defold/iap/IListProductsListener;)V"); + 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_Restore = env->GetMethodID(iap_class, "restore", "(Lcom/defold/iap/IPurchaseListener;)V"); g_IAP.m_Stop = env->GetMethodID(iap_class, "stop", "()V"); diff --git a/extension-iap/src/java/com/defold/iap/IListProductsListener.java b/extension-iap/src/java/com/defold/iap/IListProductsListener.java index 1a93be4..b4c236e 100644 --- a/extension-iap/src/java/com/defold/iap/IListProductsListener.java +++ b/extension-iap/src/java/com/defold/iap/IListProductsListener.java @@ -1,5 +1,5 @@ package com.defold.iap; public interface IListProductsListener { - public void onProductsResult(int resultCode, String productList); + public void onProductsResult(int resultCode, String productList, long cmdHandle); } diff --git a/extension-iap/src/java/com/defold/iap/IapAmazon.java b/extension-iap/src/java/com/defold/iap/IapAmazon.java index 089dc36..1badbbd 100644 --- a/extension-iap/src/java/com/defold/iap/IapAmazon.java +++ b/extension-iap/src/java/com/defold/iap/IapAmazon.java @@ -35,6 +35,7 @@ public class IapAmazon implements PurchasingListener { public static final String TAG = "iap"; private HashMap listProductsListeners; + private HashMap listProductsCommandPtrs; private HashMap purchaseListeners; private Activity activity; @@ -54,7 +55,7 @@ public class IapAmazon implements PurchasingListener { public void stop() { } - public void listItems(final String skus, final IListProductsListener listener) { + public void listItems(final String skus, final IListProductsListener listener, final long commandPtr) { final Set skuSet = new HashSet(); for (String x : skus.split(",")) { if (x.trim().length() > 0) { @@ -71,6 +72,7 @@ public class IapAmazon implements PurchasingListener { RequestId req = PurchasingService.getProductData(skuSet); if (req != null) { listProductsListeners.put(req, listener); + listProductsCommandPtrs.put(req, commandPtr); } else { Log.e(TAG, "Did not expect a null requestId"); } @@ -150,17 +152,21 @@ public class IapAmazon implements PurchasingListener { public void onProductDataResponse(ProductDataResponse productDataResponse) { RequestId reqId = productDataResponse.getRequestId(); IListProductsListener listener; + long commadPtr = 0; synchronized (this.listProductsListeners) { listener = this.listProductsListeners.get(reqId); + commadPtr = this.listProductsCommandPtrs.get(reqId); + + this.listProductsListeners.remove(reqId); + this.listProductsCommandPtrs.remove(reqId); if (listener == null) { Log.e(TAG, "No listener found for request " + reqId.toString()); return; } - this.listProductsListeners.remove(reqId); } if (productDataResponse.getRequestStatus() != ProductDataResponse.RequestStatus.SUCCESSFUL) { - listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null); + listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null, commadPtr); } else { Map products = productDataResponse.getProductData(); try { @@ -180,9 +186,9 @@ public class IapAmazon implements PurchasingListener { } data.put(key, item); } - listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_OK, data.toString()); + listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_OK, data.toString(), commadPtr); } catch (JSONException e) { - listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null); + listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null, commadPtr); } } } diff --git a/extension-iap/src/java/com/defold/iap/IapGooglePlay.java b/extension-iap/src/java/com/defold/iap/IapGooglePlay.java index 96aa612..2c7463e 100644 --- a/extension-iap/src/java/com/defold/iap/IapGooglePlay.java +++ b/extension-iap/src/java/com/defold/iap/IapGooglePlay.java @@ -238,7 +238,7 @@ public class IapGooglePlay implements Handler.Callback { }); } - public void listItems(final String skus, final IListProductsListener listener) { + 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) { @@ -261,15 +261,15 @@ public class IapGooglePlay implements Handler.Callback { products.put(key, convertProduct(product)); } } - listener.onProductsResult(resultCode, products.toString()); + listener.onProductsResult(resultCode, products.toString(), commandPtr); } catch(JSONException e) { Log.wtf(TAG, "Failed to convert products", e); - listener.onProductsResult(resultCode, null); + listener.onProductsResult(resultCode, null, commandPtr); } } else { - listener.onProductsResult(resultCode, null); + listener.onProductsResult(resultCode, null, commandPtr); } } })); diff --git a/extension-iap/src/java/com/defold/iap/IapJNI.java b/extension-iap/src/java/com/defold/iap/IapJNI.java index f333fb3..7403083 100644 --- a/extension-iap/src/java/com/defold/iap/IapJNI.java +++ b/extension-iap/src/java/com/defold/iap/IapJNI.java @@ -23,7 +23,7 @@ public class IapJNI implements IListProductsListener, IPurchaseListener { } @Override - public native void onProductsResult(int responseCode, String productList); + public native void onProductsResult(int responseCode, String productList, long cmdHandle); @Override public native void onPurchaseResult(int responseCode, String purchaseData); From cbc1f659c1889e84108a7784d8369297b3679b37 Mon Sep 17 00:00:00 2001 From: Denis Dyukorev Date: Wed, 4 Mar 2020 17:26:52 +0300 Subject: [PATCH 2/4] Add transactions handler to init method according ios documentation it's important to add handler on application:didFinishLaunchingWithOptions: (https://developer.apple.com/documentation/storekit/in-app_purchase/setting_up_the_transaction_observer_for_the_payment_queue?language=objc), move observable commands to separate queue so it can be processed separately and after listener setup, add IAP_ProcessPendingTransactions function to resolve pending transactions on iOS. --- extension-iap/src/iap_android.cpp | 7 ++++ extension-iap/src/iap_emscripten.cpp | 6 +++ extension-iap/src/iap_ios.mm | 56 ++++++++++++++++------------ 3 files changed, 46 insertions(+), 23 deletions(-) diff --git a/extension-iap/src/iap_android.cpp b/extension-iap/src/iap_android.cpp index 136b305..fa6f5a1 100644 --- a/extension-iap/src/iap_android.cpp +++ b/extension-iap/src/iap_android.cpp @@ -50,6 +50,12 @@ struct IAP static IAP g_IAP; +static int IAP_ProcessPendingTransactions(lua_State* L) +{ + //todo handle pending transactions if there is such thing on Android + return 0; +} + static int IAP_List(lua_State* L) { int top = lua_gettop(L); @@ -187,6 +193,7 @@ static const luaL_reg IAP_methods[] = {"restore", IAP_Restore}, {"set_listener", IAP_SetListener}, {"get_provider_id", IAP_GetProviderId}, + {"process_pending_transactions", IAP_ProcessPendingTransactions}, {0, 0} }; diff --git a/extension-iap/src/iap_emscripten.cpp b/extension-iap/src/iap_emscripten.cpp index 7fff8fe..6fb61d3 100644 --- a/extension-iap/src/iap_emscripten.cpp +++ b/extension-iap/src/iap_emscripten.cpp @@ -77,6 +77,11 @@ static void IAPList_Callback(void* luacallback, const char* result_json) dmScript::TeardownCallback(callback); } +static int IAP_ProcessPendingTransactions(lua_State* L) +{ + return 0; +} + static int IAP_List(lua_State* L) { DM_LUA_STACK_CHECK(L, 0); @@ -210,6 +215,7 @@ static const luaL_reg IAP_methods[] = {"restore", IAP_Restore}, {"set_listener", IAP_SetListener}, {"get_provider_id", IAP_GetProviderId}, + {"process_pending_transactions", IAP_ProcessPendingTransactions}, {0, 0} }; diff --git a/extension-iap/src/iap_ios.mm b/extension-iap/src/iap_ios.mm index befb73e..a1abaf0 100644 --- a/extension-iap/src/iap_ios.mm +++ b/extension-iap/src/iap_ios.mm @@ -29,6 +29,7 @@ struct IAP NSMutableDictionary* m_PendingTransactions; dmScript::LuaCallbackInfo* m_Listener; IAPCommandQueue m_CommandQueue; + IAPCommandQueue m_ObservableQueue; SKPaymentTransactionObserver* m_Observer; }; @@ -324,28 +325,25 @@ static void HandlePurchaseResult(IAPCommand* cmd) assert(top == lua_gettop(L)); } -@implementation SKPaymentTransactionObserver - - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions - { - for (SKPaymentTransaction* transaction in transactions) { - - if ((!self.m_IAP->m_AutoFinishTransactions) && (transaction.transactionState == SKPaymentTransactionStatePurchased)) { +static void processTransactions(IAP* m_IAP, NSArray* transactions) { + for (SKPaymentTransaction* transaction in transactions) { + if ((!m_IAP->m_AutoFinishTransactions) && (transaction.transactionState == SKPaymentTransactionStatePurchased)) { NSData *data = [transaction.transactionIdentifier dataUsingEncoding:NSUTF8StringEncoding]; uint64_t trans_id_hash = dmHashBuffer64((const char*) [data bytes], [data length]); - [self.m_IAP->m_PendingTransactions setObject:transaction forKey:[NSNumber numberWithInteger:trans_id_hash] ]; + [m_IAP->m_PendingTransactions setObject:transaction forKey:[NSNumber numberWithInteger:trans_id_hash] ]; } - if (!self.m_IAP->m_Listener) + if (!m_IAP->m_Listener) continue; IAPTransaction* iap_transaction = new IAPTransaction; CopyTransaction(transaction, iap_transaction); IAPCommand cmd; - cmd.m_Callback = self.m_IAP->m_Listener; + cmd.m_Callback = m_IAP->m_Listener; cmd.m_Command = IAP_PURCHASE_RESULT; cmd.m_Data = iap_transaction; - IAP_Queue_Push(&self.m_IAP->m_CommandQueue, &cmd); + IAP_Queue_Push(&m_IAP->m_ObservableQueue, &cmd); switch (transaction.transactionState) { @@ -363,7 +361,19 @@ static void HandlePurchaseResult(IAPCommand* cmd) default: break; } - } + } +} + +static int IAP_ProcessPendingTransactions(lua_State* L) +{ + processTransactions(&g_IAP, [SKPaymentQueue defaultQueue].transactions); + return 0; +} + +@implementation SKPaymentTransactionObserver + - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions + { + processTransactions(self.m_IAP, transactions); } @end @@ -477,18 +487,6 @@ static int IAP_SetListener(lua_State* L) dmScript::DestroyCallback(iap->m_Listener); iap->m_Listener = dmScript::CreateCallback(L, 1); - - if (g_IAP.m_Observer == 0) { - SKPaymentTransactionObserver* observer = [[SKPaymentTransactionObserver alloc] init]; - observer.m_IAP = &g_IAP; - // NOTE: We add the listener *after* a lua listener is set - // The payment queue is persistent and "old" transaction might be processed - // from previous session. We call "finishTransaction" when appropriate - // for all transaction and we must ensure that the result is delivered to lua. - [[SKPaymentQueue defaultQueue] addTransactionObserver: observer]; - g_IAP.m_Observer = observer; - } - return 0; } @@ -506,6 +504,7 @@ static const luaL_reg IAP_methods[] = {"restore", IAP_Restore}, {"set_listener", IAP_SetListener}, {"get_provider_id", IAP_GetProviderId}, + {"process_pending_transactions", IAP_ProcessPendingTransactions}, {0, 0} }; @@ -520,6 +519,7 @@ static dmExtension::Result InitializeIAP(dmExtension::Params* params) g_IAP.m_InitCount++; IAP_Queue_Create(&g_IAP.m_CommandQueue); + IAP_Queue_Create(&g_IAP.m_ObservableQueue); lua_State*L = params->m_L; int top = lua_gettop(L); @@ -535,6 +535,12 @@ static dmExtension::Result InitializeIAP(dmExtension::Params* params) lua_pop(L, 1); assert(top == lua_gettop(L)); + + SKPaymentTransactionObserver* observer = [[SKPaymentTransactionObserver alloc] init]; + observer.m_IAP = &g_IAP; + [[SKPaymentQueue defaultQueue] addTransactionObserver: observer]; + g_IAP.m_Observer = observer; + return dmExtension::RESULT_OK; } @@ -557,6 +563,9 @@ static void IAP_OnCommand(IAPCommand* cmd, void*) static dmExtension::Result UpdateIAP(dmExtension::Params* params) { IAP_Queue_Flush(&g_IAP.m_CommandQueue, IAP_OnCommand, 0); + if (g_IAP.m_Observer) { + IAP_Queue_Flush(&g_IAP.m_ObservableQueue, IAP_OnCommand, 0); + } return dmExtension::RESULT_OK; } @@ -585,6 +594,7 @@ static dmExtension::Result FinalizeIAP(dmExtension::Params* params) } IAP_Queue_Destroy(&g_IAP.m_CommandQueue); + IAP_Queue_Destroy(&g_IAP.m_ObservableQueue); return dmExtension::RESULT_OK; } From 564df1e279e85d39354b8f8ed6f8e0e19bb3a970 Mon Sep 17 00:00:00 2001 From: Denis Dyukorev Date: Tue, 31 Mar 2020 16:27:22 +0700 Subject: [PATCH 3/4] Rename function argument to match naming requirements. --- extension-iap/src/iap_ios.mm | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/extension-iap/src/iap_ios.mm b/extension-iap/src/iap_ios.mm index a1abaf0..ed6c40e 100644 --- a/extension-iap/src/iap_ios.mm +++ b/extension-iap/src/iap_ios.mm @@ -325,25 +325,25 @@ static void HandlePurchaseResult(IAPCommand* cmd) assert(top == lua_gettop(L)); } -static void processTransactions(IAP* m_IAP, NSArray* transactions) { +static void processTransactions(IAP* iap, NSArray* transactions) { for (SKPaymentTransaction* transaction in transactions) { - if ((!m_IAP->m_AutoFinishTransactions) && (transaction.transactionState == SKPaymentTransactionStatePurchased)) { + if ((!iap->m_AutoFinishTransactions) && (transaction.transactionState == SKPaymentTransactionStatePurchased)) { NSData *data = [transaction.transactionIdentifier dataUsingEncoding:NSUTF8StringEncoding]; uint64_t trans_id_hash = dmHashBuffer64((const char*) [data bytes], [data length]); - [m_IAP->m_PendingTransactions setObject:transaction forKey:[NSNumber numberWithInteger:trans_id_hash] ]; + [iap->m_PendingTransactions setObject:transaction forKey:[NSNumber numberWithInteger:trans_id_hash] ]; } - if (!m_IAP->m_Listener) + if (!iap->m_Listener) continue; IAPTransaction* iap_transaction = new IAPTransaction; CopyTransaction(transaction, iap_transaction); IAPCommand cmd; - cmd.m_Callback = m_IAP->m_Listener; + cmd.m_Callback = iap->m_Listener; cmd.m_Command = IAP_PURCHASE_RESULT; cmd.m_Data = iap_transaction; - IAP_Queue_Push(&m_IAP->m_ObservableQueue, &cmd); + IAP_Queue_Push(&iap->m_ObservableQueue, &cmd); switch (transaction.transactionState) { From 9a40a8ea731822738a20d65af1e92a5b278ae652 Mon Sep 17 00:00:00 2001 From: Denis Dyukorev Date: Tue, 31 Mar 2020 17:22:07 +0700 Subject: [PATCH 4/4] Add 'process_pending_transactions' function documentation --- docs/_data/api.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/_data/api.yml b/docs/_data/api.yml index cfa9b2a..a74426a 100644 --- a/docs/_data/api.yml +++ b/docs/_data/api.yml @@ -161,6 +161,13 @@ desc: value is `true` if current store supports handling restored transactions, otherwise `false`. +#***************************************************************************************************** + + - name: process_pending_transactions + type: function + desc: Process transactions still unprocessed from previous session if any. Transactions will be + processed with callback function set with `set_listener` function + #***************************************************************************************************** - name: set_listener