mirror of
https://github.com/defold/extension-iap
synced 2025-06-27 02:17:51 +02:00
Initial commit
This commit is contained in:
parent
1fd7aba9dd
commit
0fa8400cac
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/.internal
|
||||||
|
/build
|
||||||
|
.externalToolBuilders
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.lock-wscript
|
||||||
|
*.pyc
|
||||||
|
.project
|
||||||
|
.cproject
|
||||||
|
builtins
|
15
extension-iap/ext.manifest
Normal file
15
extension-iap/ext.manifest
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
name: IAPExt
|
||||||
|
|
||||||
|
platforms:
|
||||||
|
armv7-ios:
|
||||||
|
context:
|
||||||
|
weakFrameworks: ['StoreKit', 'UIKit', 'Foundation']
|
||||||
|
|
||||||
|
arm64-ios:
|
||||||
|
context:
|
||||||
|
weakFrameworks: ['StoreKit', 'UIKit', 'Foundation']
|
||||||
|
|
||||||
|
x86_64-ios:
|
||||||
|
context:
|
||||||
|
weakFrameworks: ['StoreKit', 'UIKit', 'Foundation']
|
||||||
|
|
BIN
extension-iap/lib/android/in-app-purchasing-2.0.61.jar
Normal file
BIN
extension-iap/lib/android/in-app-purchasing-2.0.61.jar
Normal file
Binary file not shown.
169
extension-iap/lib/web/library_facebook_iap.js
Normal file
169
extension-iap/lib/web/library_facebook_iap.js
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
|
||||||
|
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_state, 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 = allocate(intArrayFromString(productsJSON), 'i8', ALLOC_STACK);
|
||||||
|
Runtime.dynCall('vii', callback, [lua_state, res_buf]);
|
||||||
|
} else {
|
||||||
|
var xmlhttp = new XMLHttpRequest();
|
||||||
|
xmlhttp.onreadystatechange = function() {
|
||||||
|
FBinner.http_callback(xmlhttp, callback, lua_state, products, product_ids, product_count, url_index+1);
|
||||||
|
};
|
||||||
|
xmlhttp.open("GET", product_ids[url_index+1], true);
|
||||||
|
xmlhttp.send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
dmIAPFBList: function(params, callback, lua_state) {
|
||||||
|
var product_ids = Pointer_stringify(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_state, 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_state) {
|
||||||
|
var product_id = Pointer_stringify(param_product_id);
|
||||||
|
|
||||||
|
var buy_params = {
|
||||||
|
method: 'pay',
|
||||||
|
action: 'purchaseitem',
|
||||||
|
product: product_id,
|
||||||
|
};
|
||||||
|
if(param_request_id != 0) {
|
||||||
|
buy_params.request_id = Pointer_stringify(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 = allocate(intArrayFromString(productsJSON), 'i8', ALLOC_STACK);
|
||||||
|
Runtime.dynCall('viii', callback, [lua_state, 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);
|
||||||
|
}
|
||||||
|
Runtime.dynCall('viii', callback, [lua_state, 0, reason]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
autoAddDeps(LibraryFacebookIAP, '$FBinner');
|
||||||
|
mergeInto(LibraryManager.library, LibraryFacebookIAP);
|
25
extension-iap/manifests/android/AndroidManifest.xml
Normal file
25
extension-iap/manifests/android/AndroidManifest.xml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="{{android.package}}">
|
||||||
|
<uses-sdk android:minSdkVersion="{{android.minimum_sdk_version}}" android:targetSdkVersion="{{android.target_sdk_version}}" />
|
||||||
|
<application>
|
||||||
|
|
||||||
|
<!-- For IAP -->
|
||||||
|
<activity android:name="com.defold.iap.IapGooglePlayActivity"
|
||||||
|
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||||
|
android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation"
|
||||||
|
android:label="IAP">
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<!-- For Amazon IAP -->
|
||||||
|
<receiver android:name="com.amazon.device.iap.ResponseReceiver" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.amazon.inapp.purchasing.NOTIFY" android:permission="com.amazon.inapp.purchasing.Permission.NOTIFY" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
<uses-permission android:name="com.android.vending.BILLING" />
|
||||||
|
|
||||||
|
</manifest>
|
4
extension-iap/manifests/android/extension-adinfo.pro
Normal file
4
extension-iap/manifests/android/extension-adinfo.pro
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
-keep class com.defold.iap.** {
|
||||||
|
public <methods>;
|
||||||
|
}
|
||||||
|
|
46
extension-iap/src/iap.h
Normal file
46
extension-iap/src/iap.h
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
#if defined(DM_PLATFORM_HTML5) || defined(DM_PLATFORM_ANDROID) || defined(DM_PLATFORM_IOS)
|
||||||
|
|
||||||
|
#ifndef DM_IAP_EXTENSION
|
||||||
|
#define DM_IAP_EXTENSION
|
||||||
|
|
||||||
|
// NOTE: Also defined in library_facebook_iap.js
|
||||||
|
// NOTE: Also defined in IapJNI.java
|
||||||
|
|
||||||
|
enum TransactionState
|
||||||
|
{
|
||||||
|
TRANS_STATE_PURCHASING = 0,
|
||||||
|
TRANS_STATE_PURCHASED = 1,
|
||||||
|
TRANS_STATE_FAILED = 2,
|
||||||
|
TRANS_STATE_RESTORED = 3,
|
||||||
|
TRANS_STATE_UNVERIFIED = 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum ErrorReason
|
||||||
|
{
|
||||||
|
REASON_UNSPECIFIED = 0,
|
||||||
|
REASON_USER_CANCELED = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum 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,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum ProviderId
|
||||||
|
{
|
||||||
|
PROVIDER_ID_GOOGLE = 0,
|
||||||
|
PROVIDER_ID_AMAZON = 1,
|
||||||
|
PROVIDER_ID_APPLE = 2,
|
||||||
|
PROVIDER_ID_FACEBOOK = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DM_IAP_EXTENSION
|
||||||
|
|
||||||
|
#endif // DM_PLATFORM_HTML5 || DM_PLATFORM_ANDROID || DM_PLATFORM_IOS
|
583
extension-iap/src/iap_android.cpp
Normal file
583
extension-iap/src/iap_android.cpp
Normal file
@ -0,0 +1,583 @@
|
|||||||
|
#if defined(DM_PLATFORM_ANDROID)
|
||||||
|
|
||||||
|
#include <dmsdk/sdk.h>
|
||||||
|
#include <jni.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include "iap.h"
|
||||||
|
#include "iap_private.h"
|
||||||
|
|
||||||
|
#define LIB_NAME "iap"
|
||||||
|
|
||||||
|
extern struct android_app* g_AndroidApp;
|
||||||
|
|
||||||
|
struct IAP;
|
||||||
|
|
||||||
|
#define CMD_PRODUCT_RESULT (0)
|
||||||
|
#define CMD_PURCHASE_RESULT (1)
|
||||||
|
|
||||||
|
struct DM_ALIGNED(16) Command
|
||||||
|
{
|
||||||
|
Command()
|
||||||
|
{
|
||||||
|
memset(this, 0, sizeof(*this));
|
||||||
|
}
|
||||||
|
uint32_t m_Command;
|
||||||
|
int32_t m_ResponseCode;
|
||||||
|
void* m_Data1;
|
||||||
|
};
|
||||||
|
|
||||||
|
static dmArray<Command> m_commandsQueue;
|
||||||
|
static dmMutex::HMutex m_mutex;
|
||||||
|
|
||||||
|
static JNIEnv* Attach()
|
||||||
|
{
|
||||||
|
JNIEnv* env;
|
||||||
|
g_AndroidApp->activity->vm->AttachCurrentThread(&env, NULL);
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Detach()
|
||||||
|
{
|
||||||
|
g_AndroidApp->activity->vm->DetachCurrentThread();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct IAP
|
||||||
|
{
|
||||||
|
IAP()
|
||||||
|
{
|
||||||
|
memset(this, 0, sizeof(*this));
|
||||||
|
m_Callback = LUA_NOREF;
|
||||||
|
m_Self = LUA_NOREF;
|
||||||
|
m_Listener.m_Callback = LUA_NOREF;
|
||||||
|
m_Listener.m_Self = LUA_NOREF;
|
||||||
|
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;
|
||||||
|
|
||||||
|
jobject m_IAP;
|
||||||
|
jobject m_IAPJNI;
|
||||||
|
jmethodID m_List;
|
||||||
|
jmethodID m_Stop;
|
||||||
|
jmethodID m_Buy;
|
||||||
|
jmethodID m_Restore;
|
||||||
|
jmethodID m_ProcessPendingConsumables;
|
||||||
|
jmethodID m_FinishTransaction;
|
||||||
|
int m_Pipefd[2];
|
||||||
|
};
|
||||||
|
|
||||||
|
IAP g_IAP;
|
||||||
|
|
||||||
|
static void add_to_queue(Command cmd)
|
||||||
|
{
|
||||||
|
DM_MUTEX_SCOPED_LOCK(m_mutex);
|
||||||
|
|
||||||
|
if(m_commandsQueue.Full())
|
||||||
|
{
|
||||||
|
m_commandsQueue.OffsetCapacity(1);
|
||||||
|
}
|
||||||
|
m_commandsQueue.Push(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void VerifyCallback(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;
|
||||||
|
g_IAP.m_Self = LUA_NOREF;
|
||||||
|
g_IAP.m_L = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int IAP_List(lua_State* L)
|
||||||
|
{
|
||||||
|
int top = lua_gettop(L);
|
||||||
|
VerifyCallback(L);
|
||||||
|
|
||||||
|
char* buf = IAP_List_CreateBuffer(L);
|
||||||
|
if( buf == 0 )
|
||||||
|
{
|
||||||
|
assert(top == lua_gettop(L));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
g_IAP.m_L = dmScript::GetMainThread(L);
|
||||||
|
|
||||||
|
JNIEnv* env = Attach();
|
||||||
|
jstring products = env->NewStringUTF(buf);
|
||||||
|
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_List, products, g_IAP.m_IAPJNI);
|
||||||
|
env->DeleteLocalRef(products);
|
||||||
|
Detach();
|
||||||
|
|
||||||
|
free(buf);
|
||||||
|
assert(top == lua_gettop(L));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int IAP_Buy(lua_State* L)
|
||||||
|
{
|
||||||
|
int top = lua_gettop(L);
|
||||||
|
|
||||||
|
const char* id = luaL_checkstring(L, 1);
|
||||||
|
|
||||||
|
JNIEnv* env = Attach();
|
||||||
|
jstring ids = env->NewStringUTF(id);
|
||||||
|
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_Buy, ids, g_IAP.m_IAPJNI);
|
||||||
|
env->DeleteLocalRef(ids);
|
||||||
|
Detach();
|
||||||
|
|
||||||
|
assert(top == lua_gettop(L));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int IAP_Finish(lua_State* L)
|
||||||
|
{
|
||||||
|
if(g_IAP.m_autoFinishTransactions)
|
||||||
|
{
|
||||||
|
dmLogWarning("Calling iap.finish when autofinish transactions is enabled. Ignored.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int top = lua_gettop(L);
|
||||||
|
|
||||||
|
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);
|
||||||
|
assert(top == lua_gettop(L));
|
||||||
|
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);
|
||||||
|
|
||||||
|
JNIEnv* env = Attach();
|
||||||
|
jstring receiptUTF = env->NewStringUTF(receipt);
|
||||||
|
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_FinishTransaction, receiptUTF, g_IAP.m_IAPJNI);
|
||||||
|
env->DeleteLocalRef(receiptUTF);
|
||||||
|
Detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(top == lua_gettop(L));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int IAP_Restore(lua_State* L)
|
||||||
|
{
|
||||||
|
// TODO: Missing callback here for completion/error
|
||||||
|
// See iap_ios.mm
|
||||||
|
|
||||||
|
int top = lua_gettop(L);
|
||||||
|
JNIEnv* env = Attach();
|
||||||
|
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_Restore, g_IAP.m_IAPJNI);
|
||||||
|
Detach();
|
||||||
|
|
||||||
|
assert(top == lua_gettop(L));
|
||||||
|
|
||||||
|
lua_pushboolean(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
bool had_previous = false;
|
||||||
|
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);
|
||||||
|
had_previous = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// On first set listener, trigger process old ones.
|
||||||
|
if (!had_previous) {
|
||||||
|
JNIEnv* env = Attach();
|
||||||
|
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_ProcessPendingConsumables, g_IAP.m_IAPJNI);
|
||||||
|
Detach();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int IAP_GetProviderId(lua_State* L)
|
||||||
|
{
|
||||||
|
lua_pushinteger(L, g_IAP.m_ProviderId);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const luaL_reg IAP_methods[] =
|
||||||
|
{
|
||||||
|
{"list", IAP_List},
|
||||||
|
{"buy", IAP_Buy},
|
||||||
|
{"finish", IAP_Finish},
|
||||||
|
{"restore", IAP_Restore},
|
||||||
|
{"set_listener", IAP_SetListener},
|
||||||
|
{"get_provider_id", IAP_GetProviderId},
|
||||||
|
{0, 0}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onProductsResult__ILjava_lang_String_2(JNIEnv* env, jobject, jint responseCode, jstring productList)
|
||||||
|
{
|
||||||
|
const char* pl = 0;
|
||||||
|
if (productList)
|
||||||
|
{
|
||||||
|
pl = env->GetStringUTFChars(productList, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Command cmd;
|
||||||
|
cmd.m_Command = CMD_PRODUCT_RESULT;
|
||||||
|
cmd.m_ResponseCode = responseCode;
|
||||||
|
if (pl)
|
||||||
|
{
|
||||||
|
cmd.m_Data1 = strdup(pl);
|
||||||
|
env->ReleaseStringUTFChars(productList, pl);
|
||||||
|
}
|
||||||
|
if (write(g_IAP.m_Pipefd[1], &cmd, sizeof(cmd)) != sizeof(cmd)) {
|
||||||
|
dmLogFatal("Failed to write command");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onPurchaseResult__ILjava_lang_String_2(JNIEnv* env, jobject, jint responseCode, jstring purchaseData)
|
||||||
|
{
|
||||||
|
const char* pd = 0;
|
||||||
|
if (purchaseData)
|
||||||
|
{
|
||||||
|
pd = env->GetStringUTFChars(purchaseData, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Command cmd;
|
||||||
|
cmd.m_Command = CMD_PURCHASE_RESULT;
|
||||||
|
cmd.m_ResponseCode = responseCode;
|
||||||
|
|
||||||
|
if (pd)
|
||||||
|
{
|
||||||
|
cmd.m_Data1 = strdup(pd);
|
||||||
|
env->ReleaseStringUTFChars(purchaseData, pd);
|
||||||
|
}
|
||||||
|
if (write(g_IAP.m_Pipefd[1], &cmd, sizeof(cmd)) != sizeof(cmd)) {
|
||||||
|
dmLogFatal("Failed to write command");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void HandleProductResult(const Command* cmd)
|
||||||
|
{
|
||||||
|
lua_State* L = g_IAP.m_L;
|
||||||
|
int top = lua_gettop(L);
|
||||||
|
|
||||||
|
if (g_IAP.m_Callback == LUA_NOREF) {
|
||||||
|
dmLogError("No callback set");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 IAP callback because the instance has been deleted.");
|
||||||
|
lua_pop(L, 2);
|
||||||
|
assert(top == lua_gettop(L));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd->m_ResponseCode == BILLING_RESPONSE_RESULT_OK) {
|
||||||
|
dmJson::Document doc;
|
||||||
|
dmJson::Result r = dmJson::Parse((const char*) cmd->m_Data1, &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) {
|
||||||
|
dmLogError("Failed converting product result JSON to Lua; %s", err_str);
|
||||||
|
lua_pushnil(L);
|
||||||
|
IAP_PushError(L, "failed to convert JSON to Lua for product response", REASON_UNSPECIFIED);
|
||||||
|
} else {
|
||||||
|
lua_pushnil(L);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dmLogError("Failed to parse product response (%d)", r);
|
||||||
|
lua_pushnil(L);
|
||||||
|
IAP_PushError(L, "failed to parse product response", REASON_UNSPECIFIED);
|
||||||
|
}
|
||||||
|
dmJson::Free(&doc);
|
||||||
|
} else {
|
||||||
|
dmLogError("IAP error %d", cmd->m_ResponseCode);
|
||||||
|
lua_pushnil(L);
|
||||||
|
IAP_PushError(L, "failed to fetch product", REASON_UNSPECIFIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret = lua_pcall(L, 3, 0, 0);
|
||||||
|
if (ret != 0) {
|
||||||
|
dmLogError("Error running iap callback");
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandlePurchaseResult(const Command* cmd)
|
||||||
|
{
|
||||||
|
lua_State* L = g_IAP.m_Listener.m_L;
|
||||||
|
int top = lua_gettop(L);
|
||||||
|
|
||||||
|
if (g_IAP.m_Listener.m_Callback == LUA_NOREF) {
|
||||||
|
dmLogError("No callback set");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
lua_rawgeti(L, LUA_REGISTRYINDEX, g_IAP.m_Listener.m_Callback);
|
||||||
|
|
||||||
|
// Setup self
|
||||||
|
lua_rawgeti(L, LUA_REGISTRYINDEX, g_IAP.m_Listener.m_Self);
|
||||||
|
lua_pushvalue(L, -1);
|
||||||
|
dmScript::SetInstance(L);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd->m_ResponseCode == BILLING_RESPONSE_RESULT_OK) {
|
||||||
|
if (cmd->m_Data1 != 0) {
|
||||||
|
dmJson::Document doc;
|
||||||
|
dmJson::Result r = dmJson::Parse((const char*) cmd->m_Data1, &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) {
|
||||||
|
dmLogError("Failed converting purchase JSON result to Lua; %s", err_str);
|
||||||
|
lua_pushnil(L);
|
||||||
|
IAP_PushError(L, "failed to convert purchase response JSON to Lua", REASON_UNSPECIFIED);
|
||||||
|
} else {
|
||||||
|
lua_pushnil(L);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dmLogError("Failed to parse purchase response (%d)", r);
|
||||||
|
lua_pushnil(L);
|
||||||
|
IAP_PushError(L, "failed to parse purchase response", REASON_UNSPECIFIED);
|
||||||
|
}
|
||||||
|
dmJson::Free(&doc);
|
||||||
|
} 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) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret = lua_pcall(L, 3, 0, 0);
|
||||||
|
if (ret != 0) {
|
||||||
|
dmLogError("Error running iap callback");
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(top == lua_gettop(L));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void InvokeCallback(Command* cmd)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|
||||||
|
m_mutex = dmMutex::New();
|
||||||
|
|
||||||
|
g_IAP.m_autoFinishTransactions = dmConfigFile::GetInt(params->m_ConfigFile, "iap.auto_finish_transactions", 1) == 1;
|
||||||
|
|
||||||
|
JNIEnv* env = Attach();
|
||||||
|
|
||||||
|
jclass activity_class = env->FindClass("android/app/NativeActivity");
|
||||||
|
jmethodID get_class_loader = env->GetMethodID(activity_class,"getClassLoader", "()Ljava/lang/ClassLoader;");
|
||||||
|
jobject cls = env->CallObjectMethod(g_AndroidApp->activity->clazz, get_class_loader);
|
||||||
|
jclass class_loader = env->FindClass("java/lang/ClassLoader");
|
||||||
|
jmethodID find_class = env->GetMethodID(class_loader, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
|
||||||
|
|
||||||
|
const char* provider = dmConfigFile::GetString(params->m_ConfigFile, "android.iap_provider", "GooglePlay");
|
||||||
|
const char* class_name = "com.defold.iap.IapGooglePlay";
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
jstring str_class_name = env->NewStringUTF(class_name);
|
||||||
|
|
||||||
|
jclass iap_class = (jclass)env->CallObjectMethod(cls, find_class, str_class_name);
|
||||||
|
env->DeleteLocalRef(str_class_name);
|
||||||
|
|
||||||
|
str_class_name = env->NewStringUTF("com.defold.iap.IapJNI");
|
||||||
|
jclass iap_jni_class = (jclass)env->CallObjectMethod(cls, find_class, str_class_name);
|
||||||
|
env->DeleteLocalRef(str_class_name);
|
||||||
|
|
||||||
|
g_IAP.m_List = env->GetMethodID(iap_class, "listItems", "(Ljava/lang/String;Lcom/defold/iap/IListProductsListener;)V");
|
||||||
|
g_IAP.m_Buy = env->GetMethodID(iap_class, "buy", "(Ljava/lang/String;Lcom/defold/iap/IPurchaseListener;)V");
|
||||||
|
g_IAP.m_Restore = env->GetMethodID(iap_class, "restore", "(Lcom/defold/iap/IPurchaseListener;)V");
|
||||||
|
g_IAP.m_Stop = env->GetMethodID(iap_class, "stop", "()V");
|
||||||
|
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");
|
||||||
|
|
||||||
|
jmethodID jni_constructor = env->GetMethodID(iap_class, "<init>", "(Landroid/app/Activity;Z)V");
|
||||||
|
g_IAP.m_IAP = env->NewGlobalRef(env->NewObject(iap_class, jni_constructor, g_AndroidApp->activity->clazz, g_IAP.m_autoFinishTransactions));
|
||||||
|
|
||||||
|
jni_constructor = env->GetMethodID(iap_jni_class, "<init>", "()V");
|
||||||
|
g_IAP.m_IAPJNI = env->NewGlobalRef(env->NewObject(iap_jni_class, jni_constructor));
|
||||||
|
|
||||||
|
Detach();
|
||||||
|
}
|
||||||
|
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 UpdateIAP(dmExtension::Params* params)
|
||||||
|
{
|
||||||
|
if (m_commandsQueue.Empty())
|
||||||
|
{
|
||||||
|
return dmExtension::RESULT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
DM_MUTEX_SCOPED_LOCK(m_mutex);
|
||||||
|
|
||||||
|
for(uint32_t i = 0; i != m_commandsQueue.Size(); ++i)
|
||||||
|
{
|
||||||
|
Command* cmd = &m_commandsQueue[i];
|
||||||
|
InvokeCallback(cmd);
|
||||||
|
m_commandsQueue.EraseSwap(i--);
|
||||||
|
}
|
||||||
|
return dmExtension::RESULT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static dmExtension::Result FinalizeIAP(dmExtension::Params* params)
|
||||||
|
{
|
||||||
|
dmMutex::Delete(m_mutex);
|
||||||
|
--g_IAP.m_InitCount;
|
||||||
|
|
||||||
|
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 (g_IAP.m_InitCount == 0) {
|
||||||
|
JNIEnv* env = Attach();
|
||||||
|
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_Stop);
|
||||||
|
env->DeleteGlobalRef(g_IAP.m_IAP);
|
||||||
|
env->DeleteGlobalRef(g_IAP.m_IAPJNI);
|
||||||
|
Detach();
|
||||||
|
g_IAP.m_IAP = NULL;
|
||||||
|
|
||||||
|
int result = ALooper_removeFd(g_AndroidApp->looper, g_IAP.m_Pipefd[0]);
|
||||||
|
if (result != 1) {
|
||||||
|
dmLogFatal("Could not remove fd from looper: %d", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(g_IAP.m_Pipefd[0]);
|
||||||
|
close(g_IAP.m_Pipefd[1]);
|
||||||
|
}
|
||||||
|
return dmExtension::RESULT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
DM_DECLARE_EXTENSION(IAPExt, "IAP", 0, 0, InitializeIAP, UpdateIAP, 0, FinalizeIAP)
|
||||||
|
|
||||||
|
#endif //DM_PLATFORM_ANDROID
|
||||||
|
|
311
extension-iap/src/iap_emscripten.cpp
Normal file
311
extension-iap/src/iap_emscripten.cpp
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
#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_Callback = LUA_NOREF;
|
||||||
|
m_Self = LUA_NOREF;
|
||||||
|
m_Listener.m_Callback = LUA_NOREF;
|
||||||
|
m_Listener.m_Self = LUA_NOREF;
|
||||||
|
m_autoFinishTransactions = true;
|
||||||
|
}
|
||||||
|
int m_InitCount;
|
||||||
|
int m_Callback;
|
||||||
|
int m_Self;
|
||||||
|
bool m_autoFinishTransactions;
|
||||||
|
lua_State* m_L;
|
||||||
|
IAPListener m_Listener;
|
||||||
|
|
||||||
|
} g_IAP;
|
||||||
|
|
||||||
|
typedef void (*OnIAPFBList)(void *L, const char* json);
|
||||||
|
typedef void (*OnIAPFBListenerCallback)(void *L, const char* json, int error_code);
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
// Implementation in library_facebook_iap.js
|
||||||
|
void dmIAPFBList(const char* item_ids, OnIAPFBList callback, lua_State* L);
|
||||||
|
void dmIAPFBBuy(const char* item_id, const char* request_id, OnIAPFBListenerCallback callback, lua_State* L);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void VerifyCallback(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;
|
||||||
|
g_IAP.m_Self = LUA_NOREF;
|
||||||
|
g_IAP.m_L = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void IAPList_Callback(void* Lv, const char* result_json)
|
||||||
|
{
|
||||||
|
lua_State* L = (lua_State*) Lv;
|
||||||
|
if (g_IAP.m_Callback != LUA_NOREF) {
|
||||||
|
int top = lua_gettop(L);
|
||||||
|
int callback = g_IAP.m_Callback;
|
||||||
|
lua_rawgeti(L, LUA_REGISTRYINDEX, 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 iap facebook callback because the instance has been deleted.");
|
||||||
|
lua_pop(L, 2);
|
||||||
|
assert(top == lua_gettop(L));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(result_json != 0)
|
||||||
|
{
|
||||||
|
dmJson::Document doc;
|
||||||
|
dmJson::Result r = dmJson::Parse(result_json, &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) {
|
||||||
|
dmLogError("Failed converting list result JSON to Lua; %s", err_str);
|
||||||
|
lua_pushnil(L);
|
||||||
|
IAP_PushError(L, "Failed converting list result JSON to Lua", REASON_UNSPECIFIED);
|
||||||
|
} else {
|
||||||
|
lua_pushnil(L);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dmLogError("Failed to parse list result JSON (%d)", r);
|
||||||
|
lua_pushnil(L);
|
||||||
|
IAP_PushError(L, "Failed to parse list result JSON", REASON_UNSPECIFIED);
|
||||||
|
}
|
||||||
|
dmJson::Free(&doc);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dmLogError("Got empty list result.");
|
||||||
|
lua_pushnil(L);
|
||||||
|
IAP_PushError(L, "Got empty list result.", REASON_UNSPECIFIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret = lua_pcall(L, 3, 0, 0);
|
||||||
|
if (ret != 0) {
|
||||||
|
dmLogError("Error running iap callback");
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
assert(top == lua_gettop(L));
|
||||||
|
dmScript::Unref(L, LUA_REGISTRYINDEX, callback);
|
||||||
|
|
||||||
|
g_IAP.m_Callback = LUA_NOREF;
|
||||||
|
} else {
|
||||||
|
dmLogError("No callback set");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
int IAP_List(lua_State* L)
|
||||||
|
{
|
||||||
|
int top = lua_gettop(L);
|
||||||
|
VerifyCallback(L);
|
||||||
|
|
||||||
|
char* buf = IAP_List_CreateBuffer(L);
|
||||||
|
if( buf == 0 )
|
||||||
|
{
|
||||||
|
assert(top == lua_gettop(L));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
g_IAP.m_L = dmScript::GetMainThread(L);
|
||||||
|
dmIAPFBList(buf, (OnIAPFBList)IAPList_Callback, g_IAP.m_L);
|
||||||
|
|
||||||
|
free(buf);
|
||||||
|
assert(top == lua_gettop(L));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void IAPListener_Callback(void* Lv, const char* result_json, int error_code)
|
||||||
|
{
|
||||||
|
lua_State* L = g_IAP.m_Listener.m_L;
|
||||||
|
int top = lua_gettop(L);
|
||||||
|
if (g_IAP.m_Listener.m_Callback == LUA_NOREF) {
|
||||||
|
dmLogError("No callback set");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lua_rawgeti(L, LUA_REGISTRYINDEX, g_IAP.m_Listener.m_Callback);
|
||||||
|
|
||||||
|
// Setup self
|
||||||
|
lua_rawgeti(L, LUA_REGISTRYINDEX, g_IAP.m_Listener.m_Self);
|
||||||
|
lua_pushvalue(L, -1);
|
||||||
|
dmScript::SetInstance(L);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
if (result_json) {
|
||||||
|
dmJson::Document doc;
|
||||||
|
dmJson::Result r = dmJson::Parse(result_json, &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) {
|
||||||
|
dmLogError("Failed converting purchase result JSON to Lua; %s", err_str);
|
||||||
|
lua_pushnil(L);
|
||||||
|
IAP_PushError(L, "failed converting purchase result JSON to Lua", REASON_UNSPECIFIED);
|
||||||
|
} else {
|
||||||
|
lua_pushnil(L);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dmLogError("Failed to parse purchase response (%d)", r);
|
||||||
|
lua_pushnil(L);
|
||||||
|
IAP_PushError(L, "failed to parse purchase response", REASON_UNSPECIFIED);
|
||||||
|
}
|
||||||
|
dmJson::Free(&doc);
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int ret = lua_pcall(L, 3, 0, 0);
|
||||||
|
if (ret != 0) {
|
||||||
|
dmLogError("Error running iap callback");
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
assert(top == lua_gettop(L));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int IAP_Buy(lua_State* L)
|
||||||
|
{
|
||||||
|
if (g_IAP.m_Listener.m_Callback == LUA_NOREF) {
|
||||||
|
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, L);
|
||||||
|
assert(top == lua_gettop(L));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int IAP_Finish(lua_State* L)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int IAP_Restore(lua_State* L)
|
||||||
|
{
|
||||||
|
lua_pushboolean(L, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int IAP_GetProviderId(lua_State* L)
|
||||||
|
{
|
||||||
|
lua_pushinteger(L, PROVIDER_ID_FACEBOOK);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const luaL_reg IAP_methods[] =
|
||||||
|
{
|
||||||
|
{"list", IAP_List},
|
||||||
|
{"buy", IAP_Buy},
|
||||||
|
{"finish", IAP_Finish},
|
||||||
|
{"restore", IAP_Restore},
|
||||||
|
{"set_listener", IAP_SetListener},
|
||||||
|
{"get_provider_id", IAP_GetProviderId},
|
||||||
|
{0, 0}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
dmExtension::Result FinalizeIAP(dmExtension::Params* params)
|
||||||
|
{
|
||||||
|
--g_IAP.m_InitCount;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
return dmExtension::RESULT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
DM_DECLARE_EXTENSION(IAPExt, "IAP", 0, 0, InitializeIAP, 0, 0, FinalizeIAP)
|
||||||
|
|
||||||
|
#endif // DM_PLATFORM_HTML5
|
806
extension-iap/src/iap_ios.mm
Normal file
806
extension-iap/src/iap_ios.mm
Normal file
@ -0,0 +1,806 @@
|
|||||||
|
#if defined(DM_PLATFORM_IOS)
|
||||||
|
|
||||||
|
#include <dmsdk/sdk.h>
|
||||||
|
|
||||||
|
#include "iap.h"
|
||||||
|
#include "iap_private.h"
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
#import <StoreKit/StoreKit.h>
|
||||||
|
|
||||||
|
#define LIB_NAME "iap"
|
||||||
|
|
||||||
|
struct IAP;
|
||||||
|
|
||||||
|
@interface SKPaymentTransactionObserver : NSObject<SKPaymentTransactionObserver>
|
||||||
|
@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;
|
||||||
|
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 g_IAP;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@interface SKProductsRequestDelegate : NSObject<SKProductsRequestDelegate>
|
||||||
|
@property lua_State* m_LuaState;
|
||||||
|
@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) {
|
||||||
|
dmLogError("No callback set");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSArray * skProducts = response.products;
|
||||||
|
int top = lua_gettop(L);
|
||||||
|
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
|
||||||
|
NSNumberFormatter *formatter = [[[NSNumberFormatter alloc] init] autorelease];
|
||||||
|
[formatter setNumberStyle: NSNumberFormatterCurrencyStyle];
|
||||||
|
[formatter setLocale: p.priceLocale];
|
||||||
|
NSString *price_string = [formatter stringFromNumber: p.price];
|
||||||
|
|
||||||
|
lua_pushstring(L, "price_string");
|
||||||
|
lua_pushstring(L, [price_string UTF8String]);
|
||||||
|
lua_rawset(L, -3);
|
||||||
|
|
||||||
|
lua_pushstring(L, "currency_code");
|
||||||
|
lua_pushstring(L, [[p.priceLocale objectForKey:NSLocaleCurrencyCode] UTF8String]);
|
||||||
|
lua_rawset(L, -3);
|
||||||
|
|
||||||
|
lua_rawset(L, -3);
|
||||||
|
}
|
||||||
|
lua_pushnil(L);
|
||||||
|
|
||||||
|
int ret = lua_pcall(L, 3, 0, 0);
|
||||||
|
if (ret != 0) {
|
||||||
|
dmLogError("Error running iap callback");
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
- (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) {
|
||||||
|
dmLogError("No callback set");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 iap callback");
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)requestDidFinish:(SKRequest *)request
|
||||||
|
{
|
||||||
|
[self.m_Request release];
|
||||||
|
[self release];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
static void PushTransaction(lua_State* L, SKPaymentTransaction* transaction)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (transaction.transactionState == SKPaymentTransactionStateRestored && transaction.originalTransaction) {
|
||||||
|
lua_pushstring(L, "original_trans");
|
||||||
|
PushTransaction(L, transaction.originalTransaction);
|
||||||
|
lua_rawset(L, -3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunTransactionCallback(lua_State* L, int cb, int self, SKPaymentTransaction* transaction)
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lua_pushnil(L);
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret = lua_pcall(L, 3, 0, 0);
|
||||||
|
if (ret != 0) {
|
||||||
|
dmLogError("Error running iap callback");
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(top == lua_gettop(L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@implementation SKPaymentTransactionObserver
|
||||||
|
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
|
||||||
|
{
|
||||||
|
for (SKPaymentTransaction * transaction in transactions) {
|
||||||
|
|
||||||
|
if ((!g_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] ];
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (transaction.transactionState)
|
||||||
|
{
|
||||||
|
case SKPaymentTransactionStatePurchasing:
|
||||||
|
break;
|
||||||
|
case SKPaymentTransactionStatePurchased:
|
||||||
|
if (has_listener > 0 && g_IAP.m_AutoFinishTransactions) {
|
||||||
|
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SKPaymentTransactionStateFailed:
|
||||||
|
if (has_listener > 0) {
|
||||||
|
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SKPaymentTransactionStateRestored:
|
||||||
|
if (has_listener > 0) {
|
||||||
|
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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];
|
||||||
|
|
||||||
|
luaL_checktype(L, 1, LUA_TTABLE);
|
||||||
|
lua_pushnil(L);
|
||||||
|
while (lua_next(L, 1) != 0) {
|
||||||
|
const char* p = luaL_checkstring(L, -1);
|
||||||
|
[product_identifiers addObject: [NSString stringWithUTF8String: p]];
|
||||||
|
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);
|
||||||
|
delegate.m_Request = products_request;
|
||||||
|
products_request.delegate = delegate;
|
||||||
|
[products_request start];
|
||||||
|
|
||||||
|
assert(top == lua_gettop(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)
|
||||||
|
{
|
||||||
|
int top = lua_gettop(L);
|
||||||
|
|
||||||
|
const char* id = luaL_checkstring(L, 1);
|
||||||
|
SKMutablePayment* payment = [[SKMutablePayment alloc] init];
|
||||||
|
payment.productIdentifier = [NSString stringWithUTF8String: id];
|
||||||
|
payment.quantity = 1;
|
||||||
|
|
||||||
|
[[SKPaymentQueue defaultQueue] addPayment:payment];
|
||||||
|
[payment release];
|
||||||
|
|
||||||
|
assert(top == lua_gettop(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)
|
||||||
|
{
|
||||||
|
if(g_IAP.m_AutoFinishTransactions)
|
||||||
|
{
|
||||||
|
dmLogWarning("Calling iap.finish when autofinish transactions is enabled. Ignored.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int top = lua_gettop(L);
|
||||||
|
|
||||||
|
luaL_checktype(L, 1, LUA_TTABLE);
|
||||||
|
|
||||||
|
lua_getfield(L, -1, "state");
|
||||||
|
if (lua_isnumber(L, -1))
|
||||||
|
{
|
||||||
|
if(lua_tointeger(L, -1) != SKPaymentTransactionStatePurchased)
|
||||||
|
{
|
||||||
|
dmLogError("Transaction error. Invalid transaction state for transaction finish (must be iap.TRANS_STATE_PURCHASED).");
|
||||||
|
lua_pop(L, 1);
|
||||||
|
assert(top == lua_gettop(L));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
|
||||||
|
lua_getfield(L, -1, "trans_ident");
|
||||||
|
if (!lua_isstring(L, -1)) {
|
||||||
|
dmLogError("Transaction error. Invalid transaction data for transaction finish, does not contain 'trans_ident' key.");
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const char *str = lua_tostring(L, -1);
|
||||||
|
uint64_t trans_ident_hash = dmHashBuffer64(str, strlen(str));
|
||||||
|
lua_pop(L, 1);
|
||||||
|
SKPaymentTransaction * transaction = [g_IAP.m_PendingTransactions objectForKey:[NSNumber numberWithInteger:trans_ident_hash]];
|
||||||
|
if(transaction == 0x0) {
|
||||||
|
dmLogError("Transaction error. Invalid trans_ident value for transaction finish.");
|
||||||
|
} else {
|
||||||
|
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
|
||||||
|
[g_IAP.m_PendingTransactions removeObjectForKey:[NSNumber numberWithInteger:trans_ident_hash]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(top == lua_gettop(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)
|
||||||
|
{
|
||||||
|
// TODO: Missing callback here for completion/error
|
||||||
|
// See callback under "Handling Restored Transactions"
|
||||||
|
// https://developer.apple.com/library/ios/documentation/StoreKit/Reference/SKPaymentTransactionObserver_Protocol/Reference/Reference.html
|
||||||
|
int top = lua_gettop(L);
|
||||||
|
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
|
||||||
|
assert(top == lua_gettop(L));
|
||||||
|
lua_pushboolean(L, 1);
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*# 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)
|
||||||
|
{
|
||||||
|
lua_pushinteger(L, PROVIDER_ID_APPLE);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const luaL_reg IAP_methods[] =
|
||||||
|
{
|
||||||
|
{"list", IAP_List},
|
||||||
|
{"buy", IAP_Buy},
|
||||||
|
{"finish", IAP_Finish},
|
||||||
|
{"restore", IAP_Restore},
|
||||||
|
{"set_listener", IAP_SetListener},
|
||||||
|
{"get_provider_id", IAP_GetProviderId},
|
||||||
|
{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)
|
||||||
|
{
|
||||||
|
// 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_AutoFinishTransactions = dmConfigFile::GetInt(params->m_ConfigFile, "iap.auto_finish_transactions", 1) == 1;
|
||||||
|
g_IAP.m_PendingTransactions = [[NSMutableDictionary alloc]initWithCapacity:2];
|
||||||
|
}
|
||||||
|
g_IAP.m_InitCount++;
|
||||||
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
IAP_PushConstants(L);
|
||||||
|
|
||||||
|
lua_pop(L, 1);
|
||||||
|
assert(top == lua_gettop(L));
|
||||||
|
|
||||||
|
return dmExtension::RESULT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (g_IAP.m_InitCount == 0) {
|
||||||
|
if (g_IAP.m_PendingTransactions) {
|
||||||
|
[g_IAP.m_PendingTransactions release];
|
||||||
|
g_IAP.m_PendingTransactions = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_IAP.m_Observer) {
|
||||||
|
[[SKPaymentQueue defaultQueue] removeTransactionObserver: g_IAP.m_Observer];
|
||||||
|
[g_IAP.m_Observer release];
|
||||||
|
g_IAP.m_Observer = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dmExtension::RESULT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
DM_DECLARE_EXTENSION(IAPExt, "IAP", 0, 0, InitializeIAP, 0, 0, FinalizeIAP)
|
||||||
|
|
||||||
|
#endif // DM_PLATFORM_IOS
|
9
extension-iap/src/iap_null.cpp
Normal file
9
extension-iap/src/iap_null.cpp
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
#if !defined(DM_PLATFORM_HTML5) && !defined(DM_PLATFORM_ANDROID) && !defined(DM_PLATFORM_IOS)
|
||||||
|
|
||||||
|
extern "C" void IAPExt()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // !DM_PLATFORM_HTML5 && !DM_PLATFORM_ANDROID && !DM_PLATFORM_IOS
|
||||||
|
|
99
extension-iap/src/iap_private.cpp
Normal file
99
extension-iap/src/iap_private.cpp
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
#if defined(DM_PLATFORM_HTML5) || defined(DM_PLATFORM_ANDROID) || defined(DM_PLATFORM_IOS)
|
||||||
|
|
||||||
|
#include <dmsdk/sdk.h>
|
||||||
|
|
||||||
|
#include "iap.h"
|
||||||
|
#include "iap_private.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
int top = lua_gettop(L);
|
||||||
|
|
||||||
|
luaL_checktype(L, 1, LUA_TTABLE);
|
||||||
|
lua_pushnil(L);
|
||||||
|
int length = 0;
|
||||||
|
while (lua_next(L, 1) != 0) {
|
||||||
|
if (length > 0) {
|
||||||
|
++length;
|
||||||
|
}
|
||||||
|
const char* p = lua_tostring(L, -1);
|
||||||
|
if(!p)
|
||||||
|
{
|
||||||
|
luaL_error(L, "IAP: Failed to get value (string) from table");
|
||||||
|
}
|
||||||
|
length += strlen(p);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
char* buf = (char*)malloc(length+1);
|
||||||
|
if( buf == 0 )
|
||||||
|
{
|
||||||
|
dmLogError("Could not allocate buffer of size %d", length+1);
|
||||||
|
assert(top == lua_gettop(L));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
buf[0] = '\0';
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
lua_pushnil(L);
|
||||||
|
while (lua_next(L, 1) != 0) {
|
||||||
|
if (i > 0) {
|
||||||
|
strncat(buf, ",", length+1);
|
||||||
|
}
|
||||||
|
const char* p = lua_tostring(L, -1);
|
||||||
|
if(!p)
|
||||||
|
{
|
||||||
|
luaL_error(L, "IAP: Failed to get value (string) from table");
|
||||||
|
}
|
||||||
|
strncat(buf, p, length+1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(top == lua_gettop(L));
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
void IAP_PushError(lua_State* L, const char* error, int reason)
|
||||||
|
{
|
||||||
|
if (error != 0) {
|
||||||
|
lua_newtable(L);
|
||||||
|
lua_pushstring(L, "error");
|
||||||
|
lua_pushstring(L, error);
|
||||||
|
lua_rawset(L, -3);
|
||||||
|
lua_pushstring(L, "reason");
|
||||||
|
lua_pushnumber(L, reason);
|
||||||
|
lua_rawset(L, -3);
|
||||||
|
} else {
|
||||||
|
lua_pushnil(L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void IAP_PushConstants(lua_State* L)
|
||||||
|
{
|
||||||
|
#define SETCONSTANT(name) \
|
||||||
|
lua_pushnumber(L, (lua_Number) name); \
|
||||||
|
lua_setfield(L, -2, #name);\
|
||||||
|
|
||||||
|
SETCONSTANT(TRANS_STATE_PURCHASING)
|
||||||
|
SETCONSTANT(TRANS_STATE_PURCHASED)
|
||||||
|
SETCONSTANT(TRANS_STATE_FAILED)
|
||||||
|
SETCONSTANT(TRANS_STATE_RESTORED)
|
||||||
|
SETCONSTANT(TRANS_STATE_UNVERIFIED)
|
||||||
|
|
||||||
|
SETCONSTANT(REASON_UNSPECIFIED)
|
||||||
|
SETCONSTANT(REASON_USER_CANCELED)
|
||||||
|
|
||||||
|
SETCONSTANT(PROVIDER_ID_GOOGLE)
|
||||||
|
SETCONSTANT(PROVIDER_ID_AMAZON)
|
||||||
|
SETCONSTANT(PROVIDER_ID_APPLE)
|
||||||
|
SETCONSTANT(PROVIDER_ID_FACEBOOK)
|
||||||
|
|
||||||
|
#undef SETCONSTANT
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // DM_PLATFORM_HTML5 || DM_PLATFORM_ANDROID || DM_PLATFORM_IOS
|
28
extension-iap/src/iap_private.h
Normal file
28
extension-iap/src/iap_private.h
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#if defined(DM_PLATFORM_HTML5) || defined(DM_PLATFORM_ANDROID) || defined(DM_PLATFORM_IOS)
|
||||||
|
|
||||||
|
#ifndef IAP_PRIVATE_H
|
||||||
|
#define IAP_PRIVATE_H
|
||||||
|
|
||||||
|
#include <dmsdk/sdk.h>
|
||||||
|
|
||||||
|
struct IAPListener
|
||||||
|
{
|
||||||
|
IAPListener()
|
||||||
|
{
|
||||||
|
m_L = 0;
|
||||||
|
m_Callback = LUA_NOREF;
|
||||||
|
m_Self = LUA_NOREF;
|
||||||
|
}
|
||||||
|
lua_State* m_L;
|
||||||
|
int m_Callback;
|
||||||
|
int m_Self;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
char* IAP_List_CreateBuffer(lua_State* L);
|
||||||
|
void IAP_PushError(lua_State* L, const char* error, int reason);
|
||||||
|
void IAP_PushConstants(lua_State* L);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // DM_PLATFORM_HTML5 || DM_PLATFORM_ANDROID || DM_PLATFORM_IOS
|
@ -0,0 +1,501 @@
|
|||||||
|
/*
|
||||||
|
* This file is auto-generated. DO NOT MODIFY.
|
||||||
|
* Original file: /Users/chmu/android_workspace/TestBilling/src/com/android/vending/billing/IInAppBillingService.aidl
|
||||||
|
*/
|
||||||
|
package com.android.vending.billing;
|
||||||
|
/**
|
||||||
|
* InAppBillingService is the service that provides in-app billing version 3 and beyond.
|
||||||
|
* This service provides the following features:
|
||||||
|
* 1. Provides a new API to get details of in-app items published for the app including
|
||||||
|
* price, type, title and description.
|
||||||
|
* 2. The purchase flow is synchronous and purchase information is available immediately
|
||||||
|
* after it completes.
|
||||||
|
* 3. Purchase information of in-app purchases is maintained within the Google Play system
|
||||||
|
* till the purchase is consumed.
|
||||||
|
* 4. An API to consume a purchase of an inapp item. All purchases of one-time
|
||||||
|
* in-app items are consumable and thereafter can be purchased again.
|
||||||
|
* 5. An API to get current purchases of the user immediately. This will not contain any
|
||||||
|
* consumed purchases.
|
||||||
|
*
|
||||||
|
* All calls will give a response code with the following possible values
|
||||||
|
* RESULT_OK = 0 - success
|
||||||
|
* RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog
|
||||||
|
* RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested
|
||||||
|
* RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase
|
||||||
|
* RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API
|
||||||
|
* RESULT_ERROR = 6 - Fatal error during the API action
|
||||||
|
* RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
|
||||||
|
* RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
|
||||||
|
*/
|
||||||
|
public interface IInAppBillingService extends android.os.IInterface
|
||||||
|
{
|
||||||
|
/** Local-side IPC implementation stub class. */
|
||||||
|
public static abstract class Stub extends android.os.Binder implements com.android.vending.billing.IInAppBillingService
|
||||||
|
{
|
||||||
|
private static final java.lang.String DESCRIPTOR = "com.android.vending.billing.IInAppBillingService";
|
||||||
|
/** Construct the stub at attach it to the interface. */
|
||||||
|
public Stub()
|
||||||
|
{
|
||||||
|
this.attachInterface(this, DESCRIPTOR);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Cast an IBinder object into an com.android.vending.billing.IInAppBillingService interface,
|
||||||
|
* generating a proxy if needed.
|
||||||
|
*/
|
||||||
|
public static com.android.vending.billing.IInAppBillingService asInterface(android.os.IBinder obj)
|
||||||
|
{
|
||||||
|
if ((obj==null)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
|
||||||
|
if (((iin!=null)&&(iin instanceof com.android.vending.billing.IInAppBillingService))) {
|
||||||
|
return ((com.android.vending.billing.IInAppBillingService)iin);
|
||||||
|
}
|
||||||
|
return new com.android.vending.billing.IInAppBillingService.Stub.Proxy(obj);
|
||||||
|
}
|
||||||
|
@Override public android.os.IBinder asBinder()
|
||||||
|
{
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
switch (code)
|
||||||
|
{
|
||||||
|
case INTERFACE_TRANSACTION:
|
||||||
|
{
|
||||||
|
reply.writeString(DESCRIPTOR);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case TRANSACTION_isBillingSupported:
|
||||||
|
{
|
||||||
|
data.enforceInterface(DESCRIPTOR);
|
||||||
|
int _arg0;
|
||||||
|
_arg0 = data.readInt();
|
||||||
|
java.lang.String _arg1;
|
||||||
|
_arg1 = data.readString();
|
||||||
|
java.lang.String _arg2;
|
||||||
|
_arg2 = data.readString();
|
||||||
|
int _result = this.isBillingSupported(_arg0, _arg1, _arg2);
|
||||||
|
reply.writeNoException();
|
||||||
|
reply.writeInt(_result);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case TRANSACTION_getSkuDetails:
|
||||||
|
{
|
||||||
|
data.enforceInterface(DESCRIPTOR);
|
||||||
|
int _arg0;
|
||||||
|
_arg0 = data.readInt();
|
||||||
|
java.lang.String _arg1;
|
||||||
|
_arg1 = data.readString();
|
||||||
|
java.lang.String _arg2;
|
||||||
|
_arg2 = data.readString();
|
||||||
|
android.os.Bundle _arg3;
|
||||||
|
if ((0!=data.readInt())) {
|
||||||
|
_arg3 = android.os.Bundle.CREATOR.createFromParcel(data);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_arg3 = null;
|
||||||
|
}
|
||||||
|
android.os.Bundle _result = this.getSkuDetails(_arg0, _arg1, _arg2, _arg3);
|
||||||
|
reply.writeNoException();
|
||||||
|
if ((_result!=null)) {
|
||||||
|
reply.writeInt(1);
|
||||||
|
_result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
reply.writeInt(0);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case TRANSACTION_getBuyIntent:
|
||||||
|
{
|
||||||
|
data.enforceInterface(DESCRIPTOR);
|
||||||
|
int _arg0;
|
||||||
|
_arg0 = data.readInt();
|
||||||
|
java.lang.String _arg1;
|
||||||
|
_arg1 = data.readString();
|
||||||
|
java.lang.String _arg2;
|
||||||
|
_arg2 = data.readString();
|
||||||
|
java.lang.String _arg3;
|
||||||
|
_arg3 = data.readString();
|
||||||
|
java.lang.String _arg4;
|
||||||
|
_arg4 = data.readString();
|
||||||
|
android.os.Bundle _result = this.getBuyIntent(_arg0, _arg1, _arg2, _arg3, _arg4);
|
||||||
|
reply.writeNoException();
|
||||||
|
if ((_result!=null)) {
|
||||||
|
reply.writeInt(1);
|
||||||
|
_result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
reply.writeInt(0);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case TRANSACTION_getPurchases:
|
||||||
|
{
|
||||||
|
data.enforceInterface(DESCRIPTOR);
|
||||||
|
int _arg0;
|
||||||
|
_arg0 = data.readInt();
|
||||||
|
java.lang.String _arg1;
|
||||||
|
_arg1 = data.readString();
|
||||||
|
java.lang.String _arg2;
|
||||||
|
_arg2 = data.readString();
|
||||||
|
java.lang.String _arg3;
|
||||||
|
_arg3 = data.readString();
|
||||||
|
android.os.Bundle _result = this.getPurchases(_arg0, _arg1, _arg2, _arg3);
|
||||||
|
reply.writeNoException();
|
||||||
|
if ((_result!=null)) {
|
||||||
|
reply.writeInt(1);
|
||||||
|
_result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
reply.writeInt(0);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case TRANSACTION_consumePurchase:
|
||||||
|
{
|
||||||
|
data.enforceInterface(DESCRIPTOR);
|
||||||
|
int _arg0;
|
||||||
|
_arg0 = data.readInt();
|
||||||
|
java.lang.String _arg1;
|
||||||
|
_arg1 = data.readString();
|
||||||
|
java.lang.String _arg2;
|
||||||
|
_arg2 = data.readString();
|
||||||
|
int _result = this.consumePurchase(_arg0, _arg1, _arg2);
|
||||||
|
reply.writeNoException();
|
||||||
|
reply.writeInt(_result);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onTransact(code, data, reply, flags);
|
||||||
|
}
|
||||||
|
private static class Proxy implements com.android.vending.billing.IInAppBillingService
|
||||||
|
{
|
||||||
|
private android.os.IBinder mRemote;
|
||||||
|
Proxy(android.os.IBinder remote)
|
||||||
|
{
|
||||||
|
mRemote = remote;
|
||||||
|
}
|
||||||
|
@Override public android.os.IBinder asBinder()
|
||||||
|
{
|
||||||
|
return mRemote;
|
||||||
|
}
|
||||||
|
public java.lang.String getInterfaceDescriptor()
|
||||||
|
{
|
||||||
|
return DESCRIPTOR;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Checks support for the requested billing API version, package and in-app type.
|
||||||
|
* Minimum API version supported by this interface is 3.
|
||||||
|
* @param apiVersion the billing version which the app is using
|
||||||
|
* @param packageName the package name of the calling app
|
||||||
|
* @param type type of the in-app item being purchased "inapp" for one-time purchases
|
||||||
|
* and "subs" for subscription.
|
||||||
|
* @return RESULT_OK(0) on success, corresponding result code on failures
|
||||||
|
*/
|
||||||
|
@Override public int isBillingSupported(int apiVersion, java.lang.String packageName, java.lang.String type) throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
android.os.Parcel _data = android.os.Parcel.obtain();
|
||||||
|
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||||
|
int _result;
|
||||||
|
try {
|
||||||
|
_data.writeInterfaceToken(DESCRIPTOR);
|
||||||
|
_data.writeInt(apiVersion);
|
||||||
|
_data.writeString(packageName);
|
||||||
|
_data.writeString(type);
|
||||||
|
mRemote.transact(Stub.TRANSACTION_isBillingSupported, _data, _reply, 0);
|
||||||
|
_reply.readException();
|
||||||
|
_result = _reply.readInt();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_reply.recycle();
|
||||||
|
_data.recycle();
|
||||||
|
}
|
||||||
|
return _result;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Provides details of a list of SKUs
|
||||||
|
* Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
|
||||||
|
* with a list JSON strings containing the productId, price, title and description.
|
||||||
|
* This API can be called with a maximum of 20 SKUs.
|
||||||
|
* @param apiVersion billing API version that the Third-party is using
|
||||||
|
* @param packageName the package name of the calling app
|
||||||
|
* @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
|
||||||
|
* @return Bundle containing the following key-value pairs
|
||||||
|
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
||||||
|
* failure as listed above.
|
||||||
|
* "DETAILS_LIST" with a StringArrayList containing purchase information
|
||||||
|
* in JSON format similar to:
|
||||||
|
* '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
|
||||||
|
* "title : "Example Title", "description" : "This is an example description" }'
|
||||||
|
*/
|
||||||
|
@Override public android.os.Bundle getSkuDetails(int apiVersion, java.lang.String packageName, java.lang.String type, android.os.Bundle skusBundle) throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
android.os.Parcel _data = android.os.Parcel.obtain();
|
||||||
|
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||||
|
android.os.Bundle _result;
|
||||||
|
try {
|
||||||
|
_data.writeInterfaceToken(DESCRIPTOR);
|
||||||
|
_data.writeInt(apiVersion);
|
||||||
|
_data.writeString(packageName);
|
||||||
|
_data.writeString(type);
|
||||||
|
if ((skusBundle!=null)) {
|
||||||
|
_data.writeInt(1);
|
||||||
|
skusBundle.writeToParcel(_data, 0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_data.writeInt(0);
|
||||||
|
}
|
||||||
|
mRemote.transact(Stub.TRANSACTION_getSkuDetails, _data, _reply, 0);
|
||||||
|
_reply.readException();
|
||||||
|
if ((0!=_reply.readInt())) {
|
||||||
|
_result = android.os.Bundle.CREATOR.createFromParcel(_reply);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_result = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_reply.recycle();
|
||||||
|
_data.recycle();
|
||||||
|
}
|
||||||
|
return _result;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
|
||||||
|
* the type, a unique purchase token and an optional developer payload.
|
||||||
|
* @param apiVersion billing API version that the app is using
|
||||||
|
* @param packageName package name of the calling app
|
||||||
|
* @param sku the SKU of the in-app item as published in the developer console
|
||||||
|
* @param type the type of the in-app item ("inapp" for one-time purchases
|
||||||
|
* and "subs" for subscription).
|
||||||
|
* @param developerPayload optional argument to be sent back with the purchase information
|
||||||
|
* @return Bundle containing the following key-value pairs
|
||||||
|
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
||||||
|
* failure as listed above.
|
||||||
|
* "BUY_INTENT" - PendingIntent to start the purchase flow
|
||||||
|
*
|
||||||
|
* The Pending intent should be launched with startIntentSenderForResult. When purchase flow
|
||||||
|
* has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
|
||||||
|
* If the purchase is successful, the result data will contain the following key-value pairs
|
||||||
|
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
||||||
|
* failure as listed above.
|
||||||
|
* "INAPP_PURCHASE_DATA" - String in JSON format similar to
|
||||||
|
* '{"orderId":"12999763169054705758.1371079406387615",
|
||||||
|
* "packageName":"com.example.app",
|
||||||
|
* "productId":"exampleSku",
|
||||||
|
* "purchaseTime":1345678900000,
|
||||||
|
* "purchaseToken" : "122333444455555",
|
||||||
|
* "developerPayload":"example developer payload" }'
|
||||||
|
* "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
|
||||||
|
* was signed with the private key of the developer
|
||||||
|
* TODO: change this to app-specific keys.
|
||||||
|
*/
|
||||||
|
@Override public android.os.Bundle getBuyIntent(int apiVersion, java.lang.String packageName, java.lang.String sku, java.lang.String type, java.lang.String developerPayload) throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
android.os.Parcel _data = android.os.Parcel.obtain();
|
||||||
|
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||||
|
android.os.Bundle _result;
|
||||||
|
try {
|
||||||
|
_data.writeInterfaceToken(DESCRIPTOR);
|
||||||
|
_data.writeInt(apiVersion);
|
||||||
|
_data.writeString(packageName);
|
||||||
|
_data.writeString(sku);
|
||||||
|
_data.writeString(type);
|
||||||
|
_data.writeString(developerPayload);
|
||||||
|
mRemote.transact(Stub.TRANSACTION_getBuyIntent, _data, _reply, 0);
|
||||||
|
_reply.readException();
|
||||||
|
if ((0!=_reply.readInt())) {
|
||||||
|
_result = android.os.Bundle.CREATOR.createFromParcel(_reply);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_result = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_reply.recycle();
|
||||||
|
_data.recycle();
|
||||||
|
}
|
||||||
|
return _result;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Returns the current SKUs owned by the user of the type and package name specified along with
|
||||||
|
* purchase information and a signature of the data to be validated.
|
||||||
|
* This will return all SKUs that have been purchased in V3 and managed items purchased using
|
||||||
|
* V1 and V2 that have not been consumed.
|
||||||
|
* @param apiVersion billing API version that the app is using
|
||||||
|
* @param packageName package name of the calling app
|
||||||
|
* @param type the type of the in-app items being requested
|
||||||
|
* ("inapp" for one-time purchases and "subs" for subscription).
|
||||||
|
* @param continuationToken to be set as null for the first call, if the number of owned
|
||||||
|
* skus are too many, a continuationToken is returned in the response bundle.
|
||||||
|
* This method can be called again with the continuation token to get the next set of
|
||||||
|
* owned skus.
|
||||||
|
* @return Bundle containing the following key-value pairs
|
||||||
|
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
||||||
|
* failure as listed above.
|
||||||
|
* "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
|
||||||
|
* "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
|
||||||
|
* "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
|
||||||
|
* of the purchase information
|
||||||
|
* "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
|
||||||
|
* next set of in-app purchases. Only set if the
|
||||||
|
* user has more owned skus than the current list.
|
||||||
|
*/
|
||||||
|
@Override public android.os.Bundle getPurchases(int apiVersion, java.lang.String packageName, java.lang.String type, java.lang.String continuationToken) throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
android.os.Parcel _data = android.os.Parcel.obtain();
|
||||||
|
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||||
|
android.os.Bundle _result;
|
||||||
|
try {
|
||||||
|
_data.writeInterfaceToken(DESCRIPTOR);
|
||||||
|
_data.writeInt(apiVersion);
|
||||||
|
_data.writeString(packageName);
|
||||||
|
_data.writeString(type);
|
||||||
|
_data.writeString(continuationToken);
|
||||||
|
mRemote.transact(Stub.TRANSACTION_getPurchases, _data, _reply, 0);
|
||||||
|
_reply.readException();
|
||||||
|
if ((0!=_reply.readInt())) {
|
||||||
|
_result = android.os.Bundle.CREATOR.createFromParcel(_reply);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_result = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_reply.recycle();
|
||||||
|
_data.recycle();
|
||||||
|
}
|
||||||
|
return _result;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Consume the last purchase of the given SKU. This will result in this item being removed
|
||||||
|
* from all subsequent responses to getPurchases() and allow re-purchase of this item.
|
||||||
|
* @param apiVersion billing API version that the app is using
|
||||||
|
* @param packageName package name of the calling app
|
||||||
|
* @param purchaseToken token in the purchase information JSON that identifies the purchase
|
||||||
|
* to be consumed
|
||||||
|
* @return 0 if consumption succeeded. Appropriate error values for failures.
|
||||||
|
*/
|
||||||
|
@Override public int consumePurchase(int apiVersion, java.lang.String packageName, java.lang.String purchaseToken) throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
android.os.Parcel _data = android.os.Parcel.obtain();
|
||||||
|
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||||
|
int _result;
|
||||||
|
try {
|
||||||
|
_data.writeInterfaceToken(DESCRIPTOR);
|
||||||
|
_data.writeInt(apiVersion);
|
||||||
|
_data.writeString(packageName);
|
||||||
|
_data.writeString(purchaseToken);
|
||||||
|
mRemote.transact(Stub.TRANSACTION_consumePurchase, _data, _reply, 0);
|
||||||
|
_reply.readException();
|
||||||
|
_result = _reply.readInt();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_reply.recycle();
|
||||||
|
_data.recycle();
|
||||||
|
}
|
||||||
|
return _result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static final int TRANSACTION_isBillingSupported = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
|
||||||
|
static final int TRANSACTION_getSkuDetails = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
|
||||||
|
static final int TRANSACTION_getBuyIntent = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
|
||||||
|
static final int TRANSACTION_getPurchases = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
|
||||||
|
static final int TRANSACTION_consumePurchase = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Checks support for the requested billing API version, package and in-app type.
|
||||||
|
* Minimum API version supported by this interface is 3.
|
||||||
|
* @param apiVersion the billing version which the app is using
|
||||||
|
* @param packageName the package name of the calling app
|
||||||
|
* @param type type of the in-app item being purchased "inapp" for one-time purchases
|
||||||
|
* and "subs" for subscription.
|
||||||
|
* @return RESULT_OK(0) on success, corresponding result code on failures
|
||||||
|
*/
|
||||||
|
public int isBillingSupported(int apiVersion, java.lang.String packageName, java.lang.String type) throws android.os.RemoteException;
|
||||||
|
/**
|
||||||
|
* Provides details of a list of SKUs
|
||||||
|
* Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
|
||||||
|
* with a list JSON strings containing the productId, price, title and description.
|
||||||
|
* This API can be called with a maximum of 20 SKUs.
|
||||||
|
* @param apiVersion billing API version that the Third-party is using
|
||||||
|
* @param packageName the package name of the calling app
|
||||||
|
* @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
|
||||||
|
* @return Bundle containing the following key-value pairs
|
||||||
|
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
||||||
|
* failure as listed above.
|
||||||
|
* "DETAILS_LIST" with a StringArrayList containing purchase information
|
||||||
|
* in JSON format similar to:
|
||||||
|
* '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
|
||||||
|
* "title : "Example Title", "description" : "This is an example description" }'
|
||||||
|
*/
|
||||||
|
public android.os.Bundle getSkuDetails(int apiVersion, java.lang.String packageName, java.lang.String type, android.os.Bundle skusBundle) throws android.os.RemoteException;
|
||||||
|
/**
|
||||||
|
* Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
|
||||||
|
* the type, a unique purchase token and an optional developer payload.
|
||||||
|
* @param apiVersion billing API version that the app is using
|
||||||
|
* @param packageName package name of the calling app
|
||||||
|
* @param sku the SKU of the in-app item as published in the developer console
|
||||||
|
* @param type the type of the in-app item ("inapp" for one-time purchases
|
||||||
|
* and "subs" for subscription).
|
||||||
|
* @param developerPayload optional argument to be sent back with the purchase information
|
||||||
|
* @return Bundle containing the following key-value pairs
|
||||||
|
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
||||||
|
* failure as listed above.
|
||||||
|
* "BUY_INTENT" - PendingIntent to start the purchase flow
|
||||||
|
*
|
||||||
|
* The Pending intent should be launched with startIntentSenderForResult. When purchase flow
|
||||||
|
* has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
|
||||||
|
* If the purchase is successful, the result data will contain the following key-value pairs
|
||||||
|
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
||||||
|
* failure as listed above.
|
||||||
|
* "INAPP_PURCHASE_DATA" - String in JSON format similar to
|
||||||
|
* '{"orderId":"12999763169054705758.1371079406387615",
|
||||||
|
* "packageName":"com.example.app",
|
||||||
|
* "productId":"exampleSku",
|
||||||
|
* "purchaseTime":1345678900000,
|
||||||
|
* "purchaseToken" : "122333444455555",
|
||||||
|
* "developerPayload":"example developer payload" }'
|
||||||
|
* "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
|
||||||
|
* was signed with the private key of the developer
|
||||||
|
* TODO: change this to app-specific keys.
|
||||||
|
*/
|
||||||
|
public android.os.Bundle getBuyIntent(int apiVersion, java.lang.String packageName, java.lang.String sku, java.lang.String type, java.lang.String developerPayload) throws android.os.RemoteException;
|
||||||
|
/**
|
||||||
|
* Returns the current SKUs owned by the user of the type and package name specified along with
|
||||||
|
* purchase information and a signature of the data to be validated.
|
||||||
|
* This will return all SKUs that have been purchased in V3 and managed items purchased using
|
||||||
|
* V1 and V2 that have not been consumed.
|
||||||
|
* @param apiVersion billing API version that the app is using
|
||||||
|
* @param packageName package name of the calling app
|
||||||
|
* @param type the type of the in-app items being requested
|
||||||
|
* ("inapp" for one-time purchases and "subs" for subscription).
|
||||||
|
* @param continuationToken to be set as null for the first call, if the number of owned
|
||||||
|
* skus are too many, a continuationToken is returned in the response bundle.
|
||||||
|
* This method can be called again with the continuation token to get the next set of
|
||||||
|
* owned skus.
|
||||||
|
* @return Bundle containing the following key-value pairs
|
||||||
|
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
||||||
|
* failure as listed above.
|
||||||
|
* "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
|
||||||
|
* "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
|
||||||
|
* "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
|
||||||
|
* of the purchase information
|
||||||
|
* "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
|
||||||
|
* next set of in-app purchases. Only set if the
|
||||||
|
* user has more owned skus than the current list.
|
||||||
|
*/
|
||||||
|
public android.os.Bundle getPurchases(int apiVersion, java.lang.String packageName, java.lang.String type, java.lang.String continuationToken) throws android.os.RemoteException;
|
||||||
|
/**
|
||||||
|
* Consume the last purchase of the given SKU. This will result in this item being removed
|
||||||
|
* from all subsequent responses to getPurchases() and allow re-purchase of this item.
|
||||||
|
* @param apiVersion billing API version that the app is using
|
||||||
|
* @param packageName package name of the calling app
|
||||||
|
* @param purchaseToken token in the purchase information JSON that identifies the purchase
|
||||||
|
* to be consumed
|
||||||
|
* @return 0 if consumption succeeded. Appropriate error values for failures.
|
||||||
|
*/
|
||||||
|
public int consumePurchase(int apiVersion, java.lang.String packageName, java.lang.String purchaseToken) throws android.os.RemoteException;
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.defold.iap;
|
||||||
|
|
||||||
|
public interface IListProductsListener {
|
||||||
|
public void onProductsResult(int resultCode, String productList);
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.defold.iap;
|
||||||
|
|
||||||
|
public interface IPurchaseListener {
|
||||||
|
public void onPurchaseResult(int responseCode, String purchaseData);
|
||||||
|
}
|
307
extension-iap/src/java/com/defold/iap/IapAmazon.java
Normal file
307
extension-iap/src/java/com/defold/iap/IapAmazon.java
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
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 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, 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.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 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);
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Did not expect a null requestId");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void buy(final String product, 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
synchronized (this.listProductsListeners) {
|
||||||
|
listener = this.listProductsListeners.get(reqId);
|
||||||
|
if (listener == null) {
|
||||||
|
Log.e(TAG, "No listener found for request " + reqId.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.listProductsListeners.remove(reqId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (productDataResponse.getRequestStatus() != ProductDataResponse.RequestStatus.SUCCESSFUL) {
|
||||||
|
listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
|
||||||
|
} else {
|
||||||
|
Map<String, Product> products = productDataResponse.getProductData();
|
||||||
|
try {
|
||||||
|
JSONObject data = new JSONObject();
|
||||||
|
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(key, item);
|
||||||
|
}
|
||||||
|
listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_OK, data.toString());
|
||||||
|
} catch (JSONException e) {
|
||||||
|
listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
}
|
||||||
|
}
|
496
extension-iap/src/java/com/defold/iap/IapGooglePlay.java
Normal file
496
extension-iap/src/java/com/defold/iap/IapGooglePlay.java
Normal file
@ -0,0 +1,496 @@
|
|||||||
|
package com.defold.iap;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.content.pm.ResolveInfo;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.os.Messenger;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.android.vending.billing.IInAppBillingService;
|
||||||
|
|
||||||
|
public class IapGooglePlay implements Handler.Callback {
|
||||||
|
public static final String PARAM_PRODUCT = "product";
|
||||||
|
public static final String PARAM_PRODUCT_TYPE = "product_type";
|
||||||
|
public static final String PARAM_PURCHASE_DATA = "purchase_data";
|
||||||
|
public static final String PARAM_AUTOFINISH_TRANSACTIONS = "auto_finish_transactions";
|
||||||
|
public static final String PARAM_MESSENGER = "com.defold.iap.messenger";
|
||||||
|
|
||||||
|
public static final String RESPONSE_CODE = "RESPONSE_CODE";
|
||||||
|
public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST";
|
||||||
|
public static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
|
||||||
|
public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA";
|
||||||
|
public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE";
|
||||||
|
public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST";
|
||||||
|
public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
|
||||||
|
public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
|
||||||
|
public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
|
||||||
|
|
||||||
|
public static enum Action {
|
||||||
|
BUY,
|
||||||
|
RESTORE,
|
||||||
|
PROCESS_PENDING_CONSUMABLES,
|
||||||
|
FINISH_TRANSACTION
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String TAG = "iap";
|
||||||
|
|
||||||
|
private Activity activity;
|
||||||
|
private Handler handler;
|
||||||
|
private Messenger messenger;
|
||||||
|
private ServiceConnection serviceConn;
|
||||||
|
private IInAppBillingService service;
|
||||||
|
|
||||||
|
private SkuDetailsThread skuDetailsThread;
|
||||||
|
private BlockingQueue<SkuRequest> skuRequestQueue = new ArrayBlockingQueue<SkuRequest>(16);
|
||||||
|
|
||||||
|
private IPurchaseListener purchaseListener;
|
||||||
|
private boolean initialized;
|
||||||
|
private boolean autoFinishTransactions;
|
||||||
|
|
||||||
|
private static interface ISkuRequestListener {
|
||||||
|
public void onProducts(int resultCode, JSONObject products);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SkuRequest {
|
||||||
|
private ArrayList<String> skuList;
|
||||||
|
private ISkuRequestListener listener;
|
||||||
|
|
||||||
|
public SkuRequest(ArrayList<String> skuList, ISkuRequestListener listener) {
|
||||||
|
this.skuList = skuList;
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SkuDetailsThread extends Thread {
|
||||||
|
public boolean stop = false;
|
||||||
|
|
||||||
|
private void addProductsFromBundle(Bundle skuDetails, JSONObject products) throws JSONException {
|
||||||
|
int response = skuDetails.getInt("RESPONSE_CODE");
|
||||||
|
if (response == IapJNI.BILLING_RESPONSE_RESULT_OK) {
|
||||||
|
ArrayList<String> responseList = skuDetails.getStringArrayList("DETAILS_LIST");
|
||||||
|
|
||||||
|
for (String r : responseList) {
|
||||||
|
JSONObject product = new JSONObject(r);
|
||||||
|
products.put(product.getString("productId"), product);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log.e(TAG, "Failed to fetch product list: " + response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (!stop) {
|
||||||
|
try {
|
||||||
|
SkuRequest sr = skuRequestQueue.take();
|
||||||
|
if (service == null) {
|
||||||
|
Log.wtf(TAG, "service is null");
|
||||||
|
sr.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (activity == null) {
|
||||||
|
Log.wtf(TAG, "activity is null");
|
||||||
|
sr.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String packageName = activity.getPackageName();
|
||||||
|
if (packageName == null)
|
||||||
|
{
|
||||||
|
Log.wtf(TAG, "activity packageName is null");
|
||||||
|
sr.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Bundle querySkus = new Bundle();
|
||||||
|
querySkus.putStringArrayList("ITEM_ID_LIST", sr.skuList);
|
||||||
|
|
||||||
|
JSONObject products = new JSONObject();
|
||||||
|
|
||||||
|
Bundle inappSkuDetails = service.getSkuDetails(3, packageName, "inapp", querySkus);
|
||||||
|
addProductsFromBundle(inappSkuDetails, products);
|
||||||
|
|
||||||
|
Bundle subscriptionSkuDetails = service.getSkuDetails(3, packageName, "subs", querySkus);
|
||||||
|
addProductsFromBundle(subscriptionSkuDetails, products);
|
||||||
|
|
||||||
|
sr.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_OK, products);
|
||||||
|
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.e(TAG, "Failed to fetch product list", e);
|
||||||
|
sr.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
Log.e(TAG, "Failed to fetch product list", e);
|
||||||
|
sr.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IapGooglePlay(Activity activity, boolean autoFinishTransactions) {
|
||||||
|
this.activity = activity;
|
||||||
|
this.autoFinishTransactions = autoFinishTransactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init() {
|
||||||
|
// NOTE: We must create Handler lazily as construction of
|
||||||
|
// handlers must be in the context of a "looper" on Android
|
||||||
|
|
||||||
|
if (this.initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
this.handler = new Handler(this);
|
||||||
|
this.messenger = new Messenger(this.handler);
|
||||||
|
|
||||||
|
serviceConn = new ServiceConnection() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
|
Log.v(TAG, "IAP disconnected");
|
||||||
|
service = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(ComponentName name, IBinder binderService) {
|
||||||
|
Log.v(TAG, "IAP connected");
|
||||||
|
service = IInAppBillingService.Stub.asInterface(binderService);
|
||||||
|
skuDetailsThread = new SkuDetailsThread();
|
||||||
|
skuDetailsThread.start();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
|
||||||
|
// Limit intent to vending package
|
||||||
|
serviceIntent.setPackage("com.android.vending");
|
||||||
|
List<ResolveInfo> intentServices = activity.getPackageManager().queryIntentServices(serviceIntent, 0);
|
||||||
|
if (intentServices != null && !intentServices.isEmpty()) {
|
||||||
|
// service available to handle that Intent
|
||||||
|
activity.bindService(serviceIntent, serviceConn, Context.BIND_AUTO_CREATE);
|
||||||
|
} else {
|
||||||
|
serviceConn = null;
|
||||||
|
Log.e(TAG, "Billing service unavailable on device.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
this.activity.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (serviceConn != null) {
|
||||||
|
activity.unbindService(serviceConn);
|
||||||
|
serviceConn = null;
|
||||||
|
}
|
||||||
|
if (skuDetailsThread != null) {
|
||||||
|
skuDetailsThread.stop = true;
|
||||||
|
skuDetailsThread.interrupt();
|
||||||
|
try {
|
||||||
|
skuDetailsThread.join();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Log.wtf(TAG, "Failed to join thread", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void queueSkuRequest(final SkuRequest request) {
|
||||||
|
this.activity.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
init();
|
||||||
|
|
||||||
|
if (serviceConn != null) {
|
||||||
|
try {
|
||||||
|
skuRequestQueue.put(request);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Log.wtf(TAG, "Failed to add sku request", e);
|
||||||
|
request.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE, null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
request.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void listItems(final String skus, final IListProductsListener listener) {
|
||||||
|
ArrayList<String> skuList = new ArrayList<String>();
|
||||||
|
for (String x : skus.split(",")) {
|
||||||
|
if (x.trim().length() > 0) {
|
||||||
|
skuList.add(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queueSkuRequest(new SkuRequest(skuList, new ISkuRequestListener() {
|
||||||
|
@Override
|
||||||
|
public void onProducts(int resultCode, JSONObject products) {
|
||||||
|
if (products != null && products.length() > 0) {
|
||||||
|
try {
|
||||||
|
// go through all of the products and convert them into
|
||||||
|
// the generic product format used for all IAP implementations
|
||||||
|
Iterator<String> keys = products.keys();
|
||||||
|
while(keys.hasNext()) {
|
||||||
|
String key = keys.next();
|
||||||
|
if (products.get(key) instanceof JSONObject ) {
|
||||||
|
JSONObject product = products.getJSONObject(key);
|
||||||
|
products.put(key, convertProduct(product));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listener.onProductsResult(resultCode, products.toString());
|
||||||
|
}
|
||||||
|
catch(JSONException e) {
|
||||||
|
Log.wtf(TAG, "Failed to convert products", e);
|
||||||
|
listener.onProductsResult(resultCode, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
listener.onProductsResult(resultCode, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the product data into the generic format shared between all Defold IAP implementations
|
||||||
|
private static JSONObject convertProduct(JSONObject product) {
|
||||||
|
try {
|
||||||
|
// Deep copy and modify
|
||||||
|
JSONObject p = new JSONObject(product.toString());
|
||||||
|
p.put("price_string", p.get("price"));
|
||||||
|
p.put("ident", p.get("productId"));
|
||||||
|
// It is not yet possible to obtain the price (num) and currency code on Android for the correct locale/region.
|
||||||
|
// They have a currency code (price_currency_code), which reflects the merchant's locale, instead of the user's
|
||||||
|
// https://code.google.com/p/marketbilling/issues/detail?id=93&q=currency%20code&colspec=ID%20Type%20Status%20Google%20Priority%20Milestone%20Owner%20Summary
|
||||||
|
double price = 0.0;
|
||||||
|
if (p.has("price_amount_micros")) {
|
||||||
|
price = p.getLong("price_amount_micros") * 0.000001;
|
||||||
|
}
|
||||||
|
String currency_code = "Unknown";
|
||||||
|
if (p.has("price_currency_code")) {
|
||||||
|
currency_code = (String)p.get("price_currency_code");
|
||||||
|
}
|
||||||
|
p.put("currency_code", currency_code);
|
||||||
|
p.put("price", price);
|
||||||
|
|
||||||
|
p.remove("productId");
|
||||||
|
p.remove("type");
|
||||||
|
p.remove("price_amount_micros");
|
||||||
|
p.remove("price_currency_code");
|
||||||
|
return p;
|
||||||
|
} catch (JSONException e) {
|
||||||
|
Log.wtf(TAG, "Failed to convert product json", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buyProduct(final String product, final String type, final IPurchaseListener listener) {
|
||||||
|
this.activity.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
init();
|
||||||
|
IapGooglePlay.this.purchaseListener = listener;
|
||||||
|
Intent intent = new Intent(activity, IapGooglePlayActivity.class);
|
||||||
|
intent.putExtra(PARAM_MESSENGER, messenger);
|
||||||
|
intent.putExtra(PARAM_AUTOFINISH_TRANSACTIONS, IapGooglePlay.this.autoFinishTransactions);
|
||||||
|
intent.putExtra(PARAM_PRODUCT, product);
|
||||||
|
intent.putExtra(PARAM_PRODUCT_TYPE, type);
|
||||||
|
intent.setAction(Action.BUY.toString());
|
||||||
|
activity.startActivity(intent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void buy(final String product, final IPurchaseListener listener) {
|
||||||
|
ArrayList<String> skuList = new ArrayList<String>();
|
||||||
|
skuList.add(product);
|
||||||
|
queueSkuRequest(new SkuRequest(skuList, new ISkuRequestListener() {
|
||||||
|
@Override
|
||||||
|
public void onProducts(int resultCode, JSONObject products) {
|
||||||
|
String type = "inapp";
|
||||||
|
if (resultCode == IapJNI.BILLING_RESPONSE_RESULT_OK && products != null) {
|
||||||
|
try {
|
||||||
|
JSONObject productData = products.getJSONObject(product);
|
||||||
|
type = productData.getString("type");
|
||||||
|
}
|
||||||
|
catch(JSONException e) {
|
||||||
|
Log.wtf(TAG, "Failed to get product type before buying, assuming type 'inapp'", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log.wtf(TAG, "Failed to list product before buying, assuming type 'inapp'");
|
||||||
|
}
|
||||||
|
buyProduct(product, type, listener);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void finishTransaction(final String receipt, final IPurchaseListener listener) {
|
||||||
|
if(IapGooglePlay.this.autoFinishTransactions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.activity.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
init();
|
||||||
|
IapGooglePlay.this.purchaseListener = listener;
|
||||||
|
Intent intent = new Intent(activity, IapGooglePlayActivity.class);
|
||||||
|
intent.putExtra(PARAM_MESSENGER, messenger);
|
||||||
|
intent.putExtra(PARAM_AUTOFINISH_TRANSACTIONS, false);
|
||||||
|
intent.putExtra(PARAM_PURCHASE_DATA, receipt);
|
||||||
|
intent.setAction(Action.FINISH_TRANSACTION.toString());
|
||||||
|
activity.startActivity(intent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processPendingConsumables(final IPurchaseListener listener) {
|
||||||
|
this.activity.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
init();
|
||||||
|
IapGooglePlay.this.purchaseListener = listener;
|
||||||
|
Intent intent = new Intent(activity, IapGooglePlayActivity.class);
|
||||||
|
intent.putExtra(PARAM_MESSENGER, messenger);
|
||||||
|
intent.putExtra(PARAM_AUTOFINISH_TRANSACTIONS, IapGooglePlay.this.autoFinishTransactions);
|
||||||
|
intent.setAction(Action.PROCESS_PENDING_CONSUMABLES.toString());
|
||||||
|
activity.startActivity(intent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void restore(final IPurchaseListener listener) {
|
||||||
|
this.activity.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
init();
|
||||||
|
IapGooglePlay.this.purchaseListener = listener;
|
||||||
|
Intent intent = new Intent(activity, IapGooglePlayActivity.class);
|
||||||
|
intent.putExtra(PARAM_MESSENGER, messenger);
|
||||||
|
intent.putExtra(PARAM_AUTOFINISH_TRANSACTIONS, IapGooglePlay.this.autoFinishTransactions);
|
||||||
|
intent.setAction(Action.RESTORE.toString());
|
||||||
|
activity.startActivity(intent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 static String convertPurchase(String purchase, String signature) {
|
||||||
|
try {
|
||||||
|
JSONObject p = new JSONObject(purchase);
|
||||||
|
p.put("ident", p.get("productId"));
|
||||||
|
p.put("state", IapJNI.TRANS_STATE_PURCHASED);
|
||||||
|
|
||||||
|
// We check if orderId is actually set here, otherwise we return a blank string.
|
||||||
|
// This is what Google used to do, but after some updates around June/May 2016
|
||||||
|
// they stopped to include the orderId key at all for test purchases. See: DEF-1940
|
||||||
|
if (p.has("orderId")) {
|
||||||
|
p.put("trans_ident", p.get("orderId"));
|
||||||
|
} else {
|
||||||
|
p.put("trans_ident", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
p.put("date", toISO8601(new Date(p.getLong("purchaseTime"))));
|
||||||
|
// Receipt is the complete json data
|
||||||
|
// http://robertomurray.co.uk/blog/2013/server-side-google-play-in-app-billing-receipt-validation-and-testing/
|
||||||
|
p.put("receipt", purchase);
|
||||||
|
p.put("signature", signature);
|
||||||
|
// TODO: How to simulate original_trans on iOS?
|
||||||
|
|
||||||
|
p.remove("packageName");
|
||||||
|
p.remove("orderId");
|
||||||
|
p.remove("productId");
|
||||||
|
p.remove("developerPayload");
|
||||||
|
p.remove("purchaseTime");
|
||||||
|
p.remove("purchaseState");
|
||||||
|
p.remove("purchaseToken");
|
||||||
|
|
||||||
|
return p.toString();
|
||||||
|
|
||||||
|
} catch (JSONException e) {
|
||||||
|
Log.wtf(TAG, "Failed to convert purchase json", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handleMessage(Message msg) {
|
||||||
|
Bundle bundle = msg.getData();
|
||||||
|
|
||||||
|
String actionString = bundle.getString("action");
|
||||||
|
if (actionString == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (purchaseListener == null) {
|
||||||
|
Log.wtf(TAG, "No purchase listener set");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Action action = Action.valueOf(actionString);
|
||||||
|
|
||||||
|
if (action == Action.BUY) {
|
||||||
|
int responseCode = bundle.getInt(RESPONSE_CODE);
|
||||||
|
String purchaseData = bundle.getString(RESPONSE_INAPP_PURCHASE_DATA);
|
||||||
|
String dataSignature = bundle.getString(RESPONSE_INAPP_SIGNATURE);
|
||||||
|
|
||||||
|
if (purchaseData != null && dataSignature != null) {
|
||||||
|
purchaseData = convertPurchase(purchaseData, dataSignature);
|
||||||
|
} else {
|
||||||
|
purchaseData = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
purchaseListener.onPurchaseResult(responseCode, purchaseData);
|
||||||
|
} else if (action == Action.RESTORE) {
|
||||||
|
Bundle items = bundle.getBundle("items");
|
||||||
|
|
||||||
|
if (!items.containsKey(RESPONSE_INAPP_ITEM_LIST)) {
|
||||||
|
purchaseListener.onPurchaseResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, "");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<String> ownedSkus = items.getStringArrayList(RESPONSE_INAPP_ITEM_LIST);
|
||||||
|
ArrayList<String> purchaseDataList = items.getStringArrayList(RESPONSE_INAPP_PURCHASE_DATA_LIST);
|
||||||
|
ArrayList<String> signatureList = items.getStringArrayList(RESPONSE_INAPP_SIGNATURE_LIST);
|
||||||
|
for (int i = 0; i < ownedSkus.size(); ++i) {
|
||||||
|
int c = IapJNI.BILLING_RESPONSE_RESULT_OK;
|
||||||
|
String pd = convertPurchase(purchaseDataList.get(i), signatureList.get(i));
|
||||||
|
if (pd == null) {
|
||||||
|
pd = "";
|
||||||
|
c = IapJNI.BILLING_RESPONSE_RESULT_ERROR;
|
||||||
|
}
|
||||||
|
purchaseListener.onPurchaseResult(c, pd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
371
extension-iap/src/java/com/defold/iap/IapGooglePlayActivity.java
Normal file
371
extension-iap/src/java/com/defold/iap/IapGooglePlayActivity.java
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
package com.defold.iap;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentSender.SendIntentException;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.content.pm.ResolveInfo;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.os.Messenger;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup.LayoutParams;
|
||||||
|
|
||||||
|
import com.android.vending.billing.IInAppBillingService;
|
||||||
|
import com.defold.iap.IapGooglePlay.Action;
|
||||||
|
|
||||||
|
public class IapGooglePlayActivity extends Activity {
|
||||||
|
|
||||||
|
private boolean hasPendingPurchases = false;
|
||||||
|
private boolean autoFinishTransactions = true;
|
||||||
|
private boolean isDone = false;
|
||||||
|
private Messenger messenger;
|
||||||
|
ServiceConnection serviceConn;
|
||||||
|
IInAppBillingService service;
|
||||||
|
|
||||||
|
// NOTE: Code from "trivialdrivesample"
|
||||||
|
int getResponseCodeFromBundle(Bundle b) {
|
||||||
|
Object o = b.get(IapGooglePlay.RESPONSE_CODE);
|
||||||
|
if (o == null) {
|
||||||
|
Log.d(IapGooglePlay.TAG, "Bundle with null response code, assuming OK (known issue)");
|
||||||
|
return IapJNI.BILLING_RESPONSE_RESULT_OK;
|
||||||
|
} else if (o instanceof Integer)
|
||||||
|
return ((Integer) o).intValue();
|
||||||
|
else if (o instanceof Long)
|
||||||
|
return (int) ((Long) o).longValue();
|
||||||
|
else {
|
||||||
|
Log.e(IapGooglePlay.TAG, "Unexpected type for bundle response code.");
|
||||||
|
Log.e(IapGooglePlay.TAG, o.getClass().getName());
|
||||||
|
throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendBuyError(int error) {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putString("action", Action.BUY.toString());
|
||||||
|
|
||||||
|
bundle.putInt(IapGooglePlay.RESPONSE_CODE, error);
|
||||||
|
Message msg = new Message();
|
||||||
|
msg.setData(bundle);
|
||||||
|
|
||||||
|
try {
|
||||||
|
messenger.send(msg);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.wtf(IapGooglePlay.TAG, "Unable to send message", e);
|
||||||
|
}
|
||||||
|
this.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buy(String product, String productType) {
|
||||||
|
// Flush any pending items, in order to be able to buy the same (new) product again
|
||||||
|
processPendingConsumables();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Bundle buyIntentBundle = service.getBuyIntent(3, getPackageName(), product, productType, "");
|
||||||
|
int response = getResponseCodeFromBundle(buyIntentBundle);
|
||||||
|
if (response == IapJNI.BILLING_RESPONSE_RESULT_OK) {
|
||||||
|
hasPendingPurchases = true;
|
||||||
|
PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT");
|
||||||
|
startIntentSenderForResult(pendingIntent.getIntentSender(), 1001, new Intent(), Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0));
|
||||||
|
} else if (response == IapJNI.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED) {
|
||||||
|
sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED);
|
||||||
|
} else {
|
||||||
|
sendBuyError(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.e(IapGooglePlay.TAG, String.format("Failed to buy", e));
|
||||||
|
sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ERROR);
|
||||||
|
} catch (SendIntentException e) {
|
||||||
|
Log.e(IapGooglePlay.TAG, String.format("Failed to buy", e));
|
||||||
|
sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean consume(String purchaseData) {
|
||||||
|
try {
|
||||||
|
if (purchaseData == null) {
|
||||||
|
Log.e(IapGooglePlay.TAG, String.format("Failed to consume purchase, purchaseData was null!"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject pd = new JSONObject(purchaseData);
|
||||||
|
if (!pd.isNull("autoRenewing")) {
|
||||||
|
Log.i(IapGooglePlay.TAG, "Will not consume purchase since it is a subscription.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String token = pd.getString("purchaseToken");
|
||||||
|
int consumeResponse = service.consumePurchase(3, getPackageName(), token);
|
||||||
|
if (consumeResponse == IapJNI.BILLING_RESPONSE_RESULT_OK) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
Log.e(IapGooglePlay.TAG, String.format("Failed to consume purchase (%d)", consumeResponse));
|
||||||
|
sendBuyError(consumeResponse);
|
||||||
|
}
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.e(IapGooglePlay.TAG, "Failed to consume purchase", e);
|
||||||
|
sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ERROR);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
Log.e(IapGooglePlay.TAG, "Failed to consume purchase", e);
|
||||||
|
sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ERROR);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean processPurchase(String purchaseData, String signature)
|
||||||
|
{
|
||||||
|
if (this.autoFinishTransactions && !consume(purchaseData)) {
|
||||||
|
Log.e(IapGooglePlay.TAG, "Failed to consume and send message");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putString("action", Action.BUY.toString());
|
||||||
|
bundle.putInt(IapGooglePlay.RESPONSE_CODE, IapJNI.BILLING_RESPONSE_RESULT_OK);
|
||||||
|
bundle.putString(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA, purchaseData);
|
||||||
|
bundle.putString(IapGooglePlay.RESPONSE_INAPP_SIGNATURE, signature);
|
||||||
|
|
||||||
|
Message msg = new Message();
|
||||||
|
msg.setData(bundle);
|
||||||
|
try {
|
||||||
|
messenger.send(msg);
|
||||||
|
return true;
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.wtf(IapGooglePlay.TAG, "Unable to send message", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make buy response codes for all consumables not yet processed.
|
||||||
|
private void processPendingConsumables() {
|
||||||
|
try {
|
||||||
|
// Note: subscriptions cannot be consumed
|
||||||
|
// https://developer.android.com/google/play/billing/api.html#subs
|
||||||
|
Bundle items = service.getPurchases(3, getPackageName(), "inapp", null);
|
||||||
|
int response = getResponseCodeFromBundle(items);
|
||||||
|
if (response == IapJNI.BILLING_RESPONSE_RESULT_OK) {
|
||||||
|
ArrayList<String> purchaseDataList = items.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA_LIST);
|
||||||
|
ArrayList<String> signatureList = items.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_SIGNATURE_LIST);
|
||||||
|
for (int i = 0; i < purchaseDataList.size(); ++i) {
|
||||||
|
String purchaseData = purchaseDataList.get(i);
|
||||||
|
String signature = signatureList.get(i);
|
||||||
|
if (!processPurchase(purchaseData, signature)) {
|
||||||
|
// abort and retry some other time
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.e(IapGooglePlay.TAG, "Failed to process purchase", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restore() {
|
||||||
|
int response = IapJNI.BILLING_RESPONSE_RESULT_ERROR;
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putString("action", Action.RESTORE.toString());
|
||||||
|
|
||||||
|
Bundle items = new Bundle();
|
||||||
|
try {
|
||||||
|
ArrayList<String> purchaseItemList = new ArrayList<String>();
|
||||||
|
ArrayList<String> purchaseDataList = new ArrayList<String>();
|
||||||
|
ArrayList<String> signatureList = new ArrayList<String>();
|
||||||
|
|
||||||
|
Bundle inapp = service.getPurchases(3, getPackageName(), "inapp", null);
|
||||||
|
if (getResponseCodeFromBundle(inapp) == IapJNI.BILLING_RESPONSE_RESULT_OK) {
|
||||||
|
purchaseItemList.addAll(inapp.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_ITEM_LIST));
|
||||||
|
purchaseDataList.addAll(inapp.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA_LIST));
|
||||||
|
signatureList.addAll(inapp.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_SIGNATURE_LIST));
|
||||||
|
}
|
||||||
|
|
||||||
|
Bundle subs = service.getPurchases(3, getPackageName(), "subs", null);
|
||||||
|
if (getResponseCodeFromBundle(subs) == IapJNI.BILLING_RESPONSE_RESULT_OK) {
|
||||||
|
purchaseItemList.addAll(subs.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_ITEM_LIST));
|
||||||
|
purchaseDataList.addAll(subs.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA_LIST));
|
||||||
|
signatureList.addAll(subs.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_SIGNATURE_LIST));
|
||||||
|
}
|
||||||
|
|
||||||
|
items.putStringArrayList(IapGooglePlay.RESPONSE_INAPP_ITEM_LIST, purchaseItemList);
|
||||||
|
items.putStringArrayList(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA_LIST, purchaseDataList);
|
||||||
|
items.putStringArrayList(IapGooglePlay.RESPONSE_INAPP_SIGNATURE_LIST, signatureList);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.e(IapGooglePlay.TAG, "Failed to restore purchases", e);
|
||||||
|
}
|
||||||
|
bundle.putBundle("items", items);
|
||||||
|
|
||||||
|
bundle.putInt(IapGooglePlay.RESPONSE_CODE, response);
|
||||||
|
Message msg = new Message();
|
||||||
|
msg.setData(bundle);
|
||||||
|
|
||||||
|
try {
|
||||||
|
messenger.send(msg);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.wtf(IapGooglePlay.TAG, "Unable to send message", e);
|
||||||
|
}
|
||||||
|
this.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
View view = new View(this);
|
||||||
|
view.setBackgroundColor(0x10ffffff);
|
||||||
|
setContentView(view, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
|
||||||
|
|
||||||
|
Intent intent = getIntent();
|
||||||
|
final Bundle extras = intent.getExtras();
|
||||||
|
this.messenger = (Messenger) extras.getParcelable(IapGooglePlay.PARAM_MESSENGER);
|
||||||
|
final Action action = Action.valueOf(intent.getAction());
|
||||||
|
this.autoFinishTransactions = extras.getBoolean(IapGooglePlay.PARAM_AUTOFINISH_TRANSACTIONS);
|
||||||
|
|
||||||
|
Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
|
||||||
|
serviceIntent.setPackage("com.android.vending");
|
||||||
|
List<ResolveInfo> intentServices = getPackageManager().queryIntentServices(serviceIntent, 0);
|
||||||
|
if (intentServices != null && !intentServices.isEmpty()) {
|
||||||
|
// service available to handle that Intent
|
||||||
|
serviceConn = new ServiceConnection() {
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
|
service = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(ComponentName name, IBinder serviceBinder) {
|
||||||
|
service = IInAppBillingService.Stub.asInterface(serviceBinder);
|
||||||
|
if (action == Action.BUY) {
|
||||||
|
buy(extras.getString(IapGooglePlay.PARAM_PRODUCT), extras.getString(IapGooglePlay.PARAM_PRODUCT_TYPE));
|
||||||
|
} else if (action == Action.RESTORE) {
|
||||||
|
restore();
|
||||||
|
} else if (action == Action.PROCESS_PENDING_CONSUMABLES) {
|
||||||
|
processPendingConsumables();
|
||||||
|
finish();
|
||||||
|
} else if (action == Action.FINISH_TRANSACTION) {
|
||||||
|
consume(extras.getString(IapGooglePlay.PARAM_PURCHASE_DATA));
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
bindService(serviceIntent, serviceConn, Context.BIND_AUTO_CREATE);
|
||||||
|
} else {
|
||||||
|
// Service will never be connected; just send unavailability message
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putString("action", intent.getAction());
|
||||||
|
bundle.putInt(IapGooglePlay.RESPONSE_CODE, IapJNI.BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE);
|
||||||
|
Message msg = new Message();
|
||||||
|
msg.setData(bundle);
|
||||||
|
try {
|
||||||
|
messenger.send(msg);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.wtf(IapGooglePlay.TAG, "Unable to send message", e);
|
||||||
|
}
|
||||||
|
this.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void finish() {
|
||||||
|
super.finish();
|
||||||
|
this.isDone = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
if (hasPendingPurchases) {
|
||||||
|
// Not sure connection is up so need to check here.
|
||||||
|
if (service != null) {
|
||||||
|
if(autoFinishTransactions) {
|
||||||
|
processPendingConsumables();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hasPendingPurchases = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( !isDone )
|
||||||
|
{
|
||||||
|
Intent intent = getIntent();
|
||||||
|
|
||||||
|
if( intent != null && intent.getComponent().getClassName().equals( getClass().getName() ) )
|
||||||
|
{
|
||||||
|
Log.v(IapGooglePlay.TAG, "There's still an intent left: " + intent.getAction() );
|
||||||
|
sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serviceConn != null) {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
unbindService(serviceConn);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Log.wtf(IapGooglePlay.TAG, "Unable to unbind service", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Code from "trivialdrivesample"
|
||||||
|
int getResponseCodeFromIntent(Intent i) {
|
||||||
|
Object o = i.getExtras().get(IapGooglePlay.RESPONSE_CODE);
|
||||||
|
if (o == null) {
|
||||||
|
Log.e(IapGooglePlay.TAG, "Intent with no response code, assuming OK (known issue)");
|
||||||
|
return IapJNI.BILLING_RESPONSE_RESULT_OK;
|
||||||
|
} else if (o instanceof Integer) {
|
||||||
|
return ((Integer) o).intValue();
|
||||||
|
} else if (o instanceof Long) {
|
||||||
|
return (int) ((Long) o).longValue();
|
||||||
|
} else {
|
||||||
|
Log.e(IapGooglePlay.TAG, "Unexpected type for intent response code.");
|
||||||
|
Log.e(IapGooglePlay.TAG, o.getClass().getName());
|
||||||
|
throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
Bundle bundle = null;
|
||||||
|
if (data != null) {
|
||||||
|
int responseCode = getResponseCodeFromIntent(data);
|
||||||
|
String purchaseData = data.getStringExtra(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA);
|
||||||
|
String dataSignature = data.getStringExtra(IapGooglePlay.RESPONSE_INAPP_SIGNATURE);
|
||||||
|
if (responseCode == IapJNI.BILLING_RESPONSE_RESULT_OK) {
|
||||||
|
processPurchase(purchaseData, dataSignature);
|
||||||
|
} else {
|
||||||
|
bundle = new Bundle();
|
||||||
|
bundle.putString("action", Action.BUY.toString());
|
||||||
|
bundle.putInt(IapGooglePlay.RESPONSE_CODE, responseCode);
|
||||||
|
bundle.putString(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA, purchaseData);
|
||||||
|
bundle.putString(IapGooglePlay.RESPONSE_INAPP_SIGNATURE, dataSignature);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bundle = new Bundle();
|
||||||
|
bundle.putString("action", Action.BUY.toString());
|
||||||
|
bundle.putInt(IapGooglePlay.RESPONSE_CODE, IapJNI.BILLING_RESPONSE_RESULT_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send message if generated above
|
||||||
|
if (bundle != null) {
|
||||||
|
Message msg = new Message();
|
||||||
|
msg.setData(bundle);
|
||||||
|
try {
|
||||||
|
messenger.send(msg);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.wtf(IapGooglePlay.TAG, "Unable to send message", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.finish();
|
||||||
|
}
|
||||||
|
}
|
31
extension-iap/src/java/com/defold/iap/IapJNI.java
Normal file
31
extension-iap/src/java/com/defold/iap/IapJNI.java
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package com.defold.iap;
|
||||||
|
|
||||||
|
public class IapJNI implements IListProductsListener, IPurchaseListener {
|
||||||
|
|
||||||
|
// NOTE: Also defined in iap.h
|
||||||
|
public static final int TRANS_STATE_PURCHASING = 0;
|
||||||
|
public static final int TRANS_STATE_PURCHASED = 1;
|
||||||
|
public static final int TRANS_STATE_FAILED = 2;
|
||||||
|
public static final int TRANS_STATE_RESTORED = 3;
|
||||||
|
public static final int TRANS_STATE_UNVERIFIED = 4;
|
||||||
|
|
||||||
|
public static final int BILLING_RESPONSE_RESULT_OK = 0;
|
||||||
|
public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
|
||||||
|
public static final int BILLING_RESPONSE_RESULT_SERVICE_UNAVAILABLE = 2;
|
||||||
|
public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
|
||||||
|
public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
|
||||||
|
public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
|
||||||
|
public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
|
||||||
|
public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
|
||||||
|
public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
|
||||||
|
|
||||||
|
public IapJNI() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public native void onProductsResult(int responseCode, String productList);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public native void onPurchaseResult(int responseCode, String purchaseData);
|
||||||
|
|
||||||
|
}
|
23
game.project
Normal file
23
game.project
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
[bootstrap]
|
||||||
|
main_collection = /main/main.collectionc
|
||||||
|
|
||||||
|
[script]
|
||||||
|
shared_state = 1
|
||||||
|
|
||||||
|
[display]
|
||||||
|
width = 960
|
||||||
|
height = 640
|
||||||
|
|
||||||
|
[android]
|
||||||
|
input_method = HiddenInputField
|
||||||
|
package = com.defold.iap
|
||||||
|
|
||||||
|
[project]
|
||||||
|
title = extension-iap
|
||||||
|
|
||||||
|
[library]
|
||||||
|
include_dirs = extension-iap
|
||||||
|
|
||||||
|
[ios]
|
||||||
|
bundle_identifier = com.defold.iap
|
||||||
|
|
4
input/game.input_binding
Normal file
4
input/game.input_binding
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
mouse_trigger {
|
||||||
|
input: MOUSE_BUTTON_1
|
||||||
|
action: "touch"
|
||||||
|
}
|
37
main/main.collection
Normal file
37
main/main.collection
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
name: "main"
|
||||||
|
scale_along_z: 0
|
||||||
|
embedded_instances {
|
||||||
|
id: "go"
|
||||||
|
data: "components {\n"
|
||||||
|
" id: \"main\"\n"
|
||||||
|
" component: \"/main/main.gui\"\n"
|
||||||
|
" position {\n"
|
||||||
|
" x: 0.0\n"
|
||||||
|
" y: 0.0\n"
|
||||||
|
" z: 0.0\n"
|
||||||
|
" }\n"
|
||||||
|
" rotation {\n"
|
||||||
|
" x: 0.0\n"
|
||||||
|
" y: 0.0\n"
|
||||||
|
" z: 0.0\n"
|
||||||
|
" w: 1.0\n"
|
||||||
|
" }\n"
|
||||||
|
"}\n"
|
||||||
|
""
|
||||||
|
position {
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
}
|
||||||
|
rotation {
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
scale3 {
|
||||||
|
x: 1.0
|
||||||
|
y: 1.0
|
||||||
|
z: 1.0
|
||||||
|
}
|
||||||
|
}
|
76
main/main.gui
Normal file
76
main/main.gui
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
script: "/main/main.gui_script"
|
||||||
|
fonts {
|
||||||
|
name: "system_font"
|
||||||
|
font: "/builtins/fonts/system_font.font"
|
||||||
|
}
|
||||||
|
background_color {
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
w: 0.0
|
||||||
|
}
|
||||||
|
nodes {
|
||||||
|
position {
|
||||||
|
x: 467.0
|
||||||
|
y: 350.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
rotation {
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
scale {
|
||||||
|
x: 2.0
|
||||||
|
y: 2.0
|
||||||
|
z: 1.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
size {
|
||||||
|
x: 200.0
|
||||||
|
y: 100.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
color {
|
||||||
|
x: 1.0
|
||||||
|
y: 1.0
|
||||||
|
z: 1.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
type: TYPE_TEXT
|
||||||
|
blend_mode: BLEND_MODE_ALPHA
|
||||||
|
text: "<text>"
|
||||||
|
font: "system_font"
|
||||||
|
id: "text"
|
||||||
|
xanchor: XANCHOR_NONE
|
||||||
|
yanchor: YANCHOR_NONE
|
||||||
|
pivot: PIVOT_CENTER
|
||||||
|
outline {
|
||||||
|
x: 1.0
|
||||||
|
y: 1.0
|
||||||
|
z: 1.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
shadow {
|
||||||
|
x: 1.0
|
||||||
|
y: 1.0
|
||||||
|
z: 1.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
adjust_mode: ADJUST_MODE_FIT
|
||||||
|
line_break: false
|
||||||
|
layer: ""
|
||||||
|
inherit_alpha: true
|
||||||
|
alpha: 1.0
|
||||||
|
outline_alpha: 1.0
|
||||||
|
shadow_alpha: 1.0
|
||||||
|
template_node_child: false
|
||||||
|
text_leading: 1.0
|
||||||
|
text_tracking: 0.0
|
||||||
|
}
|
||||||
|
material: "/builtins/materials/gui.material"
|
||||||
|
adjust_reference: ADJUST_REFERENCE_PARENT
|
||||||
|
max_nodes: 512
|
3
main/main.gui_script
Normal file
3
main/main.gui_script
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
function init(self)
|
||||||
|
|
||||||
|
end
|
Loading…
x
Reference in New Issue
Block a user