diff --git a/extension-iap/src/iap_android.cpp b/extension-iap/src/iap_android.cpp index f036ca1..6dbfe06 100644 --- a/extension-iap/src/iap_android.cpp +++ b/extension-iap/src/iap_android.cpp @@ -9,22 +9,6 @@ #define LIB_NAME "iap" -struct IAP; - -#define CMD_PRODUCT_RESULT (0) -#define CMD_PURCHASE_RESULT (1) - -struct DM_ALIGNED(16) IAPCommand -{ - IAPCommand() - { - memset(this, 0, sizeof(IAPCommand)); - } - uint32_t m_Command; - int32_t m_ResponseCode; - void* m_Data1; -}; - static JNIEnv* Attach() { JNIEnv* env; @@ -50,44 +34,31 @@ struct IAP m_autoFinishTransactions = true; m_ProviderId = PROVIDER_ID_GOOGLE; } - int m_InitCount; - int m_Callback; - int m_Self; - bool m_autoFinishTransactions; - int m_ProviderId; - lua_State* m_L; - IAPListener m_Listener; + int m_InitCount; + int m_Callback; + int m_Self; + bool m_autoFinishTransactions; + int m_ProviderId; + lua_State* m_L; + IAPListener m_Listener; - jobject m_IAP; - jobject m_IAPJNI; - jmethodID m_List; - jmethodID m_Stop; - jmethodID m_Buy; - jmethodID m_Restore; - jmethodID m_ProcessPendingConsumables; - jmethodID m_FinishTransaction; + jobject m_IAP; + jobject m_IAPJNI; + jmethodID m_List; + jmethodID m_Stop; + jmethodID m_Buy; + jmethodID m_Restore; + jmethodID m_ProcessPendingConsumables; + jmethodID m_FinishTransaction; - dmArray m_CommandsQueue; - dmMutex::HMutex m_Mutex; + IAPCommandQueue m_CommandQueue; }; static IAP g_IAP; -static void QueueCommand(IAPCommand* cmd) -{ - DM_MUTEX_SCOPED_LOCK(g_IAP.m_Mutex); - - if(g_IAP.m_CommandsQueue.Full()) - { - g_IAP.m_CommandsQueue.OffsetCapacity(2); - } - g_IAP.m_CommandsQueue.Push(*cmd); -} - -static void VerifyCallback(lua_State* L) +static void ResetCallback(lua_State* L) { if (g_IAP.m_Callback != LUA_NOREF) { - dmLogError("Unexpected callback set"); dmScript::Unref(L, LUA_REGISTRYINDEX, g_IAP.m_Callback); dmScript::Unref(L, LUA_REGISTRYINDEX, g_IAP.m_Self); g_IAP.m_Callback = LUA_NOREF; @@ -99,7 +70,7 @@ static void VerifyCallback(lua_State* L) static int IAP_List(lua_State* L) { int top = lua_gettop(L); - VerifyCallback(L); + ResetCallback(L); char* buf = IAP_List_CreateBuffer(L); if( buf == 0 ) @@ -267,14 +238,14 @@ JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onProductsResult__ILjava_lang_ } IAPCommand cmd; - cmd.m_Command = CMD_PRODUCT_RESULT; + cmd.m_Command = IAP_PRODUCT_RESULT; cmd.m_ResponseCode = responseCode; if (pl) { - cmd.m_Data1 = strdup(pl); + cmd.m_Data = strdup(pl); env->ReleaseStringUTFChars(productList, pl); } - QueueCommand(&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) @@ -286,14 +257,14 @@ JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onPurchaseResult__ILjava_lang_ } IAPCommand cmd; - cmd.m_Command = CMD_PURCHASE_RESULT; + cmd.m_Command = IAP_PURCHASE_RESULT; cmd.m_ResponseCode = responseCode; if (pd) { - cmd.m_Data1 = strdup(pd); + cmd.m_Data = strdup(pd); env->ReleaseStringUTFChars(purchaseData, pd); } - QueueCommand(&cmd); + IAP_Queue_Push(&g_IAP.m_CommandQueue, &cmd); } #ifdef __cplusplus @@ -327,7 +298,7 @@ static void HandleProductResult(const IAPCommand* cmd) if (cmd->m_ResponseCode == BILLING_RESPONSE_RESULT_OK) { dmJson::Document doc; - dmJson::Result r = dmJson::Parse((const char*) cmd->m_Data1, &doc); + dmJson::Result r = dmJson::Parse((const char*) cmd->m_Data, &doc); if (r == dmJson::RESULT_OK && doc.m_NodeCount > 0) { char err_str[128]; if (dmScript::JsonToLua(L, &doc, 0, err_str, sizeof(err_str)) < 0) { @@ -390,9 +361,9 @@ static void HandlePurchaseResult(const IAPCommand* cmd) } if (cmd->m_ResponseCode == BILLING_RESPONSE_RESULT_OK) { - if (cmd->m_Data1 != 0) { + if (cmd->m_Data != 0) { dmJson::Document doc; - dmJson::Result r = dmJson::Parse((const char*) cmd->m_Data1, &doc); + dmJson::Result r = dmJson::Parse((const char*) cmd->m_Data, &doc); if (r == dmJson::RESULT_OK && doc.m_NodeCount > 0) { char err_str[128]; if (dmScript::JsonToLua(L, &doc, 0, err_str, sizeof(err_str)) < 0) { @@ -436,8 +407,7 @@ static dmExtension::Result InitializeIAP(dmExtension::Params* params) // TODO: Life-cycle managaemnt is *budget*. No notion of "static initalization" // Extend extension functionality with per system initalization? if (g_IAP.m_InitCount == 0) { - g_IAP.m_CommandsQueue.SetCapacity(2); - g_IAP.m_Mutex = dmMutex::New(); + IAP_Queue_Create(&g_IAP.m_CommandQueue); g_IAP.m_autoFinishTransactions = dmConfigFile::GetInt(params->m_ConfigFile, "iap.auto_finish_transactions", 1) == 1; @@ -499,42 +469,35 @@ static dmExtension::Result InitializeIAP(dmExtension::Params* params) return dmExtension::RESULT_OK; } +static void IAP_OnCommand(IAPCommand* cmd, void*) +{ + switch (cmd->m_Command) + { + case IAP_PRODUCT_RESULT: + HandleProductResult(cmd); + break; + case IAP_PURCHASE_RESULT: + HandlePurchaseResult(cmd); + break; + + default: + assert(false); + } + + if (cmd->m_Data) { + free(cmd->m_Data); + } +} + static dmExtension::Result UpdateIAP(dmExtension::Params* params) { - if (g_IAP.m_CommandsQueue.Empty()) - { - return dmExtension::RESULT_OK; - } - - DM_MUTEX_SCOPED_LOCK(g_IAP.m_Mutex); - - for(uint32_t i = 0; i != g_IAP.m_CommandsQueue.Size(); ++i) - { - IAPCommand& cmd = g_IAP.m_CommandsQueue[i]; - switch (cmd.m_Command) - { - case CMD_PRODUCT_RESULT: - HandleProductResult(&cmd); - break; - case CMD_PURCHASE_RESULT: - HandlePurchaseResult(&cmd); - break; - - default: - assert(false); - } - - if (cmd.m_Data1) { - free(cmd.m_Data1); - } - } - g_IAP.m_CommandsQueue.SetSize(0); + IAP_Queue_Flush(&g_IAP.m_CommandQueue, IAP_OnCommand, 0); return dmExtension::RESULT_OK; } static dmExtension::Result FinalizeIAP(dmExtension::Params* params) { - dmMutex::Delete(g_IAP.m_Mutex); + IAP_Queue_Destroy(&g_IAP.m_CommandQueue); --g_IAP.m_InitCount; if (params->m_L == g_IAP.m_Listener.m_L && g_IAP.m_Listener.m_Callback != LUA_NOREF) { diff --git a/extension-iap/src/iap_ios.mm b/extension-iap/src/iap_ios.mm index 663ce33..89d9fa8 100644 --- a/extension-iap/src/iap_ios.mm +++ b/extension-iap/src/iap_ios.mm @@ -17,165 +17,208 @@ struct IAP; @property IAP* m_IAP; @end -/*# In-app purchases API documentation - * - * Functions and constants for interacting with Apple's In-app purchases - * and Google's In-app billing. - * - * @document - * @name In-app purchases - * @namespace iap - */ - struct IAP { IAP() { - m_Callback = LUA_NOREF; - m_Self = LUA_NOREF; - m_InitCount = 0; + memset(this, 0, sizeof(*this)); m_AutoFinishTransactions = true; - m_PendingTransactions = 0; - } - int m_InitCount; - int m_Callback; - int m_Self; - bool m_AutoFinishTransactions; - NSMutableDictionary* m_PendingTransactions; - IAPListener m_Listener; - SKPaymentTransactionObserver* m_Observer; + IAP_ClearCallback(&m_Listener); + } + int m_InitCount; + bool m_AutoFinishTransactions; + NSMutableDictionary* m_PendingTransactions; + IAPListener m_Listener; + IAPCommandQueue m_CommandQueue; + SKPaymentTransactionObserver* m_Observer; }; IAP g_IAP; +// The payload of the purchasing commands +struct IAPProduct +{ + const char* ident; + const char* title; + const char* description; + float price; + const char* price_string; + const char* currency_code; +}; + +struct IAPResponse +{ + const char* error; // optional + int32_t error_code; // only valid if error is set + dmArray m_Products; + + IAPResponse() { + memset(this, 0, sizeof(*this)); + } +}; + +struct IAPTransaction +{ + const char* ident; + int32_t state; + const char* date; + const char* trans_ident; // optional + const char* receipt; // optional + uint32_t receipt_length; + const char* original_trans; // optional + const char* error; // optional + int32_t error_code; // only valid if error is set + IAPTransaction* m_OriginalTransaction; + + IAPTransaction() { + memset(this, 0, sizeof(*this)); + } +}; + +static void IAP_FreeProduct(IAPProduct* product) +{ + free((void*)product->ident); + free((void*)product->title); + free((void*)product->description); + free((void*)product->price_string); + free((void*)product->currency_code); +} + +static void IAP_FreeTransaction(IAPTransaction* transaction) +{ + if (transaction->m_OriginalTransaction) { + IAP_FreeTransaction(transaction->m_OriginalTransaction); + } + delete transaction->m_OriginalTransaction; + free((void*)transaction->ident); + free((void*)transaction->date); + free((void*)transaction->trans_ident); + free((void*)transaction->receipt); + free((void*)transaction->original_trans); + free((void*)transaction->error); +} + @interface SKProductsRequestDelegate : NSObject - @property lua_State* m_LuaState; + @property IAPListener m_Callback; @property (assign) SKProductsRequest* m_Request; @end @implementation SKProductsRequestDelegate - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{ - lua_State* L = self.m_LuaState; - if (g_IAP.m_Callback == LUA_NOREF) { + IAPListener callback = self.m_Callback; + if (!IAP_CallbackIsValid(&callback)) { dmLogError("No callback set"); return; } - NSArray * skProducts = response.products; - int top = lua_gettop(L); + NSArray* skProducts = response.products; - lua_rawgeti(L, LUA_REGISTRYINDEX, g_IAP.m_Callback); - - // Setup self - lua_rawgeti(L, LUA_REGISTRYINDEX, g_IAP.m_Self); - lua_pushvalue(L, -1); - dmScript::SetInstance(L); - - if (!dmScript::IsInstanceValid(L)) - { - dmLogError("Could not run facebook callback because the instance has been deleted."); - lua_pop(L, 2); - assert(top == lua_gettop(L)); - return; - } - - lua_newtable(L); + IAPResponse* iap_response = new IAPResponse; for (SKProduct * p in skProducts) { - lua_pushstring(L, [p.productIdentifier UTF8String]); - lua_newtable(L); - - lua_pushstring(L, "ident"); - lua_pushstring(L, [p.productIdentifier UTF8String]); - lua_rawset(L, -3); - - lua_pushstring(L, "title"); - lua_pushstring(L, [p.localizedTitle UTF8String]); - lua_rawset(L, -3); - - lua_pushstring(L, "description"); - lua_pushstring(L, [p.localizedDescription UTF8String]); - lua_rawset(L, -3); - - lua_pushstring(L, "price"); - lua_pushnumber(L, p.price.floatValue); - lua_rawset(L, -3); + IAPProduct product = {0}; + product.ident = strdup([p.productIdentifier UTF8String]); + product.title = strdup([p.localizedTitle UTF8String]); + product.description = strdup([p.localizedDescription UTF8String]); + product.currency_code = strdup([[p.priceLocale objectForKey:NSLocaleCurrencyCode] UTF8String]); + product.price = p.price.floatValue; NSNumberFormatter *formatter = [[[NSNumberFormatter alloc] init] autorelease]; [formatter setNumberStyle: NSNumberFormatterCurrencyStyle]; [formatter setLocale: p.priceLocale]; - NSString *price_string = [formatter stringFromNumber: p.price]; + NSString* price_string = [formatter stringFromNumber: p.price]; + product.price_string = strdup([price_string UTF8String]); - lua_pushstring(L, "price_string"); - lua_pushstring(L, [price_string UTF8String]); - lua_rawset(L, -3); + if (iap_response->m_Products.Full()) { + iap_response->m_Products.OffsetCapacity(2); + } + iap_response->m_Products.Push(product); + } - lua_pushstring(L, "currency_code"); - lua_pushstring(L, [[p.priceLocale objectForKey:NSLocaleCurrencyCode] UTF8String]); - lua_rawset(L, -3); + + IAPCommand cmd; + cmd.m_Command = IAP_PRODUCT_RESULT; + cmd.m_Callback = callback; + cmd.m_Data = iap_response; + IAP_Queue_Push(&g_IAP.m_CommandQueue, &cmd); +} + +static void HandleProductResult(IAPCommand* cmd) +{ + + IAPResponse* response = (IAPResponse*)cmd->m_Data; + + lua_State* L = cmd->m_Callback.m_L; + int top = lua_gettop(L); + + if (!IAP_SetupCallback(&cmd->m_Callback)) + { + assert(top == lua_gettop(L)); + delete response; + return; + } + + lua_newtable(L); + + for (uint32_t i = 0; i < response->m_Products.Size(); ++i) { + IAPProduct* product = &response->m_Products[i]; + + lua_pushstring(L, product->ident); + lua_newtable(L); + + lua_pushstring(L, product->ident); + lua_setfield(L, -2, "ident"); + lua_pushstring(L, product->title); + lua_setfield(L, -2, "title"); + lua_pushstring(L, product->description); + lua_setfield(L, -2, "description"); + lua_pushnumber(L, product->price); + lua_setfield(L, -2, "price"); + lua_pushstring(L, product->price_string); + lua_setfield(L, -2, "price_string"); + lua_pushstring(L, product->currency_code); + lua_setfield(L, -2, "currency_code"); lua_rawset(L, -3); + + IAP_FreeProduct(product); } lua_pushnil(L); int ret = lua_pcall(L, 3, 0, 0); if (ret != 0) { - dmLogError("Error running callback: %s", lua_tostring(L, -1)); + dmLogError("%d: Error running callback: %s", __LINE__, lua_tostring(L, -1)); lua_pop(L, 1); } - dmScript::Unref(L, LUA_REGISTRYINDEX, g_IAP.m_Callback); - dmScript::Unref(L, LUA_REGISTRYINDEX, g_IAP.m_Self); - g_IAP.m_Callback = LUA_NOREF; - g_IAP.m_Self = LUA_NOREF; + IAP_UnregisterCallback(&cmd->m_Callback); + delete response; assert(top == lua_gettop(L)); } - (void)request:(SKRequest *)request didFailWithError:(NSError *)error{ dmLogWarning("SKProductsRequest failed: %s", [error.localizedDescription UTF8String]); - lua_State* L = self.m_LuaState; - int top = lua_gettop(L); - - if (g_IAP.m_Callback == LUA_NOREF) { + IAPListener callback = self.m_Callback; + if (!IAP_CallbackIsValid(&callback)) { dmLogError("No callback set"); return; } - lua_rawgeti(L, LUA_REGISTRYINDEX, g_IAP.m_Callback); + IAPResponse* response = new IAPResponse; + response->error = strdup([error.localizedDescription UTF8String]); + response->error_code = REASON_UNSPECIFIED; - // Setup self - lua_rawgeti(L, LUA_REGISTRYINDEX, g_IAP.m_Self); - lua_pushvalue(L, -1); - dmScript::SetInstance(L); + IAPCommand cmd; + cmd.m_Command = IAP_PRODUCT_RESULT; + cmd.m_Callback = callback; + cmd.m_Data = response; - if (!dmScript::IsInstanceValid(L)) - { - dmLogError("Could not run iap callback because the instance has been deleted."); - lua_pop(L, 2); - assert(top == lua_gettop(L)); - return; - } - - lua_pushnil(L); - IAP_PushError(L, [error.localizedDescription UTF8String], REASON_UNSPECIFIED); - - int ret = lua_pcall(L, 3, 0, 0); - if (ret != 0) { - dmLogError("Error running callback: %s", lua_tostring(L, -1)); - lua_pop(L, 1); - } - - dmScript::Unref(L, LUA_REGISTRYINDEX, g_IAP.m_Callback); - dmScript::Unref(L, LUA_REGISTRYINDEX, g_IAP.m_Self); - g_IAP.m_Callback = LUA_NOREF; - g_IAP.m_Self = LUA_NOREF; - - assert(top == lua_gettop(L)); + IAP_Queue_Push(&g_IAP.m_CommandQueue, &cmd); } - (void)requestDidFinish:(SKRequest *)request @@ -186,202 +229,157 @@ IAP g_IAP; @end -static void PushTransaction(lua_State* L, SKPaymentTransaction* transaction) +static void CopyTransaction(SKPaymentTransaction* transaction, IAPTransaction* out) { - lua_newtable(L); - - lua_pushstring(L, "ident"); - lua_pushstring(L, [transaction.payment.productIdentifier UTF8String]); - lua_rawset(L, -3); - - lua_pushstring(L, "state"); - lua_pushnumber(L, transaction.transactionState); - lua_rawset(L, -3); - - if (transaction.transactionState == SKPaymentTransactionStatePurchased || transaction.transactionState == SKPaymentTransactionStateRestored) { - lua_pushstring(L, "trans_ident"); - lua_pushstring(L, [transaction.transactionIdentifier UTF8String]); - lua_rawset(L, -3); - } - - if (transaction.transactionState == SKPaymentTransactionStatePurchased) { - lua_pushstring(L, "receipt"); - if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1) { - lua_pushlstring(L, (const char*) transaction.transactionReceipt.bytes, transaction.transactionReceipt.length); - } else { - NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; - NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL]; - lua_pushlstring(L, (const char*) receiptData.bytes, receiptData.length); - } - lua_rawset(L, -3); + if (transaction.transactionState == SKPaymentTransactionStateFailed) { + out->error = strdup([transaction.error.localizedDescription UTF8String]); + out->error_code = transaction.error.code == SKErrorPaymentCancelled ? REASON_USER_CANCELED : REASON_UNSPECIFIED; + } else { + out->error = 0; } NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease]; [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZ"]; - lua_pushstring(L, "date"); - lua_pushstring(L, [[dateFormatter stringFromDate: transaction.transactionDate] UTF8String]); - lua_rawset(L, -3); + + out->ident = strdup([transaction.payment.productIdentifier UTF8String]); + + + if (transaction.transactionDate) + out->date = strdup([[dateFormatter stringFromDate: transaction.transactionDate] UTF8String]); + out->state = transaction.transactionState; + + if (transaction.transactionState == SKPaymentTransactionStatePurchased || + transaction.transactionState == SKPaymentTransactionStateRestored) { + out->trans_ident = strdup([transaction.transactionIdentifier UTF8String]); + } + + if (transaction.transactionState == SKPaymentTransactionStatePurchased) { + NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; + NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL]; + out->receipt_length = receiptData.length; + out->receipt = (const char*)malloc(out->receipt_length); + memcpy((void*)out->receipt, receiptData.bytes, out->receipt_length); + } if (transaction.transactionState == SKPaymentTransactionStateRestored && transaction.originalTransaction) { + out->m_OriginalTransaction = new IAPTransaction; + CopyTransaction(transaction.originalTransaction, out->m_OriginalTransaction); + } +} + +static void PushTransaction(lua_State* L, IAPTransaction* transaction) +{ + // first argument to the callback + lua_newtable(L); + + lua_pushstring(L, transaction->ident); + lua_setfield(L, -2, "ident"); + lua_pushnumber(L, transaction->state); + lua_setfield(L, -2, "state"); + if (transaction->date) { + lua_pushstring(L, transaction->date); + lua_setfield(L, -2, "date"); + } + + if (transaction->trans_ident) { + lua_pushstring(L, transaction->trans_ident); + lua_setfield(L, -2, "trans_ident"); + } + if (transaction->receipt) { + lua_pushlstring(L, transaction->receipt, transaction->receipt_length); + lua_setfield(L, -2, "receipt"); + } + + if (transaction->m_OriginalTransaction) { lua_pushstring(L, "original_trans"); - PushTransaction(L, transaction.originalTransaction); + PushTransaction(L, transaction->m_OriginalTransaction); lua_rawset(L, -3); } } -void RunTransactionCallback(lua_State* L, int cb, int self, SKPaymentTransaction* transaction) + +static void HandlePurchaseResult(IAPCommand* cmd) { + + IAPTransaction* transaction = (IAPTransaction*)cmd->m_Data; + + lua_State* L = cmd->m_Callback.m_L; int top = lua_gettop(L); - lua_rawgeti(L, LUA_REGISTRYINDEX, cb); - - // Setup self - lua_rawgeti(L, LUA_REGISTRYINDEX, self); - lua_pushvalue(L, -1); - dmScript::SetInstance(L); - - if (!dmScript::IsInstanceValid(L)) + if (!IAP_SetupCallback(&cmd->m_Callback)) { - dmLogError("Could not run iap callback because the instance has been deleted."); - lua_pop(L, 2); assert(top == lua_gettop(L)); return; } PushTransaction(L, transaction); - if (transaction.transactionState == SKPaymentTransactionStateFailed) { - if (transaction.error.code == SKErrorPaymentCancelled) { - IAP_PushError(L, [transaction.error.localizedDescription UTF8String], REASON_USER_CANCELED); - } else { - IAP_PushError(L, [transaction.error.localizedDescription UTF8String], REASON_UNSPECIFIED); - } + // Second argument to callback + if (transaction->error) { + IAP_PushError(L, transaction->error, transaction->error_code); } else { lua_pushnil(L); } int ret = lua_pcall(L, 3, 0, 0); if (ret != 0) { - dmLogError("Error running callback: %s", lua_tostring(L, -1)); + dmLogError("%d: Error running callback: %s", __LINE__, lua_tostring(L, -1)); lua_pop(L, 1); } + IAP_FreeTransaction(transaction); + + delete transaction; assert(top == lua_gettop(L)); } @implementation SKPaymentTransactionObserver - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { - for (SKPaymentTransaction * transaction in transactions) { + for (SKPaymentTransaction* transaction in transactions) { - if ((!g_IAP.m_AutoFinishTransactions) && (transaction.transactionState == SKPaymentTransactionStatePurchased)) { + if ((!self.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]); - [g_IAP.m_PendingTransactions setObject:transaction forKey:[NSNumber numberWithInteger:trans_id_hash] ]; + [self.m_IAP->m_PendingTransactions setObject:transaction forKey:[NSNumber numberWithInteger:trans_id_hash] ]; } - bool has_listener = false; - if (self.m_IAP->m_Listener.m_Callback != LUA_NOREF) { - const IAPListener& l = self.m_IAP->m_Listener; - RunTransactionCallback(l.m_L, l.m_Callback, l.m_Self, transaction); - has_listener = true; - } + bool has_listener = self.m_IAP->m_Listener.m_Callback != LUA_NOREF; + if (!has_listener) + continue; + + IAPTransaction* iap_transaction = new IAPTransaction; + CopyTransaction(transaction, iap_transaction); + + IAPCommand cmd; + cmd.m_Callback = self.m_IAP->m_Listener; + cmd.m_Command = IAP_PURCHASE_RESULT; + cmd.m_Data = iap_transaction; + IAP_Queue_Push(&self.m_IAP->m_CommandQueue, &cmd); switch (transaction.transactionState) { - case SKPaymentTransactionStatePurchasing: - break; case SKPaymentTransactionStatePurchased: - if (has_listener > 0 && g_IAP.m_AutoFinishTransactions) { + if (g_IAP.m_AutoFinishTransactions) { [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; } break; case SKPaymentTransactionStateFailed: - if (has_listener > 0) { - [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; - } + [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; break; case SKPaymentTransactionStateRestored: - if (has_listener > 0) { - [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; - } + [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; + break; + default: break; } } } @end -/*# list in-app products - * - * Get a list of all avaliable iap products. Products are described as a [type:table] - * with the following fields: - * - * `ident` - * : The product identifier. - * - * `title` - * : The product title. - * - * `description` - * : The product description. - * - * `price` - * : The price of the product. - * - * `price_string` - * : The price of the product, as a formatted string (amount and currency symbol). - * - * `currency_code` [icon:ios] [icon:googleplay] [icon:facebook] - * : The currency code. On Google Play, this reflects the merchant's locale, instead of the user's. - * - * [icon:attention] Nested calls, that is calling `iap.list()` from within the callback is - * not supported. Doing so will result in call being ignored with the engine reporting - * "Unexpected callback set". - * - * @name iap.list - * @param ids [type:table] table (array) of identifiers to get products from - * @param callback [type:function(self, products, error)] result callback - * - * `self` - * : [type:object] The current object. - * - * `products` - * : [type:table] Table describing the available iap products. See above for details. - * - * `error` - * : [type:table] a table containing error information. `nil` if there is no error. - * - `error` (the error message) - * - * @examples - * - * ```lua - * local function iap_callback(self, products, error) - * if error == nil then - * for k,p in pairs(products) do - * -- present the product - * print(p.title) - * print(p.description) - * end - * else - * print(error.error) - * end - * end - * - * function init(self) - * iap.list({"my_iap"}, iap_callback) - * end - * ``` - */ -int IAP_List(lua_State* L) +static int IAP_List(lua_State* L) { int top = lua_gettop(L); - if (g_IAP.m_Callback != LUA_NOREF) { - dmLogError("Unexpected callback set"); - dmScript::Unref(L, LUA_REGISTRYINDEX, g_IAP.m_Callback); - dmScript::Unref(L, LUA_REGISTRYINDEX, g_IAP.m_Self); - g_IAP.m_Callback = LUA_NOREF; - g_IAP.m_Self = LUA_NOREF; - } NSCountedSet* product_identifiers = [[[NSCountedSet alloc] init] autorelease]; @@ -393,16 +391,13 @@ int IAP_List(lua_State* L) lua_pop(L, 1); } - luaL_checktype(L, 2, LUA_TFUNCTION); - lua_pushvalue(L, 2); - g_IAP.m_Callback = dmScript::Ref(L, LUA_REGISTRYINDEX); - - dmScript::GetInstance(L); - g_IAP.m_Self = dmScript::Ref(L, LUA_REGISTRYINDEX); - SKProductsRequest* products_request = [[SKProductsRequest alloc] initWithProductIdentifiers: product_identifiers]; SKProductsRequestDelegate* delegate = [SKProductsRequestDelegate alloc]; - delegate.m_LuaState = dmScript::GetMainThread(L); + + IAPListener callback; + IAP_RegisterCallback(L, 2, &callback); + delegate.m_Callback = callback; + delegate.m_Request = products_request; products_request.delegate = delegate; [products_request start]; @@ -411,44 +406,7 @@ int IAP_List(lua_State* L) return 0; } -/*# buy product - * - * Perform a product purchase. - * - * [icon:attention] Calling `iap.finish()` is required on a successful transaction if - * `auto_finish_transactions` is disabled in project settings. - * - * @name iap.buy - * @param id [type:string] product to buy - * @param [options] [type:table] optional parameters as properties. The following parameters can be set: - * - * - `request_id` ([icon:facebook] Facebook only. Optional custom unique request id to - * set for this transaction. The id becomes attached to the payment within the Graph API.) - * - * @examples - * - * ```lua - * local function iap_listener(self, transaction, error) - * if error == nil then - * -- purchase is successful. - * print(transaction.date) - * -- required if auto finish transactions is disabled in project settings - * if (transaction.state == iap.TRANS_STATE_PURCHASED) then - * -- do server-side verification of purchase here.. - * iap.finish(transaction) - * end - * else - * print(error.error, error.reason) - * end - * end - * - * function init(self) - * iap.set_listener(iap_listener) - * iap.buy("my_iap") - * end - * ``` - */ -int IAP_Buy(lua_State* L) +static int IAP_Buy(lua_State* L) { int top = lua_gettop(L); @@ -464,20 +422,7 @@ int IAP_Buy(lua_State* L) return 0; } -/*# finish buying product - * - * Explicitly finish a product transaction. - * - * [icon:attention] Calling iap.finish is required on a successful transaction - * if `auto_finish_transactions` is disabled in project settings. Calling this function - * with `auto_finish_transactions` set will be ignored and a warning is printed. - * The `transaction.state` field must equal `iap.TRANS_STATE_PURCHASED`. - * - * @name iap.finish - * @param transaction [type:table] transaction table parameter as supplied in listener callback - * - */ -int IAP_Finish(lua_State* L) +static int IAP_Finish(lua_State* L) { if(g_IAP.m_AutoFinishTransactions) { @@ -525,15 +470,7 @@ int IAP_Finish(lua_State* L) return 0; } -/*# restore products (non-consumable) - * - * Restore previously purchased products. - * - * @name iap.restore - * @return success [type:boolean] `true` if current store supports handling - * restored transactions, otherwise `false`. - */ -int IAP_Restore(lua_State* L) +static int IAP_Restore(lua_State* L) { // TODO: Missing callback here for completion/error // See callback under "Handling Restored Transactions" @@ -545,88 +482,12 @@ int IAP_Restore(lua_State* L) return 1; } -/*# set purchase transaction listener - * - * Set the callback function to receive purchase transaction events. Transactions are - * described as a [type:table] with the following fields: - * - * `ident` - * : The product identifier. - * - * `state` - * : The transaction state. See `iap.TRANS_STATE_*`. - * - * `date` - * : The date and time for the transaction. - * - * `trans_ident` - * : The transaction identifier. This field is only set when `state` is TRANS_STATE_RESTORED, - * TRANS_STATE_UNVERIFIED or TRANS_STATE_PURCHASED. - * - * `receipt` - * : The transaction receipt. This field is only set when `state` is TRANS_STATE_PURCHASED - * or TRANS_STATE_UNVERIFIED. - * - * `original_trans` [icon:apple] - * : Apple only. The original transaction. This field is only set when `state` is - * TRANS_STATE_RESTORED. - * - * `signature` [icon:googleplay] - * : Google Play only. A string containing the signature of the purchase data that was signed - * with the private key of the developer. - * - * `request_id` [icon:facebook] - * : Facebook only. This field is set to the optional custom unique request id `request_id` - * if set in the `iap.buy()` call parameters. - * - * `user_id` [icon:amazon] - * : Amazon Pay only. The user ID. - * - * `is_sandbox_mode` [icon:amazon] - * : Amazon Pay only. If `true`, the SDK is running in Sandbox mode. This only allows - * interactions with the Amazon AppTester. Use this mode only for testing locally. - * - * `cancel_date` [icon:amazon] - * : Amazon Pay only. The cancel date for the purchase. This field is only set if the - * purchase is canceled. - * - * `canceled` [icon:amazon] - * : Amazon Pay only. Is set to `true` if the receipt was canceled or has expired; - * otherwise `false`. - * - * @name iap.set_listener - * @param listener [type:function(self, transaction, error)] listener callback function. - * Pass an empty function if you no longer wish to receive callbacks. - * - * `self` - * : [type:object] The current object. - * - * `transaction` - * : [type:table] a table describing the transaction. See above for details. - * - * `error` - * : [type:table] a table containing error information. `nil` if there is no error. - * - `error` (the error message) - * - `reason` (the reason for the error, see `iap.REASON_*`) - * - */ -int IAP_SetListener(lua_State* L) +static int IAP_SetListener(lua_State* L) { IAP* iap = &g_IAP; - luaL_checktype(L, 1, LUA_TFUNCTION); - lua_pushvalue(L, 1); - int cb = dmScript::Ref(L, LUA_REGISTRYINDEX); - if (iap->m_Listener.m_Callback != LUA_NOREF) { - dmScript::Unref(iap->m_Listener.m_L, LUA_REGISTRYINDEX, iap->m_Listener.m_Callback); - dmScript::Unref(iap->m_Listener.m_L, LUA_REGISTRYINDEX, iap->m_Listener.m_Self); - } - - iap->m_Listener.m_L = dmScript::GetMainThread(L); - iap->m_Listener.m_Callback = cb; - - dmScript::GetInstance(L); - iap->m_Listener.m_Self = dmScript::Ref(L, LUA_REGISTRYINDEX); + IAP_UnregisterCallback(&iap->m_Listener); + IAP_RegisterCallback(L, 1, &iap->m_Listener); if (g_IAP.m_Observer == 0) { SKPaymentTransactionObserver* observer = [[SKPaymentTransactionObserver alloc] init]; @@ -642,18 +503,7 @@ int IAP_SetListener(lua_State* L) return 0; } -/*# get current provider id - * - * @name iap.get_provider_id - * @return id [type:constant] provider id. - * - * - `iap.PROVIDER_ID_GOOGLE` - * - `iap.PROVIDER_ID_AMAZON` - * - `iap.PROVIDER_ID_APPLE` - * - `iap.PROVIDER_ID_FACEBOOK` - * - */ -int IAP_GetProviderId(lua_State* L) +static int IAP_GetProviderId(lua_State* L) { lua_pushinteger(L, PROVIDER_ID_APPLE); return 1; @@ -670,79 +520,7 @@ static const luaL_reg IAP_methods[] = {0, 0} }; -/*# transaction purchasing state - * - * This is an intermediate mode followed by TRANS_STATE_PURCHASED. - * Store provider support dependent. - * - * @name iap.TRANS_STATE_PURCHASING - * @variable - */ - -/*# transaction purchased state - * - * @name iap.TRANS_STATE_PURCHASED - * @variable - */ - -/*# transaction unverified state, requires verification of purchase - * - * @name iap.TRANS_STATE_UNVERIFIED - * @variable - */ - -/*# transaction failed state - * - * @name iap.TRANS_STATE_FAILED - * @variable - */ - -/*# transaction restored state - * - * This is only available on store providers supporting restoring purchases. - * - * @name iap.TRANS_STATE_RESTORED - * @variable - */ - -/*# unspecified error reason - * - * @name iap.REASON_UNSPECIFIED - * @variable - */ - -/*# user canceled reason - * - * @name iap.REASON_USER_CANCELED - * @variable - */ - - -/*# iap provider id for Google - * - * @name iap.PROVIDER_ID_GOOGLE - * @variable - */ - -/*# provider id for Amazon - * - * @name iap.PROVIDER_ID_AMAZON - * @variable - */ - -/*# provider id for Apple - * - * @name iap.PROVIDER_ID_APPLE - * @variable - */ - -/*# provider id for Facebook - * - * @name iap.PROVIDER_ID_FACEBOOK - * @variable - */ - -dmExtension::Result InitializeIAP(dmExtension::Params* params) +static dmExtension::Result InitializeIAP(dmExtension::Params* params) { // TODO: Life-cycle managaemnt is *budget*. No notion of "static initalization" // Extend extension functionality with per system initalization? @@ -752,37 +530,55 @@ dmExtension::Result InitializeIAP(dmExtension::Params* params) } g_IAP.m_InitCount++; + IAP_Queue_Create(&g_IAP.m_CommandQueue); lua_State*L = params->m_L; int top = lua_gettop(L); luaL_register(L, LIB_NAME, IAP_methods); // ensure ios payment constants values corresponds to iap constants. - assert(TRANS_STATE_PURCHASING == SKPaymentTransactionStatePurchasing); - assert(TRANS_STATE_PURCHASED == SKPaymentTransactionStatePurchased); - assert(TRANS_STATE_FAILED == SKPaymentTransactionStateFailed); - assert(TRANS_STATE_RESTORED == SKPaymentTransactionStateRestored); + assert((int)TRANS_STATE_PURCHASING == (int)SKPaymentTransactionStatePurchasing); + assert((int)TRANS_STATE_PURCHASED == (int)SKPaymentTransactionStatePurchased); + assert((int)TRANS_STATE_FAILED == (int)SKPaymentTransactionStateFailed); + assert((int)TRANS_STATE_RESTORED == (int)SKPaymentTransactionStateRestored); IAP_PushConstants(L); lua_pop(L, 1); assert(top == lua_gettop(L)); - return dmExtension::RESULT_OK; } -dmExtension::Result FinalizeIAP(dmExtension::Params* params) +static void IAP_OnCommand(IAPCommand* cmd, void*) +{ + switch (cmd->m_Command) + { + case IAP_PRODUCT_RESULT: + HandleProductResult(cmd); + break; + case IAP_PURCHASE_RESULT: + HandlePurchaseResult(cmd); + break; + + default: + assert(false); + } +} + +static dmExtension::Result UpdateIAP(dmExtension::Params* params) +{ + IAP_Queue_Flush(&g_IAP.m_CommandQueue, IAP_OnCommand, 0); + return dmExtension::RESULT_OK; +} + +static dmExtension::Result FinalizeIAP(dmExtension::Params* params) { --g_IAP.m_InitCount; // TODO: Should we support one listener per lua-state? // Or just use a single lua-state...? - if (params->m_L == g_IAP.m_Listener.m_L && g_IAP.m_Listener.m_Callback != LUA_NOREF) { - dmScript::Unref(g_IAP.m_Listener.m_L, LUA_REGISTRYINDEX, g_IAP.m_Listener.m_Callback); - dmScript::Unref(g_IAP.m_Listener.m_L, LUA_REGISTRYINDEX, g_IAP.m_Listener.m_Self); - g_IAP.m_Listener.m_L = 0; - g_IAP.m_Listener.m_Callback = LUA_NOREF; - g_IAP.m_Listener.m_Self = LUA_NOREF; + if (params->m_L == g_IAP.m_Listener.m_L) { + IAP_UnregisterCallback(&g_IAP.m_Listener); } if (g_IAP.m_InitCount == 0) { @@ -797,10 +593,13 @@ dmExtension::Result FinalizeIAP(dmExtension::Params* params) g_IAP.m_Observer = 0; } } + + IAP_Queue_Destroy(&g_IAP.m_CommandQueue); + return dmExtension::RESULT_OK; } -DM_DECLARE_EXTENSION(IAPExt, "IAP", 0, 0, InitializeIAP, 0, 0, FinalizeIAP) +DM_DECLARE_EXTENSION(IAPExt, "IAP", 0, 0, InitializeIAP, UpdateIAP, 0, FinalizeIAP) #endif // DM_PLATFORM_IOS diff --git a/extension-iap/src/iap_private.cpp b/extension-iap/src/iap_private.cpp index fad4ed6..a295f48 100644 --- a/extension-iap/src/iap_private.cpp +++ b/extension-iap/src/iap_private.cpp @@ -7,6 +7,65 @@ #include #include + +IAPListener::IAPListener() { + IAP_ClearCallback(this); +} + +void IAP_ClearCallback(IAPListener* callback) +{ + callback->m_L = 0; + callback->m_Callback = LUA_NOREF; + callback->m_Self = LUA_NOREF; +} + +void IAP_RegisterCallback(lua_State* L, int index, IAPListener* callback) +{ + luaL_checktype(L, index, LUA_TFUNCTION); + lua_pushvalue(L, index); + callback->m_Callback = dmScript::Ref(L, LUA_REGISTRYINDEX); + + dmScript::GetInstance(L); + callback->m_Self = dmScript::Ref(L, LUA_REGISTRYINDEX); + + callback->m_L = L; +} + +void IAP_UnregisterCallback(IAPListener* callback) +{ + if (LUA_NOREF != callback->m_Callback) + dmScript::Unref(callback->m_L, LUA_REGISTRYINDEX, callback->m_Callback); + if (LUA_NOREF != callback->m_Self) + dmScript::Unref(callback->m_L, LUA_REGISTRYINDEX, callback->m_Self); + callback->m_Callback = LUA_NOREF; + callback->m_Self = LUA_NOREF; + callback->m_L = 0; +} + +bool IAP_SetupCallback(IAPListener* callback) +{ + lua_State* L = callback->m_L; + lua_rawgeti(L, LUA_REGISTRYINDEX, callback->m_Callback); + + // Setup self + lua_rawgeti(L, LUA_REGISTRYINDEX, callback->m_Self); + lua_pushvalue(L, -1); + dmScript::SetInstance(L); + + if (!dmScript::IsInstanceValid(L)) + { + dmLogError("Could not run callback because the instance has been deleted."); + lua_pop(L, 2); + return false; + } + return true; +} + +bool IAP_CallbackIsValid(IAPListener* callback) +{ + return callback != 0 && callback->m_L != 0 && callback->m_Callback != LUA_NOREF && callback->m_Self != LUA_NOREF; +} + // Creates a comma separated string, given a table where all values are strings (or numbers) // Returns a malloc'ed string, which the caller must free char* IAP_List_CreateBuffer(lua_State* L) @@ -96,4 +155,43 @@ void IAP_PushConstants(lua_State* L) #undef SETCONSTANT } + +void IAP_Queue_Create(IAPCommandQueue* queue) +{ + queue->m_Mutex = dmMutex::New(); +} + +void IAP_Queue_Destroy(IAPCommandQueue* queue) +{ + dmMutex::Delete(queue->m_Mutex); +} + +void IAP_Queue_Push(IAPCommandQueue* queue, IAPCommand* cmd) +{ + DM_MUTEX_SCOPED_LOCK(queue->m_Mutex); + + if(queue->m_Commands.Full()) + { + queue->m_Commands.OffsetCapacity(2); + } + queue->m_Commands.Push(*cmd); +} + +void IAP_Queue_Flush(IAPCommandQueue* queue, IAPCommandFn fn, void* ctx) +{ + assert(fn != 0); + if (queue->m_Commands.Empty()) + { + return; + } + + DM_MUTEX_SCOPED_LOCK(queue->m_Mutex); + + for(uint32_t i = 0; i != queue->m_Commands.Size(); ++i) + { + fn(&queue->m_Commands[i], ctx); + } + queue->m_Commands.SetSize(0); +} + #endif // DM_PLATFORM_HTML5 || DM_PLATFORM_ANDROID || DM_PLATFORM_IOS diff --git a/extension-iap/src/iap_private.h b/extension-iap/src/iap_private.h index 811ae15..44c0852 100644 --- a/extension-iap/src/iap_private.h +++ b/extension-iap/src/iap_private.h @@ -5,24 +5,62 @@ #include +// TODO: Rename Callback struct IAPListener { - IAPListener() - { - m_L = 0; - m_Callback = LUA_NOREF; - m_Self = LUA_NOREF; - } + IAPListener(); + lua_State* m_L; int m_Callback; int m_Self; }; +enum EIAPCommand +{ + IAP_PRODUCT_RESULT, + IAP_PURCHASE_RESULT, +}; + +struct DM_ALIGNED(16) IAPCommand +{ + IAPCommand() + { + memset(this, 0, sizeof(IAPCommand)); + } + + // Used for storing eventual callback info (if needed) + IAPListener m_Callback; + + // THe actual command payload + int32_t m_Command; + int32_t m_ResponseCode; + void* m_Data; +}; + +struct IAPCommandQueue +{ + dmArray m_Commands; + dmMutex::HMutex m_Mutex; +}; + +void IAP_ClearCallback(IAPListener* callback); +void IAP_RegisterCallback(lua_State* L, int index, IAPListener* callback); +void IAP_UnregisterCallback(IAPListener* callback); +bool IAP_SetupCallback(IAPListener* callback); +bool IAP_CallbackIsValid(IAPListener* callback); char* IAP_List_CreateBuffer(lua_State* L); void IAP_PushError(lua_State* L, const char* error, int reason); void IAP_PushConstants(lua_State* L); +typedef void (*IAPCommandFn)(IAPCommand* cmd, void* ctx); + +void IAP_Queue_Create(IAPCommandQueue* queue); +void IAP_Queue_Destroy(IAPCommandQueue* queue); +// The command is copied by value into the queue +void IAP_Queue_Push(IAPCommandQueue* queue, IAPCommand* cmd); +void IAP_Queue_Flush(IAPCommandQueue* queue, IAPCommandFn fn, void* ctx); + #endif #endif // DM_PLATFORM_HTML5 || DM_PLATFORM_ANDROID || DM_PLATFORM_IOS