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 diff --git a/extension-iap/src/iap_android.cpp b/extension-iap/src/iap_android.cpp index 92adf32..fa6f5a1 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; @@ -51,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); @@ -61,14 +66,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(); @@ -189,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} }; @@ -198,7 +203,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 +211,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 +290,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 +389,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/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 750b5e8..6c4a0d2 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; }; @@ -337,28 +338,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* iap, NSArray* transactions) { + for (SKPaymentTransaction* transaction in transactions) { + 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]); - [self.m_IAP->m_PendingTransactions setObject:transaction forKey:[NSNumber numberWithInteger:trans_id_hash] ]; + [iap->m_PendingTransactions setObject:transaction forKey:[NSNumber numberWithInteger:trans_id_hash] ]; } - if (!self.m_IAP->m_Listener) + if (!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 = 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(&iap->m_ObservableQueue, &cmd); switch (transaction.transactionState) { @@ -376,7 +374,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 @@ -490,18 +500,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; } @@ -519,6 +517,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} }; @@ -533,6 +532,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); @@ -548,6 +548,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; } @@ -570,6 +576,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; } @@ -598,6 +607,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; } 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 00372c7..fce6853 100644 --- a/extension-iap/src/java/com/defold/iap/IapGooglePlay.java +++ b/extension-iap/src/java/com/defold/iap/IapGooglePlay.java @@ -253,7 +253,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) { @@ -276,15 +276,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);