#if defined(DM_PLATFORM_ANDROID) #define EXTENSION_NAME IAPExt #define LIB_NAME "IAPExt" #define MODULE_NAME "IAPExt" #define DLIB_LOG_DOMAIN LIB_NAME #include #include #include #include #include "iap.h" #include "iap_private.h" struct IAP { IAP() { memset(this, 0, sizeof(*this)); } dmScript::LuaCallbackInfo* 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_AcknowledgeTransaction; jmethodID m_FinishTransaction; jmethodID m_SetAccountId; IAPCommandQueue m_CommandQueue; }; static IAP g_IAP; static int IAP_SetAccountId(lua_State* L) { DM_LUA_STACK_CHECK(L, 0); const char* account_id = luaL_checkstring(L, 1); dmAndroid::ThreadAttacher threadAttacher; JNIEnv* env = threadAttacher.GetEnv(); jstring account_id_utf = env->NewStringUTF(account_id); env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_SetAccountId, account_id_utf); env->DeleteLocalRef(account_id_utf); return 0; } 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); return 0; } static int IAP_List(lua_State* L) { DM_LUA_STACK_CHECK(L, 0); char* buf = IAP_List_CreateBuffer(L); if(buf == 0) { return 0; } dmAndroid::ThreadAttacher threadAttacher; JNIEnv* env = threadAttacher.GetEnv(); 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, (jlong)cmd); env->DeleteLocalRef(products); free(buf); return 0; } static int IAP_Buy(lua_State* 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); 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); return 0; } static int IAP_Finish(lua_State* L) { DM_LUA_STACK_CHECK(L, 0); 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); 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); dmAndroid::ThreadAttacher threadAttacher; JNIEnv* env = threadAttacher.GetEnv(); jstring receiptUTF = env->NewStringUTF(receipt); env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_FinishTransaction, receiptUTF, g_IAP.m_IAPJNI); env->DeleteLocalRef(receiptUTF); } return 0; } static int IAP_Acknowledge(lua_State* L) { DM_LUA_STACK_CHECK(L, 0); 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); 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); dmAndroid::ThreadAttacher threadAttacher; JNIEnv* env = threadAttacher.GetEnv(); jstring receiptUTF = env->NewStringUTF(receipt); env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_AcknowledgeTransaction, receiptUTF, g_IAP.m_IAPJNI); env->DeleteLocalRef(receiptUTF); } return 0; } static int IAP_Restore(lua_State* L) { // TODO: Missing callback here for completion/error // See iap_ios.mm DM_LUA_STACK_CHECK(L, 1); dmAndroid::ThreadAttacher threadAttacher; JNIEnv* env = threadAttacher.GetEnv(); env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_Restore, g_IAP.m_IAPJNI); 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; if (iap->m_Listener) { dmScript::DestroyCallback(iap->m_Listener); } iap->m_Listener = dmScript::CreateCallback(L, 1); // On first set listener, trigger process old ones. if (!had_previous) { dmAndroid::ThreadAttacher threadAttacher; JNIEnv* env = threadAttacher.GetEnv(); env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_ProcessPendingConsumables, g_IAP.m_IAPJNI); dmLogInfo("IAP: Processing pending transactions"); } dmLogInfo("IAP listener set"); return 0; } static int IAP_GetProviderId(lua_State* L) { DM_LUA_STACK_CHECK(L, 1); lua_pushinteger(L, PROVIDER_ID_GOOGLE); return 1; } 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}, {"process_pending_transactions", IAP_ProcessPendingTransactions}, {"set_account_id", IAP_SetAccountId}, {0, 0} }; #ifdef __cplusplus extern "C" { #endif JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onProductsResult(JNIEnv* env, jobject, jint responseCode, jstring productList, jlong cmdHandle) { const char* pl = 0; if (productList) { pl = env->GetStringUTFChars(productList, 0); } IAPCommand* cmd = (IAPCommand*)cmdHandle; cmd->m_ResponseCode = responseCode; if (pl) { cmd->m_Data = strdup(pl); env->ReleaseStringUTFChars(productList, pl); } 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) { dmLogInfo("Java_com_defold_iap_IapJNI_onPurchaseResult__ILjava_lang_String_2 %d", (int)responseCode); const char* pd = 0; if (purchaseData) { pd = env->GetStringUTFChars(purchaseData, 0); } IAPCommand cmd; cmd.m_Callback = g_IAP.m_Listener; cmd.m_Command = IAP_PURCHASE_RESULT; cmd.m_ResponseCode = responseCode; if (pd) { cmd.m_Data = strdup(pd); env->ReleaseStringUTFChars(purchaseData, pd); } IAP_Queue_Push(&g_IAP.m_CommandQueue, &cmd); } #ifdef __cplusplus } #endif static void HandleProductResult(const IAPCommand* cmd) { if (cmd->m_Callback == 0) { dmLogWarning("Received product list but no listener was set!"); return; } lua_State* L = dmScript::GetCallbackLuaContext(cmd->m_Callback); int top = lua_gettop(L); if (!dmScript::SetupCallback(cmd->m_Callback)) { assert(top == lua_gettop(L)); return; } if (cmd->m_ResponseCode == BILLING_RESPONSE_RESULT_OK) { const char* json = (const char*)cmd->m_Data; dmScript::JsonToLua(L, json, strlen(json)); // throws lua error if it fails lua_pushnil(L); } else { dmLogError("IAP error %d", cmd->m_ResponseCode); lua_pushnil(L); IAP_PushError(L, "failed to fetch product", REASON_UNSPECIFIED); } dmScript::PCall(L, 3, 0); dmScript::TeardownCallback(cmd->m_Callback); dmScript::DestroyCallback(cmd->m_Callback); assert(top == lua_gettop(L)); } static void HandlePurchaseResult(const IAPCommand* cmd) { if (cmd->m_Callback == 0) { dmLogWarning("Received purchase result but no listener was set!"); return; } lua_State* L = dmScript::GetCallbackLuaContext(cmd->m_Callback); int top = lua_gettop(L); if (!dmScript::SetupCallback(cmd->m_Callback)) { dmLogError("Failed to setup callback for purchase result!"); assert(top == lua_gettop(L)); return; } if (cmd->m_ResponseCode == BILLING_RESPONSE_RESULT_OK) { if (cmd->m_Data != 0) { const char* json = (const char*)cmd->m_Data; dmScript::JsonToLua(L, json, strlen(json)); // throws lua error if it fails lua_pushnil(L); } else { dmLogError("IAP error, purchase response was null"); lua_pushnil(L); IAP_PushError(L, "purchase response was null", REASON_UNSPECIFIED); } } else if (cmd->m_ResponseCode == BILLING_RESPONSE_RESULT_USER_CANCELED) { dmLogInfo("User canceled purchase"); lua_pushnil(L); IAP_PushError(L, "user canceled purchase", REASON_USER_CANCELED); } else { dmLogError("IAP error %d", cmd->m_ResponseCode); lua_pushnil(L); IAP_PushError(L, "failed to buy product", REASON_UNSPECIFIED); } dmScript::PCall(L, 3, 0); dmScript::TeardownCallback(cmd->m_Callback); assert(top == lua_gettop(L)); } static dmExtension::Result InitializeIAP(dmExtension::Params* params) { IAP_Queue_Create(&g_IAP.m_CommandQueue); dmAndroid::ThreadAttacher threadAttacher; JNIEnv* env = threadAttacher.GetEnv(); const char* class_name = "com.defold.iap.IapGooglePlay"; jclass iap_class = dmAndroid::LoadClass(env, class_name); 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;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"); 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"); g_IAP.m_SetAccountId = env->GetMethodID(iap_class, "setAccountId", "(Ljava/lang/String;)V"); jmethodID jni_constructor = env->GetMethodID(iap_class, "", "(Landroid/app/Activity;)V"); g_IAP.m_IAP = env->NewGlobalRef(env->NewObject(iap_class, jni_constructor, threadAttacher.GetActivity()->clazz)); jni_constructor = env->GetMethodID(iap_jni_class, "", "()V"); g_IAP.m_IAPJNI = env->NewGlobalRef(env->NewObject(iap_jni_class, jni_constructor)); lua_State*L = params->m_L; int top = lua_gettop(L); luaL_register(L, LIB_NAME, IAP_methods); IAP_PushConstants(L); lua_pop(L, 1); assert(top == lua_gettop(L)); 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) { IAP_Queue_Flush(&g_IAP.m_CommandQueue, IAP_OnCommand, 0); return dmExtension::RESULT_OK; } static dmExtension::Result FinalizeIAP(dmExtension::Params* params) { IAP_Queue_Destroy(&g_IAP.m_CommandQueue); if (params->m_L == dmScript::GetCallbackLuaContext(g_IAP.m_Listener)) { dmScript::DestroyCallback(g_IAP.m_Listener); g_IAP.m_Listener = 0; } dmAndroid::ThreadAttacher threadAttacher; JNIEnv* env = threadAttacher.GetEnv(); env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_Stop); env->DeleteGlobalRef(g_IAP.m_IAP); env->DeleteGlobalRef(g_IAP.m_IAPJNI); g_IAP.m_IAP = NULL; return dmExtension::RESULT_OK; } DM_DECLARE_EXTENSION(EXTENSION_NAME, LIB_NAME, 0, 0, InitializeIAP, UpdateIAP, 0, FinalizeIAP) #endif //DM_PLATFORM_ANDROID