Merge pull request #22 from dyukorev/master

Pending transactions fix
This commit is contained in:
Mathias Westerdahl 2020-04-01 10:10:41 +02:00 committed by GitHub
commit e79b5cfb36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 81 additions and 51 deletions

View File

@ -161,6 +161,13 @@
desc: value is `true` if current store supports handling desc: value is `true` if current store supports handling
restored transactions, otherwise `false`. 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 - name: set_listener

View File

@ -34,7 +34,6 @@ struct IAP
bool m_autoFinishTransactions; bool m_autoFinishTransactions;
int m_ProviderId; int m_ProviderId;
dmScript::LuaCallbackInfo* m_ProductCallback;
dmScript::LuaCallbackInfo* m_Listener; dmScript::LuaCallbackInfo* m_Listener;
jobject m_IAP; jobject m_IAP;
@ -51,6 +50,12 @@ struct IAP
static IAP g_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) static int IAP_List(lua_State* L)
{ {
int top = lua_gettop(L); int top = lua_gettop(L);
@ -61,14 +66,13 @@ static int IAP_List(lua_State* L)
return 0; return 0;
} }
if (g_IAP.m_ProductCallback)
dmScript::DestroyCallback(g_IAP.m_ProductCallback);
g_IAP.m_ProductCallback = dmScript::CreateCallback(L, 2);
JNIEnv* env = Attach(); 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); 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); env->DeleteLocalRef(products);
Detach(); Detach();
@ -189,6 +193,7 @@ static const luaL_reg IAP_methods[] =
{"restore", IAP_Restore}, {"restore", IAP_Restore},
{"set_listener", IAP_SetListener}, {"set_listener", IAP_SetListener},
{"get_provider_id", IAP_GetProviderId}, {"get_provider_id", IAP_GetProviderId},
{"process_pending_transactions", IAP_ProcessPendingTransactions},
{0, 0} {0, 0}
}; };
@ -198,7 +203,7 @@ extern "C" {
#endif #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; const char* pl = 0;
if (productList) if (productList)
@ -206,16 +211,14 @@ JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onProductsResult__ILjava_lang_
pl = env->GetStringUTFChars(productList, 0); pl = env->GetStringUTFChars(productList, 0);
} }
IAPCommand cmd; IAPCommand* cmd = (IAPCommand*)cmdHandle;
cmd.m_Callback = g_IAP.m_ProductCallback; cmd->m_ResponseCode = responseCode;
cmd.m_Command = IAP_PRODUCT_RESULT;
cmd.m_ResponseCode = responseCode;
if (pl) if (pl)
{ {
cmd.m_Data = strdup(pl); cmd->m_Data = strdup(pl);
env->ReleaseStringUTFChars(productList, 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) 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::TeardownCallback(cmd->m_Callback);
dmScript::DestroyCallback(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)); 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); jclass iap_jni_class = (jclass)env->CallObjectMethod(cls, find_class, str_class_name);
env->DeleteLocalRef(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_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_Restore = env->GetMethodID(iap_class, "restore", "(Lcom/defold/iap/IPurchaseListener;)V");
g_IAP.m_Stop = env->GetMethodID(iap_class, "stop", "()V"); g_IAP.m_Stop = env->GetMethodID(iap_class, "stop", "()V");

View File

@ -77,6 +77,11 @@ static void IAPList_Callback(void* luacallback, const char* result_json)
dmScript::TeardownCallback(callback); dmScript::TeardownCallback(callback);
} }
static int IAP_ProcessPendingTransactions(lua_State* L)
{
return 0;
}
static int IAP_List(lua_State* L) static int IAP_List(lua_State* L)
{ {
DM_LUA_STACK_CHECK(L, 0); DM_LUA_STACK_CHECK(L, 0);
@ -210,6 +215,7 @@ static const luaL_reg IAP_methods[] =
{"restore", IAP_Restore}, {"restore", IAP_Restore},
{"set_listener", IAP_SetListener}, {"set_listener", IAP_SetListener},
{"get_provider_id", IAP_GetProviderId}, {"get_provider_id", IAP_GetProviderId},
{"process_pending_transactions", IAP_ProcessPendingTransactions},
{0, 0} {0, 0}
}; };

View File

@ -29,6 +29,7 @@ struct IAP
NSMutableDictionary* m_PendingTransactions; NSMutableDictionary* m_PendingTransactions;
dmScript::LuaCallbackInfo* m_Listener; dmScript::LuaCallbackInfo* m_Listener;
IAPCommandQueue m_CommandQueue; IAPCommandQueue m_CommandQueue;
IAPCommandQueue m_ObservableQueue;
SKPaymentTransactionObserver* m_Observer; SKPaymentTransactionObserver* m_Observer;
}; };
@ -337,28 +338,25 @@ static void HandlePurchaseResult(IAPCommand* cmd)
assert(top == lua_gettop(L)); assert(top == lua_gettop(L));
} }
@implementation SKPaymentTransactionObserver static void processTransactions(IAP* iap, NSArray* transactions) {
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction* transaction in transactions) { for (SKPaymentTransaction* transaction in transactions) {
if ((!iap->m_AutoFinishTransactions) && (transaction.transactionState == SKPaymentTransactionStatePurchased)) {
if ((!self.m_IAP->m_AutoFinishTransactions) && (transaction.transactionState == SKPaymentTransactionStatePurchased)) {
NSData *data = [transaction.transactionIdentifier dataUsingEncoding:NSUTF8StringEncoding]; NSData *data = [transaction.transactionIdentifier dataUsingEncoding:NSUTF8StringEncoding];
uint64_t trans_id_hash = dmHashBuffer64((const char*) [data bytes], [data length]); 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; continue;
IAPTransaction* iap_transaction = new IAPTransaction; IAPTransaction* iap_transaction = new IAPTransaction;
CopyTransaction(transaction, iap_transaction); CopyTransaction(transaction, iap_transaction);
IAPCommand cmd; IAPCommand cmd;
cmd.m_Callback = self.m_IAP->m_Listener; cmd.m_Callback = iap->m_Listener;
cmd.m_Command = IAP_PURCHASE_RESULT; cmd.m_Command = IAP_PURCHASE_RESULT;
cmd.m_Data = iap_transaction; cmd.m_Data = iap_transaction;
IAP_Queue_Push(&self.m_IAP->m_CommandQueue, &cmd); IAP_Queue_Push(&iap->m_ObservableQueue, &cmd);
switch (transaction.transactionState) switch (transaction.transactionState)
{ {
@ -378,6 +376,18 @@ static void HandlePurchaseResult(IAPCommand* cmd)
} }
} }
} }
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 @end
static int IAP_List(lua_State* L) static int IAP_List(lua_State* L)
@ -490,18 +500,6 @@ static int IAP_SetListener(lua_State* L)
dmScript::DestroyCallback(iap->m_Listener); dmScript::DestroyCallback(iap->m_Listener);
iap->m_Listener = dmScript::CreateCallback(L, 1); 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; return 0;
} }
@ -519,6 +517,7 @@ static const luaL_reg IAP_methods[] =
{"restore", IAP_Restore}, {"restore", IAP_Restore},
{"set_listener", IAP_SetListener}, {"set_listener", IAP_SetListener},
{"get_provider_id", IAP_GetProviderId}, {"get_provider_id", IAP_GetProviderId},
{"process_pending_transactions", IAP_ProcessPendingTransactions},
{0, 0} {0, 0}
}; };
@ -533,6 +532,7 @@ static dmExtension::Result InitializeIAP(dmExtension::Params* params)
g_IAP.m_InitCount++; g_IAP.m_InitCount++;
IAP_Queue_Create(&g_IAP.m_CommandQueue); IAP_Queue_Create(&g_IAP.m_CommandQueue);
IAP_Queue_Create(&g_IAP.m_ObservableQueue);
lua_State*L = params->m_L; lua_State*L = params->m_L;
int top = lua_gettop(L); int top = lua_gettop(L);
@ -548,6 +548,12 @@ static dmExtension::Result InitializeIAP(dmExtension::Params* params)
lua_pop(L, 1); lua_pop(L, 1);
assert(top == lua_gettop(L)); 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; return dmExtension::RESULT_OK;
} }
@ -570,6 +576,9 @@ static void IAP_OnCommand(IAPCommand* cmd, void*)
static dmExtension::Result UpdateIAP(dmExtension::Params* params) static dmExtension::Result UpdateIAP(dmExtension::Params* params)
{ {
IAP_Queue_Flush(&g_IAP.m_CommandQueue, IAP_OnCommand, 0); 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; 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_CommandQueue);
IAP_Queue_Destroy(&g_IAP.m_ObservableQueue);
return dmExtension::RESULT_OK; return dmExtension::RESULT_OK;
} }

View File

@ -1,5 +1,5 @@
package com.defold.iap; package com.defold.iap;
public interface IListProductsListener { public interface IListProductsListener {
public void onProductsResult(int resultCode, String productList); public void onProductsResult(int resultCode, String productList, long cmdHandle);
} }

View File

@ -35,6 +35,7 @@ public class IapAmazon implements PurchasingListener {
public static final String TAG = "iap"; public static final String TAG = "iap";
private HashMap<RequestId, IListProductsListener> listProductsListeners; private HashMap<RequestId, IListProductsListener> listProductsListeners;
private HashMap<RequestId, Long> listProductsCommandPtrs;
private HashMap<RequestId, IPurchaseListener> purchaseListeners; private HashMap<RequestId, IPurchaseListener> purchaseListeners;
private Activity activity; private Activity activity;
@ -54,7 +55,7 @@ public class IapAmazon implements PurchasingListener {
public void stop() { 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<String> skuSet = new HashSet<String>(); final Set<String> skuSet = new HashSet<String>();
for (String x : skus.split(",")) { for (String x : skus.split(",")) {
if (x.trim().length() > 0) { if (x.trim().length() > 0) {
@ -71,6 +72,7 @@ public class IapAmazon implements PurchasingListener {
RequestId req = PurchasingService.getProductData(skuSet); RequestId req = PurchasingService.getProductData(skuSet);
if (req != null) { if (req != null) {
listProductsListeners.put(req, listener); listProductsListeners.put(req, listener);
listProductsCommandPtrs.put(req, commandPtr);
} else { } else {
Log.e(TAG, "Did not expect a null requestId"); Log.e(TAG, "Did not expect a null requestId");
} }
@ -150,17 +152,21 @@ public class IapAmazon implements PurchasingListener {
public void onProductDataResponse(ProductDataResponse productDataResponse) { public void onProductDataResponse(ProductDataResponse productDataResponse) {
RequestId reqId = productDataResponse.getRequestId(); RequestId reqId = productDataResponse.getRequestId();
IListProductsListener listener; IListProductsListener listener;
long commadPtr = 0;
synchronized (this.listProductsListeners) { synchronized (this.listProductsListeners) {
listener = this.listProductsListeners.get(reqId); listener = this.listProductsListeners.get(reqId);
commadPtr = this.listProductsCommandPtrs.get(reqId);
this.listProductsListeners.remove(reqId);
this.listProductsCommandPtrs.remove(reqId);
if (listener == null) { if (listener == null) {
Log.e(TAG, "No listener found for request " + reqId.toString()); Log.e(TAG, "No listener found for request " + reqId.toString());
return; return;
} }
this.listProductsListeners.remove(reqId);
} }
if (productDataResponse.getRequestStatus() != ProductDataResponse.RequestStatus.SUCCESSFUL) { if (productDataResponse.getRequestStatus() != ProductDataResponse.RequestStatus.SUCCESSFUL) {
listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null); listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null, commadPtr);
} else { } else {
Map<String, Product> products = productDataResponse.getProductData(); Map<String, Product> products = productDataResponse.getProductData();
try { try {
@ -180,9 +186,9 @@ public class IapAmazon implements PurchasingListener {
} }
data.put(key, item); 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) { } catch (JSONException e) {
listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null); listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null, commadPtr);
} }
} }
} }

View File

@ -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<String> skuList = new ArrayList<String>(); ArrayList<String> skuList = new ArrayList<String>();
for (String x : skus.split(",")) { for (String x : skus.split(",")) {
if (x.trim().length() > 0) { if (x.trim().length() > 0) {
@ -276,15 +276,15 @@ public class IapGooglePlay implements Handler.Callback {
products.put(key, convertProduct(product)); products.put(key, convertProduct(product));
} }
} }
listener.onProductsResult(resultCode, products.toString()); listener.onProductsResult(resultCode, products.toString(), commandPtr);
} }
catch(JSONException e) { catch(JSONException e) {
Log.wtf(TAG, "Failed to convert products", e); Log.wtf(TAG, "Failed to convert products", e);
listener.onProductsResult(resultCode, null); listener.onProductsResult(resultCode, null, commandPtr);
} }
} }
else { else {
listener.onProductsResult(resultCode, null); listener.onProductsResult(resultCode, null, commandPtr);
} }
} }
})); }));

View File

@ -23,7 +23,7 @@ public class IapJNI implements IListProductsListener, IPurchaseListener {
} }
@Override @Override
public native void onProductsResult(int responseCode, String productList); public native void onProductsResult(int responseCode, String productList, long cmdHandle);
@Override @Override
public native void onPurchaseResult(int responseCode, String purchaseData); public native void onPurchaseResult(int responseCode, String purchaseData);