Refactoring base extension. Removed unneeded logic.
This commit is contained in:
parent
a586e94e48
commit
f20a12ca05
Binary file not shown.
@ -1,169 +0,0 @@
|
|||||||
|
|
||||||
var LibraryFacebookIAP = {
|
|
||||||
$FBinner: {
|
|
||||||
// NOTE: Also defined in iap.h
|
|
||||||
TransactionState :
|
|
||||||
{
|
|
||||||
TRANS_STATE_PURCHASING : 0,
|
|
||||||
TRANS_STATE_PURCHASED : 1,
|
|
||||||
TRANS_STATE_FAILED : 2,
|
|
||||||
TRANS_STATE_RESTORED : 3,
|
|
||||||
TRANS_STATE_UNVERIFIED : 4
|
|
||||||
},
|
|
||||||
|
|
||||||
// NOTE: Also defined in iap.h
|
|
||||||
BillingResponse :
|
|
||||||
{
|
|
||||||
BILLING_RESPONSE_RESULT_OK : 0,
|
|
||||||
BILLING_RESPONSE_RESULT_USER_CANCELED : 1,
|
|
||||||
BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE : 3,
|
|
||||||
BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE : 4,
|
|
||||||
BILLING_RESPONSE_RESULT_DEVELOPER_ERROR : 5,
|
|
||||||
BILLING_RESPONSE_RESULT_ERROR : 6,
|
|
||||||
BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED : 7,
|
|
||||||
BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED : 8
|
|
||||||
},
|
|
||||||
|
|
||||||
// See Facebook definitions https://developers.facebook.com/docs/payments/reference/errorcodes
|
|
||||||
FBPaymentResponse :
|
|
||||||
{
|
|
||||||
FB_PAYMENT_RESPONSE_USERCANCELED : 1383010,
|
|
||||||
FB_PAYMENT_RESPONSE_APPINVALIDITEMPARAM : 1383051
|
|
||||||
},
|
|
||||||
|
|
||||||
http_callback: function(xmlhttp, callback, lua_callback, products, product_ids, product_count, url_index, url_count) {
|
|
||||||
if (xmlhttp.readyState == 4) {
|
|
||||||
if(xmlhttp.status == 200) {
|
|
||||||
var xmlDoc = document.createElement( 'html' );
|
|
||||||
xmlDoc.innerHTML = xmlhttp.responseText;
|
|
||||||
var elements = xmlDoc.getElementsByTagName('meta');
|
|
||||||
|
|
||||||
var productInfo = {};
|
|
||||||
for (var i=0; i<elements.length; i++) {
|
|
||||||
if(elements[i].getAttribute("property") == "og:url") {
|
|
||||||
productInfo.ident = elements[i].getAttribute("content");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if(elements[i].getAttribute("property") == "og:title") {
|
|
||||||
productInfo.title = elements[i].getAttribute("content");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if(elements[i].getAttribute("property") == "og:description") {
|
|
||||||
productInfo.description = elements[i].getAttribute("content");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if(elements[i].getAttribute("property") == "product:price:amount") {
|
|
||||||
productInfo.price = elements[i].getAttribute("content");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if(elements[i].getAttribute("property") == "product:price:currency") {
|
|
||||||
productInfo.currency_code = elements[i].getAttribute("content");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
productInfo.price_string = productInfo.price + productInfo.currency_code;
|
|
||||||
|
|
||||||
products[product_ids[url_index]] = productInfo;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
products[product_ids[url_index]] = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if(url_index == product_count-1) {
|
|
||||||
var productsJSON = JSON.stringify(products);
|
|
||||||
var res_buf = stringToUTF8OnStack(productsJSON);
|
|
||||||
{{{ makeDynCall('vii', 'callback')}}}(lua_callback, res_buf);
|
|
||||||
} else {
|
|
||||||
var xmlhttp = new XMLHttpRequest();
|
|
||||||
xmlhttp.onreadystatechange = function() {
|
|
||||||
FBinner.http_callback(xmlhttp, callback, lua_callback, products, product_ids, product_count, url_index+1);
|
|
||||||
};
|
|
||||||
xmlhttp.open("GET", product_ids[url_index+1], true);
|
|
||||||
xmlhttp.send();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
dmIAPFBList: function(params, callback, lua_callback) {
|
|
||||||
var product_ids = UTF8ToString(params).trim().split(',');
|
|
||||||
var product_count = product_ids.length;
|
|
||||||
if(product_count == 0) {
|
|
||||||
console.log("Calling iap.list with no item id's. Ignored.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
products = {};
|
|
||||||
var xmlhttp = new XMLHttpRequest();
|
|
||||||
xmlhttp.onreadystatechange = function() {
|
|
||||||
FBinner.http_callback(xmlhttp, callback, lua_callback, products, product_ids, product_count, 0);
|
|
||||||
};
|
|
||||||
// chain of async http read of product files
|
|
||||||
xmlhttp.open("GET", product_ids[0], true);
|
|
||||||
xmlhttp.send();
|
|
||||||
},
|
|
||||||
|
|
||||||
// https://developers.facebook.com/docs/javascript/reference/FB.ui
|
|
||||||
dmIAPFBBuy: function(param_product_id, param_request_id, callback, lua_callback) {
|
|
||||||
var product_id = UTF8ToString(param_product_id);
|
|
||||||
|
|
||||||
var buy_params = {
|
|
||||||
method: 'pay',
|
|
||||||
action: 'purchaseitem',
|
|
||||||
product: product_id,
|
|
||||||
};
|
|
||||||
if(param_request_id != 0) {
|
|
||||||
buy_params.request_id = UTF8ToString(param_request_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
FB.ui(buy_params,
|
|
||||||
function(response) {
|
|
||||||
if(response && response.status) {
|
|
||||||
var result = {};
|
|
||||||
result.ident = product_id;
|
|
||||||
var currentDate = new Date();
|
|
||||||
result.date = currentDate.toISOString();
|
|
||||||
|
|
||||||
if (response.status == 'initiated') {
|
|
||||||
result.state = FBinner.TransactionState.TRANS_STATE_UNVERIFIED;
|
|
||||||
result.trans_ident = response.payment_id.toString();
|
|
||||||
result.receipt = response.signed_request;
|
|
||||||
result.request_id = response.request_id;
|
|
||||||
} else if (response.status == 'completed') {
|
|
||||||
result.state = FBinner.TransactionState.TRANS_STATE_PURCHASED;
|
|
||||||
result.trans_ident = response.payment_id.toString();
|
|
||||||
result.receipt = response.signed_request;
|
|
||||||
result.request_id = response.request_id;
|
|
||||||
} else {
|
|
||||||
// unknown and 'failed' state
|
|
||||||
if (response.status != 'failed') {
|
|
||||||
console.log("Unknown response status (default to 'failed'): ", response.status);
|
|
||||||
}
|
|
||||||
result.state = FBinner.TransactionState.TRANS_STATE_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
var productsJSON = JSON.stringify(result)
|
|
||||||
var res_buf = stringToUTF8OnStack(productsJSON);
|
|
||||||
{{{ makeDynCall('viii', 'callback')}}}(lua_callback, res_buf, 0);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
var reason;
|
|
||||||
if(!response || response.error_code == FBinner.FBPaymentResponse.FB_PAYMENT_RESPONSE_USERCANCELED) {
|
|
||||||
reason = FBinner.BillingResponse.BILLING_RESPONSE_RESULT_USER_CANCELED;
|
|
||||||
} else if (response.error_code == FBinner.FBPaymentResponse.FB_PAYMENT_RESPONSE_APPINVALIDITEMPARAM) {
|
|
||||||
reason = FBinner.BillingResponse.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED;
|
|
||||||
} else {
|
|
||||||
reason = FBinner.BillingResponse.BILLING_RESPONSE_RESULT_ERROR;
|
|
||||||
console.log("Unknown response: ", response);
|
|
||||||
}
|
|
||||||
{{{ makeDynCall('viii', 'callback')}}}(lua_callback, 0, reason);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
autoAddDeps(LibraryFacebookIAP, '$FBinner');
|
|
||||||
addToLibrary(LibraryFacebookIAP);
|
|
@ -1,4 +1,4 @@
|
|||||||
#if defined(DM_PLATFORM_ANDROID)
|
// #if defined(DM_PLATFORM_ANDROID)
|
||||||
|
|
||||||
#include <dmsdk/sdk.h>
|
#include <dmsdk/sdk.h>
|
||||||
#include <dmsdk/dlib/android.h>
|
#include <dmsdk/dlib/android.h>
|
||||||
@ -396,13 +396,6 @@ static dmExtension::Result InitializeIAP(dmExtension::Params* params)
|
|||||||
const char* class_name = "com.defold.iap.IapGooglePlay";
|
const char* class_name = "com.defold.iap.IapGooglePlay";
|
||||||
|
|
||||||
g_IAP.m_ProviderId = PROVIDER_ID_GOOGLE;
|
g_IAP.m_ProviderId = PROVIDER_ID_GOOGLE;
|
||||||
if (!strcmp(provider, "Amazon")) {
|
|
||||||
g_IAP.m_ProviderId = PROVIDER_ID_AMAZON;
|
|
||||||
class_name = "com.defold.iap.IapAmazon";
|
|
||||||
}
|
|
||||||
else if (strcmp(provider, "GooglePlay")) {
|
|
||||||
dmLogWarning("Unknown IAP provider name [%s], defaulting to GooglePlay", provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
jclass iap_class = dmAndroid::LoadClass(env, class_name);
|
jclass iap_class = dmAndroid::LoadClass(env, class_name);
|
||||||
jclass iap_jni_class = dmAndroid::LoadClass(env, "com.defold.iap.IapJNI");
|
jclass iap_jni_class = dmAndroid::LoadClass(env, "com.defold.iap.IapJNI");
|
||||||
|
@ -1,234 +0,0 @@
|
|||||||
#if defined(DM_PLATFORM_HTML5)
|
|
||||||
|
|
||||||
#include <dmsdk/sdk.h>
|
|
||||||
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
#include "iap.h"
|
|
||||||
#include "iap_private.h"
|
|
||||||
|
|
||||||
#define LIB_NAME "iap"
|
|
||||||
|
|
||||||
struct IAP
|
|
||||||
{
|
|
||||||
IAP()
|
|
||||||
{
|
|
||||||
memset(this, 0, sizeof(*this));
|
|
||||||
m_autoFinishTransactions = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
dmScript::LuaCallbackInfo* m_Listener;
|
|
||||||
int m_InitCount;
|
|
||||||
bool m_autoFinishTransactions;
|
|
||||||
} g_IAP;
|
|
||||||
|
|
||||||
typedef void (*OnIAPFBList)(void* luacallback, const char* json);
|
|
||||||
typedef void (*OnIAPFBListenerCallback)(void* luacallback, const char* json, int error_code);
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
// Implementation in library_facebook_iap.js
|
|
||||||
void dmIAPFBList(const char* item_ids, OnIAPFBList callback, dmScript::LuaCallbackInfo* luacallback);
|
|
||||||
void dmIAPFBBuy(const char* item_id, const char* request_id, OnIAPFBListenerCallback callback, dmScript::LuaCallbackInfo* luacallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void IAPList_Callback(void* luacallback, const char* result_json)
|
|
||||||
{
|
|
||||||
dmScript::LuaCallbackInfo* callback = (dmScript::LuaCallbackInfo*)luacallback;
|
|
||||||
lua_State* L = dmScript::GetCallbackLuaContext(callback);
|
|
||||||
DM_LUA_STACK_CHECK(L, 0);
|
|
||||||
|
|
||||||
if (!dmScript::SetupCallback(callback))
|
|
||||||
{
|
|
||||||
dmScript::DestroyCallback(callback);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(result_json != 0)
|
|
||||||
{
|
|
||||||
dmScript::JsonToLua(L, result_json, strlen(result_json)); // throws lua error if it fails
|
|
||||||
lua_pushnil(L);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
dmLogError("Got empty list result.");
|
|
||||||
lua_pushnil(L);
|
|
||||||
IAP_PushError(L, "Got empty list result.", REASON_UNSPECIFIED);
|
|
||||||
}
|
|
||||||
|
|
||||||
dmScript::PCall(L, 3, 0);
|
|
||||||
|
|
||||||
dmScript::DestroyCallback(callback);
|
|
||||||
dmScript::TeardownCallback(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int IAP_SetAccountId(lua_State* L)
|
|
||||||
{
|
|
||||||
dmLogError("iap.set_account_id is not supported on this platform.");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int IAP_ProcessPendingTransactions(lua_State* L)
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
dmScript::LuaCallbackInfo* callback = dmScript::CreateCallback(L, 2);
|
|
||||||
|
|
||||||
dmIAPFBList(buf, (OnIAPFBList)IAPList_Callback, callback);
|
|
||||||
|
|
||||||
free(buf);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void IAPListener_Callback(void* luacallback, const char* result_json, int error_code)
|
|
||||||
{
|
|
||||||
dmScript::LuaCallbackInfo* callback = (dmScript::LuaCallbackInfo*)luacallback;
|
|
||||||
lua_State* L = dmScript::GetCallbackLuaContext(callback);
|
|
||||||
DM_LUA_STACK_CHECK(L, 0);
|
|
||||||
|
|
||||||
if (!dmScript::SetupCallback(callback))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result_json) {
|
|
||||||
dmScript::JsonToLua(L, result_json, strlen(result_json)); // throws lua error if it fails
|
|
||||||
lua_pushnil(L);
|
|
||||||
} else {
|
|
||||||
lua_pushnil(L);
|
|
||||||
switch(error_code)
|
|
||||||
{
|
|
||||||
case BILLING_RESPONSE_RESULT_USER_CANCELED:
|
|
||||||
IAP_PushError(L, "user canceled purchase", REASON_USER_CANCELED);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED:
|
|
||||||
IAP_PushError(L, "product already owned", REASON_UNSPECIFIED);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
dmLogError("IAP error %d", error_code);
|
|
||||||
IAP_PushError(L, "failed to buy product", REASON_UNSPECIFIED);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dmScript::PCall(L, 3, 0);
|
|
||||||
|
|
||||||
dmScript::TeardownCallback(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static int IAP_Buy(lua_State* L)
|
|
||||||
{
|
|
||||||
DM_LUA_STACK_CHECK(L, 0);
|
|
||||||
|
|
||||||
if (!g_IAP.m_Listener) {
|
|
||||||
dmLogError("No callback set");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int top = lua_gettop(L);
|
|
||||||
const char* id = luaL_checkstring(L, 1);
|
|
||||||
const char* request_id = 0x0;
|
|
||||||
|
|
||||||
if (top >= 2 && lua_istable(L, 2)) {
|
|
||||||
luaL_checktype(L, 2, LUA_TTABLE);
|
|
||||||
lua_pushvalue(L, 2);
|
|
||||||
lua_getfield(L, -1, "request_id");
|
|
||||||
request_id = lua_isnil(L, -1) ? 0x0 : luaL_checkstring(L, -1);
|
|
||||||
lua_pop(L, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
dmIAPFBBuy(id, request_id, (OnIAPFBListenerCallback)IAPListener_Callback, g_IAP.m_Listener);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int IAP_SetListener(lua_State* L)
|
|
||||||
{
|
|
||||||
DM_LUA_STACK_CHECK(L, 0);
|
|
||||||
if (g_IAP.m_Listener)
|
|
||||||
dmScript::DestroyCallback(g_IAP.m_Listener);
|
|
||||||
g_IAP.m_Listener = dmScript::CreateCallback(L, 1);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int IAP_Finish(lua_State* L)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int IAP_Acknowledge(lua_State* L)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int IAP_Restore(lua_State* L)
|
|
||||||
{
|
|
||||||
DM_LUA_STACK_CHECK(L, 1);
|
|
||||||
lua_pushboolean(L, 0);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int IAP_GetProviderId(lua_State* L)
|
|
||||||
{
|
|
||||||
DM_LUA_STACK_CHECK(L, 1);
|
|
||||||
lua_pushinteger(L, PROVIDER_ID_FACEBOOK);
|
|
||||||
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},
|
|
||||||
{0, 0}
|
|
||||||
};
|
|
||||||
|
|
||||||
static dmExtension::Result InitializeIAP(dmExtension::Params* params)
|
|
||||||
{
|
|
||||||
if (g_IAP.m_InitCount == 0) {
|
|
||||||
g_IAP.m_autoFinishTransactions = dmConfigFile::GetInt(params->m_ConfigFile, "iap.auto_finish_transactions", 1) == 1;
|
|
||||||
}
|
|
||||||
g_IAP.m_InitCount++;
|
|
||||||
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 dmExtension::Result FinalizeIAP(dmExtension::Params* params)
|
|
||||||
{
|
|
||||||
--g_IAP.m_InitCount;
|
|
||||||
if (g_IAP.m_Listener && g_IAP.m_InitCount == 0)
|
|
||||||
{
|
|
||||||
dmScript::DestroyCallback(g_IAP.m_Listener);
|
|
||||||
g_IAP.m_Listener = 0;
|
|
||||||
}
|
|
||||||
return dmExtension::RESULT_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
DM_DECLARE_EXTENSION(IAPExt, "IAP", 0, 0, InitializeIAP, 0, 0, FinalizeIAP)
|
|
||||||
|
|
||||||
#endif // DM_PLATFORM_HTML5
|
|
@ -1,323 +0,0 @@
|
|||||||
package com.defold.iap;
|
|
||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.concurrent.ArrayBlockingQueue;
|
|
||||||
import java.util.concurrent.BlockingQueue;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import org.json.JSONArray;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.os.RemoteException;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.amazon.device.iap.PurchasingService;
|
|
||||||
import com.amazon.device.iap.PurchasingListener;
|
|
||||||
import com.amazon.device.iap.model.ProductDataResponse;
|
|
||||||
import com.amazon.device.iap.model.PurchaseUpdatesResponse;
|
|
||||||
import com.amazon.device.iap.model.PurchaseResponse;
|
|
||||||
import com.amazon.device.iap.model.UserDataResponse;
|
|
||||||
import com.amazon.device.iap.model.RequestId;
|
|
||||||
import com.amazon.device.iap.model.Product;
|
|
||||||
import com.amazon.device.iap.model.Receipt;
|
|
||||||
import com.amazon.device.iap.model.UserData;
|
|
||||||
import com.amazon.device.iap.model.FulfillmentResult;
|
|
||||||
|
|
||||||
public class IapAmazon implements PurchasingListener {
|
|
||||||
|
|
||||||
public static final String TAG = "iap";
|
|
||||||
|
|
||||||
private HashMap<RequestId, IListProductsListener> listProductsListeners;
|
|
||||||
private HashMap<RequestId, Long> listProductsCommandPtrs;
|
|
||||||
private HashMap<RequestId, IPurchaseListener> purchaseListeners;
|
|
||||||
|
|
||||||
private Activity activity;
|
|
||||||
private boolean autoFinishTransactions;
|
|
||||||
|
|
||||||
public IapAmazon(Activity activity, boolean autoFinishTransactions) {
|
|
||||||
this.activity = activity;
|
|
||||||
this.autoFinishTransactions = autoFinishTransactions;
|
|
||||||
this.listProductsListeners = new HashMap<RequestId, IListProductsListener>();
|
|
||||||
this.listProductsCommandPtrs = new HashMap<RequestId, Long>();
|
|
||||||
this.purchaseListeners = new HashMap<RequestId, IPurchaseListener>();
|
|
||||||
PurchasingService.registerListener(activity, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void init() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void listItems(final String skus, final IListProductsListener listener, final long commandPtr) {
|
|
||||||
final Set<String> skuSet = new HashSet<String>();
|
|
||||||
for (String x : skus.split(",")) {
|
|
||||||
if (x.trim().length() > 0) {
|
|
||||||
if (!skuSet.contains(x)) {
|
|
||||||
skuSet.add(x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// It might seem unconventional to hold the lock while doing the function call,
|
|
||||||
// but it prevents a race condition, as the API does not allow supplying own
|
|
||||||
// requestId which could be generated ahead of time.
|
|
||||||
synchronized (listProductsListeners) {
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void buy(final String product, final String token, final IPurchaseListener listener) {
|
|
||||||
synchronized (purchaseListeners) {
|
|
||||||
RequestId req = PurchasingService.purchase(product);
|
|
||||||
if (req != null) {
|
|
||||||
purchaseListeners.put(req, listener);
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "Did not expect a null requestId");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void finishTransaction(final String receipt, final IPurchaseListener listener) {
|
|
||||||
if(this.autoFinishTransactions) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
PurchasingService.notifyFulfillment(receipt, FulfillmentResult.FULFILLED);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void acknowledgeTransaction(final String purchaseToken, final IPurchaseListener purchaseListener) {
|
|
||||||
// Stub to prevent errors.
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doGetPurchaseUpdates(final IPurchaseListener listener, final boolean reset) {
|
|
||||||
synchronized (purchaseListeners) {
|
|
||||||
RequestId req = PurchasingService.getPurchaseUpdates(reset);
|
|
||||||
if (req != null) {
|
|
||||||
purchaseListeners.put(req, listener);
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "Did not expect a null requestId");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void processPendingConsumables(final IPurchaseListener listener) {
|
|
||||||
// reset = false means getting any new receipts since the last call.
|
|
||||||
doGetPurchaseUpdates(listener, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void restore(final IPurchaseListener listener) {
|
|
||||||
// reset = true means getting all transaction history, although consumables
|
|
||||||
// are not included, only entitlements, after testing.
|
|
||||||
doGetPurchaseUpdates(listener, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String toISO8601(final Date date) {
|
|
||||||
String formatted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(date);
|
|
||||||
return formatted.substring(0, 22) + ":" + formatted.substring(22);
|
|
||||||
}
|
|
||||||
|
|
||||||
private JSONObject makeTransactionObject(final UserData user, final Receipt receipt, int state) throws JSONException {
|
|
||||||
JSONObject transaction = new JSONObject();
|
|
||||||
transaction.put("ident", receipt.getSku());
|
|
||||||
transaction.put("state", state);
|
|
||||||
transaction.put("date", toISO8601(receipt.getPurchaseDate()));
|
|
||||||
transaction.put("trans_ident", receipt.getReceiptId());
|
|
||||||
transaction.put("receipt", receipt.getReceiptId());
|
|
||||||
|
|
||||||
// Only for amazon (this far), but required for using their server side receipt validation.
|
|
||||||
transaction.put("is_sandbox_mode", PurchasingService.IS_SANDBOX_MODE);
|
|
||||||
transaction.put("user_id", user.getUserId());
|
|
||||||
|
|
||||||
// According to documentation, cancellation support has to be enabled per item, and this is
|
|
||||||
// not officially supported by any other IAP provider, and it is not expected to be used here either.
|
|
||||||
//
|
|
||||||
// But enforcing the use of only non-cancelable items is not possible either; so include these flags
|
|
||||||
// for completeness.
|
|
||||||
if (receipt.getCancelDate() != null)
|
|
||||||
transaction.put("cancel_date", toISO8601(receipt.getCancelDate()));
|
|
||||||
transaction.put("canceled", receipt.isCanceled());
|
|
||||||
return transaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This callback method is invoked when an ProductDataResponse is available for a request initiated by PurchasingService.getProductData(java.util.Set).
|
|
||||||
@Override
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (productDataResponse.getRequestStatus() != ProductDataResponse.RequestStatus.SUCCESSFUL) {
|
|
||||||
listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null, commadPtr);
|
|
||||||
} else {
|
|
||||||
for (final String s : productDataResponse.getUnavailableSkus()) {
|
|
||||||
Log.v(TAG, "Unavailable SKU: " + s);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Product> products = productDataResponse.getProductData();
|
|
||||||
try {
|
|
||||||
JSONArray data = new JSONArray();
|
|
||||||
for (Map.Entry<String, Product> entry : products.entrySet()) {
|
|
||||||
String key = entry.getKey();
|
|
||||||
Product product = entry.getValue();
|
|
||||||
JSONObject item = new JSONObject();
|
|
||||||
item.put("ident", product.getSku());
|
|
||||||
item.put("title", product.getTitle());
|
|
||||||
item.put("description", product.getDescription());
|
|
||||||
if (product.getPrice() != null) {
|
|
||||||
String priceString = product.getPrice();
|
|
||||||
item.put("price_string", priceString);
|
|
||||||
// Based on return values from getPrice: https://developer.amazon.com/public/binaries/content/assets/javadoc/in-app-purchasing-api/com/amazon/inapp/purchasing/item.html
|
|
||||||
item.put("price", priceString.replaceAll("[^0-9.,]", ""));
|
|
||||||
}
|
|
||||||
data.put(item);
|
|
||||||
}
|
|
||||||
listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_OK, data.toString(), commadPtr);
|
|
||||||
} catch (JSONException e) {
|
|
||||||
listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null, commadPtr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convenience function for getting and removing a purchaseListener (used for more than one operation).
|
|
||||||
private IPurchaseListener pickPurchaseListener(RequestId requestId) {
|
|
||||||
synchronized (this.purchaseListeners) {
|
|
||||||
IPurchaseListener listener = this.purchaseListeners.get(requestId);
|
|
||||||
if (listener != null) {
|
|
||||||
this.purchaseListeners.remove(requestId);
|
|
||||||
return listener;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This callback method is invoked when a PurchaseResponse is available for a purchase initiated by PurchasingService.purchase(String).
|
|
||||||
@Override
|
|
||||||
public void onPurchaseResponse(PurchaseResponse purchaseResponse) {
|
|
||||||
|
|
||||||
IPurchaseListener listener = pickPurchaseListener(purchaseResponse.getRequestId());
|
|
||||||
if (listener == null) {
|
|
||||||
Log.e(TAG, "No listener found for request: " + purchaseResponse.getRequestId().toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int code;
|
|
||||||
String data = null;
|
|
||||||
String fulfilReceiptId = null;
|
|
||||||
|
|
||||||
switch (purchaseResponse.getRequestStatus()) {
|
|
||||||
case SUCCESSFUL:
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
code = IapJNI.BILLING_RESPONSE_RESULT_OK;
|
|
||||||
data = makeTransactionObject(purchaseResponse.getUserData(), purchaseResponse.getReceipt(), IapJNI.TRANS_STATE_PURCHASED).toString();
|
|
||||||
fulfilReceiptId = purchaseResponse.getReceipt().getReceiptId();
|
|
||||||
} catch (JSONException e) {
|
|
||||||
Log.e(TAG, "JSON Exception occured: " + e.toString());
|
|
||||||
code = IapJNI.BILLING_RESPONSE_RESULT_DEVELOPER_ERROR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ALREADY_PURCHASED:
|
|
||||||
code = IapJNI.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED;
|
|
||||||
break;
|
|
||||||
case INVALID_SKU:
|
|
||||||
code = IapJNI.BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE;
|
|
||||||
break;
|
|
||||||
case FAILED:
|
|
||||||
case NOT_SUPPORTED:
|
|
||||||
default:
|
|
||||||
code = IapJNI.BILLING_RESPONSE_RESULT_ERROR;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
listener.onPurchaseResult(code, data);
|
|
||||||
|
|
||||||
if (fulfilReceiptId != null && autoFinishTransactions) {
|
|
||||||
PurchasingService.notifyFulfillment(fulfilReceiptId, FulfillmentResult.FULFILLED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This callback method is invoked when a PurchaseUpdatesResponse is available for a request initiated by PurchasingService.getPurchaseUpdates(boolean).
|
|
||||||
@Override
|
|
||||||
public void onPurchaseUpdatesResponse(PurchaseUpdatesResponse purchaseUpdatesResponse) {
|
|
||||||
|
|
||||||
// The documentation seems to be a little misguiding regarding how to handle this.
|
|
||||||
// This call is in response to getPurchaseUpdates() which can be called in two modes
|
|
||||||
//
|
|
||||||
// 1) Get all receipts since last call (reset = true)
|
|
||||||
// 2) Get the whole transaction history.
|
|
||||||
//
|
|
||||||
// The result can carry the flag hasMore() where it is required to call getPurchaseUpdates again. See docs:
|
|
||||||
// https://developer.amazon.com/public/apis/earn/in-app-purchasing/docs-v2/implementing-iap-2.0
|
|
||||||
//
|
|
||||||
// Examples indicate it should be called with the same value for 'reset' the secon time around
|
|
||||||
// but actual testing ends up in an infinite loop where the same results are returned over and over.
|
|
||||||
//
|
|
||||||
// So here getPurchaseUpdates is called with result=false to fetch the next round of receipts.
|
|
||||||
|
|
||||||
RequestId reqId = purchaseUpdatesResponse.getRequestId();
|
|
||||||
IPurchaseListener listener = pickPurchaseListener(reqId);
|
|
||||||
if (listener == null) {
|
|
||||||
Log.e(TAG, "No listener found for request " + reqId.toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (purchaseUpdatesResponse.getRequestStatus()) {
|
|
||||||
case SUCCESSFUL:
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
for (Receipt receipt : purchaseUpdatesResponse.getReceipts()) {
|
|
||||||
JSONObject trans = makeTransactionObject(purchaseUpdatesResponse.getUserData(), receipt, IapJNI.TRANS_STATE_PURCHASED);
|
|
||||||
listener.onPurchaseResult(IapJNI.BILLING_RESPONSE_RESULT_OK, trans.toString());
|
|
||||||
if(autoFinishTransactions) {
|
|
||||||
PurchasingService.notifyFulfillment(receipt.getReceiptId(), FulfillmentResult.FULFILLED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (purchaseUpdatesResponse.hasMore()) {
|
|
||||||
doGetPurchaseUpdates(listener, false);
|
|
||||||
}
|
|
||||||
} catch (JSONException e) {
|
|
||||||
Log.e(TAG, "JSON Exception occured: " + e.toString());
|
|
||||||
listener.onPurchaseResult(IapJNI.BILLING_RESPONSE_RESULT_DEVELOPER_ERROR, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case FAILED:
|
|
||||||
case NOT_SUPPORTED:
|
|
||||||
default:
|
|
||||||
listener.onPurchaseResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This callback method is invoked when a UserDataResponse is available for a request initiated by PurchasingService.getUserData().
|
|
||||||
@Override
|
|
||||||
public void onUserDataResponse(UserDataResponse userDataResponse) {
|
|
||||||
// Intentionally left un-implemented; not used.
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user