mirror of
https://github.com/defold/extension-iap
synced 2025-06-27 10:27:46 +02:00
Merge pull request #25 from defold/dev-upgrade-to-billing-library
Upgrade to billing 3.0
This commit is contained in:
commit
6d898ab5ac
@ -1,6 +1,6 @@
|
|||||||
- name: iap
|
- name: iap
|
||||||
type: table
|
type: table
|
||||||
desc: Functions and constants for doing in-app purchases. Supported on iOS, Android (Google Play and Amazon)
|
desc: Functions and constants for doing in-app purchases. Supported on iOS, Android (Google Play and Amazon)
|
||||||
and Facebook Canvas platforms.
|
and Facebook Canvas platforms.
|
||||||
[icon:ios] [icon:googleplay] [icon:amazon] [icon:facebook]
|
[icon:ios] [icon:googleplay] [icon:amazon] [icon:facebook]
|
||||||
members:
|
members:
|
||||||
@ -61,6 +61,19 @@
|
|||||||
type: table
|
type: table
|
||||||
desc: transaction table parameter as supplied in listener callback
|
desc: transaction table parameter as supplied in listener callback
|
||||||
|
|
||||||
|
#*****************************************************************************************************
|
||||||
|
|
||||||
|
- name: acknowledge
|
||||||
|
type: function
|
||||||
|
desc: Acknowledges a product transaction but does not consume it.
|
||||||
|
[icon:attention] [icon:googleplay] Calling iap.acknowledge is required on a successful transaction
|
||||||
|
on Google Play unless iap.finish is called.
|
||||||
|
The `transaction.state` field must equal `iap.TRANS_STATE_PURCHASED`.
|
||||||
|
parameters:
|
||||||
|
- name: transaction
|
||||||
|
type: table
|
||||||
|
desc: transaction table parameter as supplied in listener callback
|
||||||
|
|
||||||
#*****************************************************************************************************
|
#*****************************************************************************************************
|
||||||
|
|
||||||
- name: get_provider_id
|
- name: get_provider_id
|
||||||
@ -210,7 +223,7 @@
|
|||||||
|
|
||||||
- name: trans_ident
|
- name: trans_ident
|
||||||
type: string
|
type: string
|
||||||
desc: The transaction identifier. This field is only set when `state` is
|
desc: The transaction identifier. This field is only set when `state` is
|
||||||
`TRANS_STATE_RESTORED`, `TRANS_STATE_UNVERIFIED` or `TRANS_STATE_PURCHASED`.
|
`TRANS_STATE_RESTORED`, `TRANS_STATE_UNVERIFIED` or `TRANS_STATE_PURCHASED`.
|
||||||
|
|
||||||
- name: receipt
|
- name: receipt
|
||||||
@ -260,47 +273,45 @@
|
|||||||
- name: PROVIDER_ID_AMAZON
|
- name: PROVIDER_ID_AMAZON
|
||||||
type: number
|
type: number
|
||||||
desc: provider id for Amazon
|
desc: provider id for Amazon
|
||||||
|
|
||||||
- name: PROVIDER_ID_APPLE
|
- name: PROVIDER_ID_APPLE
|
||||||
type: number
|
type: number
|
||||||
desc: provider id for Apple
|
desc: provider id for Apple
|
||||||
|
|
||||||
- name: PROVIDER_ID_FACEBOOK
|
- name: PROVIDER_ID_FACEBOOK
|
||||||
type: number
|
type: number
|
||||||
desc: provider id for Facebook
|
desc: provider id for Facebook
|
||||||
|
|
||||||
- name: PROVIDER_ID_GOOGLE
|
- name: PROVIDER_ID_GOOGLE
|
||||||
type: number
|
type: number
|
||||||
desc: iap provider id for Google
|
desc: iap provider id for Google
|
||||||
|
|
||||||
- name: REASON_UNSPECIFIED
|
- name: REASON_UNSPECIFIED
|
||||||
type: number
|
type: number
|
||||||
desc: unspecified error reason
|
desc: unspecified error reason
|
||||||
|
|
||||||
- name: REASON_USER_CANCELED
|
- name: REASON_USER_CANCELED
|
||||||
type: number
|
type: number
|
||||||
desc: user canceled reason
|
desc: user canceled reason
|
||||||
|
|
||||||
- name: TRANS_STATE_FAILED
|
- name: TRANS_STATE_FAILED
|
||||||
type: number
|
type: number
|
||||||
desc: transaction failed state
|
desc: transaction failed state
|
||||||
|
|
||||||
- name: TRANS_STATE_PURCHASED
|
- name: TRANS_STATE_PURCHASED
|
||||||
type: number
|
type: number
|
||||||
desc: transaction purchased state
|
desc: transaction purchased state
|
||||||
|
|
||||||
- name: TRANS_STATE_PURCHASING
|
- name: TRANS_STATE_PURCHASING
|
||||||
type: number
|
type: number
|
||||||
desc: transaction purchasing state
|
desc: transaction purchasing state
|
||||||
This is an intermediate mode followed by TRANS_STATE_PURCHASED. Store provider support dependent.
|
This is an intermediate mode followed by TRANS_STATE_PURCHASED. Store provider support dependent.
|
||||||
|
|
||||||
- name: TRANS_STATE_RESTORED
|
- name: TRANS_STATE_RESTORED
|
||||||
type: number
|
type: number
|
||||||
desc: transaction restored state
|
desc: transaction restored state
|
||||||
This is only available on store providers supporting restoring purchases.
|
This is only available on store providers supporting restoring purchases.
|
||||||
|
|
||||||
- name: TRANS_STATE_UNVERIFIED
|
- name: TRANS_STATE_UNVERIFIED
|
||||||
type: number
|
type: number
|
||||||
desc: transaction unverified state, requires verification of purchase
|
desc: transaction unverified state, requires verification of purchase
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,32 +4,47 @@ layout: default
|
|||||||
|
|
||||||
# Defold In-app purchase extension API documentation
|
# Defold In-app purchase extension API documentation
|
||||||
|
|
||||||
This extension provides functions for making in-app purchases. Supported on iOS, Android (Google Play and Amazon) and Facebook Canvas.
|
This extension provides functions for making in-app purchases.
|
||||||
|
|
||||||
# Usage
|
|
||||||
To use this library in your Defold project, add the following URL to your <code class="inline-code-block">game.project</code> dependencies:
|
## Usage
|
||||||
|
To use this library in your Defold project, add the following URL to your `game.project` dependencies:
|
||||||
|
|
||||||
https://github.com/defold/extension-iap/archive/master.zip
|
https://github.com/defold/extension-iap/archive/master.zip
|
||||||
|
|
||||||
We recommend using a link to a zip file of a [specific release](https://github.com/defold/extension-iap/releases).
|
We recommend using a link to a zip file of a [specific release](https://github.com/defold/extension-iap/releases).
|
||||||
|
|
||||||
|
For Facebook Canvas you also need to add the [Facebook extension as a dependency](https://github.com/defold/extension-facebook).
|
||||||
|
|
||||||
|
|
||||||
## Source code
|
## Source code
|
||||||
|
|
||||||
The source code is available on [GitHub](https://github.com/defold/extension-iap)
|
The source code is available on [GitHub](https://github.com/defold/extension-iap)
|
||||||
|
|
||||||
## Differences between supported platforms
|
|
||||||
|
|
||||||
Google Play and Amazon supports two different product types: subscriptions and consumable products.
|
## Supported platforms
|
||||||
|
|
||||||
Apple supports three different product types: subscriptions, consumable and non-consumable products.
|
The following platforms are supported by the extension:
|
||||||
|
|
||||||
If you want to simulate non-consumable products on Google Play/Amazon you need to make sure to not call `iap.finish()` on the product in question (and make sure to not have enabled Auto Finish Transactions in *game.project*).
|
* iOS - StoreKit
|
||||||
|
* Google Play - Billing 3.0.0
|
||||||
|
* Amazon - 2.0.61
|
||||||
|
* Facebook Canvas
|
||||||
|
|
||||||
|
|
||||||
|
### Differences between supported platforms
|
||||||
|
|
||||||
|
Amazon supports two different product types: subscriptions and consumable products.
|
||||||
|
|
||||||
|
Google Play and Apple supports three different product types: subscriptions, consumable and non-consumable products.
|
||||||
|
|
||||||
|
If you want to simulate non-consumable products on Amazon you need to make sure to not call `iap.finish()` on the product in question (and make sure to not have enabled Auto Finish Transactions in *game.project*).
|
||||||
|
|
||||||
Calls to `iap.buy()` and `iap.set_listener()` will return all non-finished purchases on Google Play. (This will not happen on iOS)
|
Calls to `iap.buy()` and `iap.set_listener()` will return all non-finished purchases on Google Play. (This will not happen on iOS)
|
||||||
|
|
||||||
The concept of restoring purchases does not exist on Google Play/Amazon. Calls to `iap.restore()` on iOS will return all purchased products (and have product state set to TRANS_STATE_RESTORED). Calls to `iap.restore()` on Google Play will return all non-finished purchases (and have product state set to TRANS_STATE_PURCHASED).
|
The concept of restoring purchases does not exist on Google Play/Amazon. Calls to `iap.restore()` on iOS will return all purchased products (and have product state set to TRANS_STATE_RESTORED). Calls to `iap.restore()` on Google Play will return all non-finished purchases (and have product state set to TRANS_STATE_PURCHASED).
|
||||||
|
|
||||||
###
|
---
|
||||||
|
|
||||||
# API reference
|
# API reference
|
||||||
|
|
||||||
|
@ -3,23 +3,11 @@
|
|||||||
package="{{android.package}}">
|
package="{{android.package}}">
|
||||||
<uses-sdk android:minSdkVersion="{{android.minimum_sdk_version}}" android:targetSdkVersion="{{android.target_sdk_version}}" />
|
<uses-sdk android:minSdkVersion="{{android.minimum_sdk_version}}" android:targetSdkVersion="{{android.target_sdk_version}}" />
|
||||||
<application>
|
<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 -->
|
<!-- For Amazon IAP -->
|
||||||
<receiver android:name="com.amazon.device.iap.ResponseReceiver" >
|
<receiver android:name="com.amazon.device.iap.ResponseReceiver" >
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.amazon.inapp.purchasing.NOTIFY" android:permission="com.amazon.inapp.purchasing.Permission.NOTIFY" />
|
<action android:name="com.amazon.inapp.purchasing.NOTIFY" android:permission="com.amazon.inapp.purchasing.Permission.NOTIFY" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
<uses-permission android:name="com.android.vending.BILLING" />
|
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
3
extension-iap/manifests/android/build.gradle
Normal file
3
extension-iap/manifests/android/build.gradle
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
dependencies {
|
||||||
|
implementation 'com.android.billingclient:billing:3.0.0'
|
||||||
|
}
|
@ -43,6 +43,7 @@ struct IAP
|
|||||||
jmethodID m_Buy;
|
jmethodID m_Buy;
|
||||||
jmethodID m_Restore;
|
jmethodID m_Restore;
|
||||||
jmethodID m_ProcessPendingConsumables;
|
jmethodID m_ProcessPendingConsumables;
|
||||||
|
jmethodID m_AcknowledgeTransaction;
|
||||||
jmethodID m_FinishTransaction;
|
jmethodID m_FinishTransaction;
|
||||||
|
|
||||||
IAPCommandQueue m_CommandQueue;
|
IAPCommandQueue m_CommandQueue;
|
||||||
@ -52,7 +53,10 @@ static IAP g_IAP;
|
|||||||
|
|
||||||
static int IAP_ProcessPendingTransactions(lua_State* L)
|
static int IAP_ProcessPendingTransactions(lua_State* L)
|
||||||
{
|
{
|
||||||
//todo handle pending transactions if there is such thing on Android
|
JNIEnv* env = Attach();
|
||||||
|
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_ProcessPendingConsumables, g_IAP.m_IAPJNI);
|
||||||
|
Detach();
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,6 +147,46 @@ static int IAP_Finish(lua_State* L)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int IAP_Acknowledge(lua_State* L)
|
||||||
|
{
|
||||||
|
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_AcknowledgeTransaction, receiptUTF, g_IAP.m_IAPJNI);
|
||||||
|
env->DeleteLocalRef(receiptUTF);
|
||||||
|
Detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(top == lua_gettop(L));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static int IAP_Restore(lua_State* L)
|
static int IAP_Restore(lua_State* L)
|
||||||
{
|
{
|
||||||
// TODO: Missing callback here for completion/error
|
// TODO: Missing callback here for completion/error
|
||||||
@ -190,6 +234,7 @@ static const luaL_reg IAP_methods[] =
|
|||||||
{"list", IAP_List},
|
{"list", IAP_List},
|
||||||
{"buy", IAP_Buy},
|
{"buy", IAP_Buy},
|
||||||
{"finish", IAP_Finish},
|
{"finish", IAP_Finish},
|
||||||
|
{"acknowledge", IAP_Acknowledge},
|
||||||
{"restore", IAP_Restore},
|
{"restore", IAP_Restore},
|
||||||
{"set_listener", IAP_SetListener},
|
{"set_listener", IAP_SetListener},
|
||||||
{"get_provider_id", IAP_GetProviderId},
|
{"get_provider_id", IAP_GetProviderId},
|
||||||
@ -223,6 +268,7 @@ JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onProductsResult(JNIEnv* env,
|
|||||||
|
|
||||||
JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onPurchaseResult__ILjava_lang_String_2(JNIEnv* env, jobject, jint responseCode, jstring purchaseData)
|
JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onPurchaseResult__ILjava_lang_String_2(JNIEnv* env, jobject, jint responseCode, jstring purchaseData)
|
||||||
{
|
{
|
||||||
|
dmLogInfo("Java_com_defold_iap_IapJNI_onPurchaseResult__ILjava_lang_String_2 %d", (int)responseCode);
|
||||||
const char* pd = 0;
|
const char* pd = 0;
|
||||||
if (purchaseData)
|
if (purchaseData)
|
||||||
{
|
{
|
||||||
@ -395,6 +441,7 @@ static dmExtension::Result InitializeIAP(dmExtension::Params* params)
|
|||||||
g_IAP.m_Stop = env->GetMethodID(iap_class, "stop", "()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_ProcessPendingConsumables = env->GetMethodID(iap_class, "processPendingConsumables", "(Lcom/defold/iap/IPurchaseListener;)V");
|
||||||
g_IAP.m_FinishTransaction = env->GetMethodID(iap_class, "finishTransaction", "(Ljava/lang/String;Lcom/defold/iap/IPurchaseListener;)V");
|
g_IAP.m_FinishTransaction = env->GetMethodID(iap_class, "finishTransaction", "(Ljava/lang/String;Lcom/defold/iap/IPurchaseListener;)V");
|
||||||
|
g_IAP.m_AcknowledgeTransaction = env->GetMethodID(iap_class, "acknowledgeTransaction", "(Ljava/lang/String;Lcom/defold/iap/IPurchaseListener;)V");
|
||||||
|
|
||||||
jmethodID jni_constructor = env->GetMethodID(iap_class, "<init>", "(Landroid/app/Activity;Z)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, dmGraphics::GetNativeAndroidActivity(), g_IAP.m_autoFinishTransactions));
|
g_IAP.m_IAP = env->NewGlobalRef(env->NewObject(iap_class, jni_constructor, dmGraphics::GetNativeAndroidActivity(), g_IAP.m_autoFinishTransactions));
|
||||||
@ -468,4 +515,3 @@ static dmExtension::Result FinalizeIAP(dmExtension::Params* params)
|
|||||||
DM_DECLARE_EXTENSION(IAPExt, "IAP", 0, 0, InitializeIAP, UpdateIAP, 0, FinalizeIAP)
|
DM_DECLARE_EXTENSION(IAPExt, "IAP", 0, 0, InitializeIAP, UpdateIAP, 0, FinalizeIAP)
|
||||||
|
|
||||||
#endif //DM_PLATFORM_ANDROID
|
#endif //DM_PLATFORM_ANDROID
|
||||||
|
|
||||||
|
@ -193,6 +193,11 @@ static int IAP_Finish(lua_State* L)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int IAP_Acknowledge(lua_State* L)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static int IAP_Restore(lua_State* L)
|
static int IAP_Restore(lua_State* L)
|
||||||
{
|
{
|
||||||
DM_LUA_STACK_CHECK(L, 1);
|
DM_LUA_STACK_CHECK(L, 1);
|
||||||
@ -212,6 +217,7 @@ static const luaL_reg IAP_methods[] =
|
|||||||
{"list", IAP_List},
|
{"list", IAP_List},
|
||||||
{"buy", IAP_Buy},
|
{"buy", IAP_Buy},
|
||||||
{"finish", IAP_Finish},
|
{"finish", IAP_Finish},
|
||||||
|
{"acknowledge", IAP_Acknowledge},
|
||||||
{"restore", IAP_Restore},
|
{"restore", IAP_Restore},
|
||||||
{"set_listener", IAP_SetListener},
|
{"set_listener", IAP_SetListener},
|
||||||
{"get_provider_id", IAP_GetProviderId},
|
{"get_provider_id", IAP_GetProviderId},
|
||||||
|
@ -503,6 +503,11 @@ static int IAP_SetListener(lua_State* L)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int IAP_Acknowledge(lua_State* L)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static int IAP_GetProviderId(lua_State* L)
|
static int IAP_GetProviderId(lua_State* L)
|
||||||
{
|
{
|
||||||
lua_pushinteger(L, PROVIDER_ID_APPLE);
|
lua_pushinteger(L, PROVIDER_ID_APPLE);
|
||||||
@ -514,6 +519,7 @@ static const luaL_reg IAP_methods[] =
|
|||||||
{"list", IAP_List},
|
{"list", IAP_List},
|
||||||
{"buy", IAP_Buy},
|
{"buy", IAP_Buy},
|
||||||
{"finish", IAP_Finish},
|
{"finish", IAP_Finish},
|
||||||
|
{"acknowledge", IAP_Acknowledge},
|
||||||
{"restore", IAP_Restore},
|
{"restore", IAP_Restore},
|
||||||
{"set_listener", IAP_SetListener},
|
{"set_listener", IAP_SetListener},
|
||||||
{"get_provider_id", IAP_GetProviderId},
|
{"get_provider_id", IAP_GetProviderId},
|
||||||
|
@ -1,501 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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;
|
|
||||||
}
|
|
@ -5,507 +5,418 @@ import java.util.ArrayList;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.concurrent.ArrayBlockingQueue;
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
|
||||||
import android.app.Activity;
|
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.content.pm.PackageManager;
|
|
||||||
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 android.util.Log;
|
||||||
|
|
||||||
import com.android.vending.billing.IInAppBillingService;
|
import com.android.billingclient.api.BillingClient;
|
||||||
|
import com.android.billingclient.api.BillingClient.BillingResponseCode;
|
||||||
|
import com.android.billingclient.api.BillingClient.SkuType;
|
||||||
|
import com.android.billingclient.api.BillingResult;
|
||||||
|
import com.android.billingclient.api.Purchase;
|
||||||
|
import com.android.billingclient.api.Purchase.PurchasesResult;
|
||||||
|
import com.android.billingclient.api.Purchase.PurchaseState;
|
||||||
|
import com.android.billingclient.api.SkuDetails;
|
||||||
|
import com.android.billingclient.api.ConsumeParams;
|
||||||
|
import com.android.billingclient.api.BillingFlowParams;
|
||||||
|
import com.android.billingclient.api.SkuDetailsParams;
|
||||||
|
import com.android.billingclient.api.AcknowledgePurchaseParams;
|
||||||
|
import com.android.billingclient.api.PurchasesUpdatedListener;
|
||||||
|
import com.android.billingclient.api.BillingClientStateListener;
|
||||||
|
import com.android.billingclient.api.ConsumeResponseListener;
|
||||||
|
import com.android.billingclient.api.SkuDetailsResponseListener;
|
||||||
|
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
|
||||||
|
|
||||||
public class IapGooglePlay implements Handler.Callback {
|
public class IapGooglePlay implements PurchasesUpdatedListener {
|
||||||
public static final String PARAM_PRODUCT = "product";
|
public static final String TAG = "IapGooglePlay";
|
||||||
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 Map<String, SkuDetails> products = new HashMap<String, SkuDetails>();
|
||||||
|
private BillingClient billingClient;
|
||||||
private IPurchaseListener purchaseListener;
|
private IPurchaseListener purchaseListener;
|
||||||
private boolean initialized;
|
|
||||||
private boolean autoFinishTransactions;
|
private boolean autoFinishTransactions;
|
||||||
|
private Activity activity;
|
||||||
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) {
|
public IapGooglePlay(Activity activity, boolean autoFinishTransactions) {
|
||||||
this.activity = activity;
|
this.activity = activity;
|
||||||
this.autoFinishTransactions = autoFinishTransactions;
|
this.autoFinishTransactions = autoFinishTransactions;
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isPlayStoreInstalled(Context context){
|
|
||||||
try {
|
|
||||||
context.getPackageManager().getPackageInfo("com.android.vending", 0);
|
|
||||||
return true;
|
|
||||||
} catch (PackageManager.NameNotFoundException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void init() {
|
|
||||||
// NOTE: We must create Handler lazily as construction of
|
|
||||||
// handlers must be in the context of a "looper" on Android
|
|
||||||
|
|
||||||
if (!isPlayStoreInstalled(activity)) {
|
|
||||||
Log.e(TAG, "Unable to find Google Play Store (com.android.vending)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.initialized)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.initialized = true;
|
|
||||||
this.handler = new Handler(this);
|
|
||||||
this.messenger = new Messenger(this.handler);
|
|
||||||
|
|
||||||
serviceConn = new ServiceConnection() {
|
|
||||||
|
|
||||||
|
billingClient = BillingClient.newBuilder(activity).setListener(this).enablePendingPurchases().build();
|
||||||
|
billingClient.startConnection(new BillingClientStateListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onServiceDisconnected(ComponentName name) {
|
public void onBillingSetupFinished(BillingResult billingResult) {
|
||||||
Log.v(TAG, "IAP disconnected");
|
if (billingResult.getResponseCode() == BillingResponseCode.OK) {
|
||||||
service = null;
|
Log.v(TAG, "Setup finished");
|
||||||
|
// NOTE: we will not query purchases here. This is done
|
||||||
|
// when the extension listener is set
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log.wtf(TAG, "Setup error: " + billingResult.getDebugMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onServiceConnected(ComponentName name, IBinder binderService) {
|
public void onBillingServiceDisconnected() {
|
||||||
Log.v(TAG, "IAP connected");
|
Log.v(TAG, "Service disconnected");
|
||||||
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() {
|
public void stop() {
|
||||||
this.activity.runOnUiThread(new Runnable() {
|
Log.d(TAG, "stop()");
|
||||||
@Override
|
if (billingClient.isReady()) {
|
||||||
public void run() {
|
billingClient.endConnection();
|
||||||
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, final long commandPtr) {
|
|
||||||
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(), commandPtr);
|
|
||||||
}
|
|
||||||
catch(JSONException e) {
|
|
||||||
Log.wtf(TAG, "Failed to convert products", e);
|
|
||||||
listener.onProductsResult(resultCode, null, commandPtr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
listener.onProductsResult(resultCode, null, commandPtr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the product data into the generic format shared between all Defold IAP implementations
|
private String toISO8601(final Date date) {
|
||||||
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);
|
String formatted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(date);
|
||||||
return formatted.substring(0, 22) + ":" + formatted.substring(22);
|
return formatted.substring(0, 22) + ":" + formatted.substring(22);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String convertPurchase(String purchase, String signature) {
|
private String convertPurchase(Purchase purchase) {
|
||||||
|
// original JSON:
|
||||||
|
// {
|
||||||
|
// "orderId":"GPA.3301-1670-7033-37542",
|
||||||
|
// "packageName":"com.defold.extension.iap",
|
||||||
|
// "productId":"com.defold.iap.goldbar.medium",
|
||||||
|
// "purchaseTime":1595455967875,
|
||||||
|
// "purchaseState":0,
|
||||||
|
// "purchaseToken":"kacckamkehbbammphdcnhbme.AO-J1OxznnK6E8ILqaAgrPa-3sfaHny424R1e_ZJ2LkaJVsy-5aEOmHizw0vUp-017m8OUvw1rSvfAHbOog1fIvDGJmjaze3MEVFOh1ayJsNFfPDUGwMA_u_9rlV7OqX_nnIyDShH2KE5WrnMC0yQyw7sg5hfgeW6A",
|
||||||
|
// "acknowledged":false
|
||||||
|
// }
|
||||||
|
Log.d(TAG, "convertPurchase() original json: " + purchase.getOriginalJson());
|
||||||
|
JSONObject p = new JSONObject();
|
||||||
try {
|
try {
|
||||||
JSONObject p = new JSONObject(purchase);
|
JSONObject original = new JSONObject(purchase.getOriginalJson());
|
||||||
p.put("ident", p.get("productId"));
|
p.put("ident", original.get("productId"));
|
||||||
p.put("state", IapJNI.TRANS_STATE_PURCHASED);
|
p.put("state", purchaseStateToDefoldState(purchase.getPurchaseState()));
|
||||||
|
p.put("trans_ident", purchase.getOrderId());
|
||||||
// We check if orderId is actually set here, otherwise we return a blank string.
|
p.put("date", toISO8601(new Date(purchase.getPurchaseTime())));
|
||||||
// This is what Google used to do, but after some updates around June/May 2016
|
p.put("receipt", purchase.getPurchaseToken());
|
||||||
// they stopped to include the orderId key at all for test purchases. See: DEF-1940
|
p.put("signature", purchase.getSignature());
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
catch (JSONException e) {
|
||||||
return null;
|
Log.wtf(TAG, "Failed to convert purchase", e);
|
||||||
|
}
|
||||||
|
return p.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private JSONObject convertSkuDetails(SkuDetails skuDetails) {
|
||||||
public boolean handleMessage(Message msg) {
|
JSONObject p = new JSONObject();
|
||||||
Bundle bundle = msg.getData();
|
try {
|
||||||
|
p.put("price_string", skuDetails.getPrice());
|
||||||
String actionString = bundle.getString("action");
|
p.put("ident", skuDetails.getSku());
|
||||||
if (actionString == null) {
|
p.put("currency_code", skuDetails.getPriceCurrencyCode());
|
||||||
return false;
|
p.put("price", skuDetails.getPriceAmountMicros() * 0.000001);
|
||||||
}
|
}
|
||||||
|
catch(JSONException e) {
|
||||||
if (purchaseListener == null) {
|
Log.wtf(TAG, "Failed to convert sku details", e);
|
||||||
Log.wtf(TAG, "No purchase listener set");
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
Action action = Action.valueOf(actionString);
|
private int purchaseStateToDefoldState(int purchaseState) {
|
||||||
|
int defoldState;
|
||||||
|
switch(purchaseState) {
|
||||||
|
case PurchaseState.PENDING:
|
||||||
|
defoldState = IapJNI.TRANS_STATE_PURCHASING;
|
||||||
|
break;
|
||||||
|
case PurchaseState.PURCHASED:
|
||||||
|
defoldState = IapJNI.TRANS_STATE_PURCHASED;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
case PurchaseState.UNSPECIFIED_STATE:
|
||||||
|
defoldState = IapJNI.TRANS_STATE_UNVERIFIED;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return defoldState;
|
||||||
|
}
|
||||||
|
|
||||||
if (action == Action.BUY) {
|
private int billingResponseCodeToDefoldResponse(int responseCode) {
|
||||||
int responseCode = bundle.getInt(RESPONSE_CODE);
|
int defoldResponse;
|
||||||
String purchaseData = bundle.getString(RESPONSE_INAPP_PURCHASE_DATA);
|
switch(responseCode) {
|
||||||
String dataSignature = bundle.getString(RESPONSE_INAPP_SIGNATURE);
|
case BillingResponseCode.BILLING_UNAVAILABLE:
|
||||||
|
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE;
|
||||||
|
break;
|
||||||
|
case BillingResponseCode.DEVELOPER_ERROR:
|
||||||
|
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_DEVELOPER_ERROR;
|
||||||
|
break;
|
||||||
|
case BillingResponseCode.ITEM_ALREADY_OWNED:
|
||||||
|
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED;
|
||||||
|
break;
|
||||||
|
case BillingResponseCode.ITEM_NOT_OWNED:
|
||||||
|
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED;
|
||||||
|
break;
|
||||||
|
case BillingResponseCode.ITEM_UNAVAILABLE:
|
||||||
|
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE;
|
||||||
|
break;
|
||||||
|
case BillingResponseCode.OK:
|
||||||
|
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_OK;
|
||||||
|
break;
|
||||||
|
case BillingResponseCode.SERVICE_TIMEOUT:
|
||||||
|
case BillingResponseCode.SERVICE_UNAVAILABLE:
|
||||||
|
case BillingResponseCode.SERVICE_DISCONNECTED:
|
||||||
|
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_SERVICE_UNAVAILABLE;
|
||||||
|
break;
|
||||||
|
case BillingResponseCode.USER_CANCELED:
|
||||||
|
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_USER_CANCELED;
|
||||||
|
break;
|
||||||
|
case BillingResponseCode.FEATURE_NOT_SUPPORTED:
|
||||||
|
case BillingResponseCode.ERROR:
|
||||||
|
default:
|
||||||
|
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_ERROR;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Log.d(TAG, "billingResponseCodeToDefoldResponse: " + responseCode + " defoldResponse: " + defoldResponse);
|
||||||
|
return defoldResponse;
|
||||||
|
}
|
||||||
|
|
||||||
if (purchaseData != null && dataSignature != null) {
|
private int billingResultToDefoldResponse(BillingResult result) {
|
||||||
purchaseData = convertPurchase(purchaseData, dataSignature);
|
return billingResponseCodeToDefoldResponse(result.getResponseCode());
|
||||||
} else {
|
}
|
||||||
purchaseData = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
purchaseListener.onPurchaseResult(responseCode, purchaseData);
|
/**
|
||||||
} else if (action == Action.RESTORE) {
|
* Query Google Play for purchases done within the app.
|
||||||
Bundle items = bundle.getBundle("items");
|
*/
|
||||||
|
private List<Purchase> queryPurchases(final String type) {
|
||||||
|
PurchasesResult result = billingClient.queryPurchases(type);
|
||||||
|
List<Purchase> purchases = result.getPurchasesList();
|
||||||
|
if (purchases == null) {
|
||||||
|
purchases = new ArrayList<Purchase>();
|
||||||
|
}
|
||||||
|
if (result.getBillingResult().getResponseCode() != BillingResponseCode.OK) {
|
||||||
|
Log.e(TAG, "Unable to query pending purchases: " + result.getBillingResult().getDebugMessage());
|
||||||
|
}
|
||||||
|
return purchases;
|
||||||
|
}
|
||||||
|
|
||||||
if (!items.containsKey(RESPONSE_INAPP_ITEM_LIST)) {
|
/**
|
||||||
purchaseListener.onPurchaseResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, "");
|
* This method is called either explicitly from Lua or from extension code
|
||||||
return true;
|
* when "set_listener()" is called from Lua.
|
||||||
}
|
* The method will query purchases and try to handle them one by one (either
|
||||||
|
* trying to consume/finish them or simply notify the provided listener).
|
||||||
|
*/
|
||||||
|
public void processPendingConsumables(final IPurchaseListener purchaseListener) {
|
||||||
|
Log.d(TAG, "processPendingConsumables()");
|
||||||
|
List<Purchase> purchasesList = new ArrayList<Purchase>();
|
||||||
|
purchasesList.addAll(queryPurchases(SkuType.INAPP));
|
||||||
|
purchasesList.addAll(queryPurchases(SkuType.SUBS));
|
||||||
|
for (Purchase purchase : purchasesList) {
|
||||||
|
handlePurchase(purchase, purchaseListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ArrayList<String> ownedSkus = items.getStringArrayList(RESPONSE_INAPP_ITEM_LIST);
|
/**
|
||||||
ArrayList<String> purchaseDataList = items.getStringArrayList(RESPONSE_INAPP_PURCHASE_DATA_LIST);
|
* Consume a purchase. This will acknowledge the purchase and make it
|
||||||
ArrayList<String> signatureList = items.getStringArrayList(RESPONSE_INAPP_SIGNATURE_LIST);
|
* available to buy again.
|
||||||
for (int i = 0; i < ownedSkus.size(); ++i) {
|
*/
|
||||||
int c = IapJNI.BILLING_RESPONSE_RESULT_OK;
|
private void consumePurchase(final String purchaseToken, final ConsumeResponseListener consumeListener) {
|
||||||
String pd = convertPurchase(purchaseDataList.get(i), signatureList.get(i));
|
Log.d(TAG, "consumePurchase() " + purchaseToken);
|
||||||
if (pd == null) {
|
ConsumeParams consumeParams = ConsumeParams.newBuilder()
|
||||||
pd = "";
|
.setPurchaseToken(purchaseToken)
|
||||||
c = IapJNI.BILLING_RESPONSE_RESULT_ERROR;
|
.build();
|
||||||
|
|
||||||
|
billingClient.consumeAsync(consumeParams, consumeListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from Lua. This method will try to consume a purchase.
|
||||||
|
*/
|
||||||
|
public void finishTransaction(final String purchaseToken, final IPurchaseListener purchaseListener) {
|
||||||
|
Log.d(TAG, "finishTransaction() " + purchaseToken);
|
||||||
|
consumePurchase(purchaseToken, new ConsumeResponseListener() {
|
||||||
|
@Override
|
||||||
|
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
|
||||||
|
Log.d(TAG, "finishTransaction() response code " + billingResult.getResponseCode() + " purchaseToken: " + purchaseToken);
|
||||||
|
// note: we only call the purchase listener if an error happens
|
||||||
|
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
|
||||||
|
Log.e(TAG, "Unable to consume purchase: " + billingResult.getDebugMessage());
|
||||||
|
purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
|
||||||
}
|
}
|
||||||
purchaseListener.onPurchaseResult(c, pd);
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from Lua. This method will try to acknowledge a purchase (but not finish/consume it).
|
||||||
|
*/
|
||||||
|
public void acknowledgeTransaction(final String purchaseToken, final IPurchaseListener purchaseListener) {
|
||||||
|
Log.d(TAG, "acknowledgeTransaction() " + purchaseToken);
|
||||||
|
|
||||||
|
AcknowledgePurchaseParams acknowledgeParams = AcknowledgePurchaseParams.newBuilder()
|
||||||
|
.setPurchaseToken(purchaseToken)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
billingClient.acknowledgePurchase(acknowledgeParams, new AcknowledgePurchaseResponseListener() {
|
||||||
|
@Override
|
||||||
|
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
|
||||||
|
Log.d(TAG, "acknowledgeTransaction() response code " + billingResult.getResponseCode());
|
||||||
|
// note: we only call the purchase listener if an error happens
|
||||||
|
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
|
||||||
|
Log.e(TAG, "Unable to acknowledge purchase: " + billingResult.getDebugMessage());
|
||||||
|
purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a purchase. If the extension is configured to automatically
|
||||||
|
* finish transactions the purchase will be immediately consumed. Otherwise
|
||||||
|
* the product will be returned via the listener without being consumed.
|
||||||
|
* NOTE: Billing 3.0 requires purchases to be acknowledged within 3 days of
|
||||||
|
* purchase unless they are consumed.
|
||||||
|
*/
|
||||||
|
private void handlePurchase(final Purchase purchase, final IPurchaseListener purchaseListener) {
|
||||||
|
if (this.autoFinishTransactions) {
|
||||||
|
consumePurchase(purchase.getPurchaseToken(), new ConsumeResponseListener() {
|
||||||
|
@Override
|
||||||
|
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
|
||||||
|
Log.d(TAG, "handlePurchase() response code " + billingResult.getResponseCode() + " purchaseToken: " + purchaseToken);
|
||||||
|
purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), convertPurchase(purchase));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
purchaseListener.onPurchaseResult(billingResponseCodeToDefoldResponse(BillingResponseCode.OK), convertPurchase(purchase));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BillingClient listener set in the constructor.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
|
||||||
|
if (billingResult.getResponseCode() == BillingResponseCode.OK && purchases != null) {
|
||||||
|
for (Purchase purchase : purchases) {
|
||||||
|
handlePurchase(purchase, this.purchaseListener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
else {
|
||||||
|
this.purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buy a product. This method stores the listener and uses it in the
|
||||||
|
* onPurchasesUpdated() callback.
|
||||||
|
*/
|
||||||
|
private void buyProduct(SkuDetails sku, final IPurchaseListener purchaseListener) {
|
||||||
|
this.purchaseListener = purchaseListener;
|
||||||
|
|
||||||
|
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
|
||||||
|
.setSkuDetails(sku)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
BillingResult billingResult = billingClient.launchBillingFlow(this.activity, billingFlowParams);
|
||||||
|
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
|
||||||
|
Log.e(TAG, "Purchase failed: " + billingResult.getDebugMessage());
|
||||||
|
purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from Lua.
|
||||||
|
*/
|
||||||
|
public void buy(final String product, final IPurchaseListener purchaseListener) {
|
||||||
|
Log.d(TAG, "buy()");
|
||||||
|
|
||||||
|
SkuDetails sku = this.products.get(product);
|
||||||
|
if (sku != null) {
|
||||||
|
buyProduct(sku, purchaseListener);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
List<String> skuList = new ArrayList<String>();
|
||||||
|
skuList.add(product);
|
||||||
|
querySkuDetailsAsync(skuList, new SkuDetailsResponseListener() {
|
||||||
|
@Override
|
||||||
|
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
|
||||||
|
if (billingResult.getResponseCode() == BillingResponseCode.OK) {
|
||||||
|
buyProduct(skuDetailsList.get(0), purchaseListener);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log.e(TAG, "Unable to get product details before buying: " + billingResult.getDebugMessage());
|
||||||
|
purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get details for a list of products. The products can be a mix of
|
||||||
|
* in-app products and subscriptions.
|
||||||
|
*/
|
||||||
|
private void querySkuDetailsAsync(final List<String> skuList, final SkuDetailsResponseListener listener) {
|
||||||
|
SkuDetailsResponseListener detailsListener = new SkuDetailsResponseListener() {
|
||||||
|
private List<SkuDetails> allSkuDetails = new ArrayList<SkuDetails>();
|
||||||
|
private int queries = 2;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetails) {
|
||||||
|
if (skuDetails != null) {
|
||||||
|
// cache skus (cache will be used to speed up buying)
|
||||||
|
for (SkuDetails sd : skuDetails) {
|
||||||
|
IapGooglePlay.this.products.put(sd.getSku(), sd);
|
||||||
|
}
|
||||||
|
// add to list of all sku details
|
||||||
|
allSkuDetails.addAll(skuDetails);
|
||||||
|
}
|
||||||
|
// we're finished when we have queried for both in-app and subs
|
||||||
|
queries--;
|
||||||
|
if (queries == 0) {
|
||||||
|
listener.onSkuDetailsResponse(billingResult, allSkuDetails);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
billingClient.querySkuDetailsAsync(SkuDetailsParams.newBuilder().setSkusList(skuList).setType(SkuType.INAPP).build(), detailsListener);
|
||||||
|
billingClient.querySkuDetailsAsync(SkuDetailsParams.newBuilder().setSkusList(skuList).setType(SkuType.SUBS).build(), detailsListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from Lua.
|
||||||
|
*/
|
||||||
|
public void listItems(final String products, final IListProductsListener productsListener, final long commandPtr) {
|
||||||
|
Log.d(TAG, "listItems()");
|
||||||
|
|
||||||
|
// create list of skus from comma separated string
|
||||||
|
List<String> skuList = new ArrayList<String>();
|
||||||
|
for (String p : products.split(",")) {
|
||||||
|
if (p.trim().length() > 0) {
|
||||||
|
skuList.add(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
querySkuDetailsAsync(skuList, new SkuDetailsResponseListener() {
|
||||||
|
@Override
|
||||||
|
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetails) {
|
||||||
|
JSONArray a = new JSONArray();
|
||||||
|
if (billingResult.getResponseCode() == BillingResponseCode.OK) {
|
||||||
|
for (SkuDetails sd : skuDetails) {
|
||||||
|
a.put(convertSkuDetails(sd));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log.e(TAG, "Unable to list products: " + billingResult.getDebugMessage());
|
||||||
|
}
|
||||||
|
productsListener.onProductsResult(billingResultToDefoldResponse(billingResult), a.toString(), commandPtr);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from Lua.
|
||||||
|
*/
|
||||||
|
public void restore(final IPurchaseListener listener) {
|
||||||
|
Log.d(TAG, "restore()");
|
||||||
|
processPendingConsumables(listener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,371 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
497
main/main.gui
497
main/main.gui
@ -169,7 +169,7 @@ nodes {
|
|||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
x: 485.0
|
x: 530.0
|
||||||
y: 56.0
|
y: 56.0
|
||||||
z: 0.0
|
z: 0.0
|
||||||
w: 1.0
|
w: 1.0
|
||||||
@ -199,7 +199,7 @@ nodes {
|
|||||||
w: 1.0
|
w: 1.0
|
||||||
}
|
}
|
||||||
type: TYPE_TEMPLATE
|
type: TYPE_TEMPLATE
|
||||||
id: "reset"
|
id: "restore"
|
||||||
layer: ""
|
layer: ""
|
||||||
inherit_alpha: true
|
inherit_alpha: true
|
||||||
alpha: 1.0
|
alpha: 1.0
|
||||||
@ -226,7 +226,7 @@ nodes {
|
|||||||
w: 1.0
|
w: 1.0
|
||||||
}
|
}
|
||||||
size {
|
size {
|
||||||
x: 300.0
|
x: 200.0
|
||||||
y: 88.0
|
y: 88.0
|
||||||
z: 0.0
|
z: 0.0
|
||||||
w: 1.0
|
w: 1.0
|
||||||
@ -240,12 +240,12 @@ nodes {
|
|||||||
type: TYPE_BOX
|
type: TYPE_BOX
|
||||||
blend_mode: BLEND_MODE_ALPHA
|
blend_mode: BLEND_MODE_ALPHA
|
||||||
texture: "button/button_normal"
|
texture: "button/button_normal"
|
||||||
id: "reset/larrybutton"
|
id: "restore/larrybutton"
|
||||||
xanchor: XANCHOR_NONE
|
xanchor: XANCHOR_NONE
|
||||||
yanchor: YANCHOR_NONE
|
yanchor: YANCHOR_NONE
|
||||||
pivot: PIVOT_CENTER
|
pivot: PIVOT_CENTER
|
||||||
adjust_mode: ADJUST_MODE_FIT
|
adjust_mode: ADJUST_MODE_FIT
|
||||||
parent: "reset"
|
parent: "restore"
|
||||||
layer: ""
|
layer: ""
|
||||||
inherit_alpha: true
|
inherit_alpha: true
|
||||||
slice9 {
|
slice9 {
|
||||||
@ -258,6 +258,7 @@ nodes {
|
|||||||
clipping_visible: true
|
clipping_visible: true
|
||||||
clipping_inverted: false
|
clipping_inverted: false
|
||||||
alpha: 1.0
|
alpha: 1.0
|
||||||
|
overridden_fields: 4
|
||||||
template_node_child: true
|
template_node_child: true
|
||||||
size_mode: SIZE_MODE_MANUAL
|
size_mode: SIZE_MODE_MANUAL
|
||||||
}
|
}
|
||||||
@ -294,9 +295,9 @@ nodes {
|
|||||||
}
|
}
|
||||||
type: TYPE_TEXT
|
type: TYPE_TEXT
|
||||||
blend_mode: BLEND_MODE_ALPHA
|
blend_mode: BLEND_MODE_ALPHA
|
||||||
text: "Reset"
|
text: "Restore"
|
||||||
font: "larryfont"
|
font: "larryfont"
|
||||||
id: "reset/larrylabel"
|
id: "restore/larrylabel"
|
||||||
xanchor: XANCHOR_NONE
|
xanchor: XANCHOR_NONE
|
||||||
yanchor: YANCHOR_NONE
|
yanchor: YANCHOR_NONE
|
||||||
pivot: PIVOT_CENTER
|
pivot: PIVOT_CENTER
|
||||||
@ -314,7 +315,7 @@ nodes {
|
|||||||
}
|
}
|
||||||
adjust_mode: ADJUST_MODE_FIT
|
adjust_mode: ADJUST_MODE_FIT
|
||||||
line_break: false
|
line_break: false
|
||||||
parent: "reset/larrybutton"
|
parent: "restore/larrybutton"
|
||||||
layer: ""
|
layer: ""
|
||||||
inherit_alpha: true
|
inherit_alpha: true
|
||||||
alpha: 1.0
|
alpha: 1.0
|
||||||
@ -705,7 +706,7 @@ nodes {
|
|||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
x: 159.0
|
x: 109.0
|
||||||
y: 56.0
|
y: 56.0
|
||||||
z: 0.0
|
z: 0.0
|
||||||
w: 1.0
|
w: 1.0
|
||||||
@ -762,7 +763,7 @@ nodes {
|
|||||||
w: 1.0
|
w: 1.0
|
||||||
}
|
}
|
||||||
size {
|
size {
|
||||||
x: 300.0
|
x: 200.0
|
||||||
y: 88.0
|
y: 88.0
|
||||||
z: 0.0
|
z: 0.0
|
||||||
w: 1.0
|
w: 1.0
|
||||||
@ -794,6 +795,7 @@ nodes {
|
|||||||
clipping_visible: true
|
clipping_visible: true
|
||||||
clipping_inverted: false
|
clipping_inverted: false
|
||||||
alpha: 1.0
|
alpha: 1.0
|
||||||
|
overridden_fields: 4
|
||||||
template_node_child: true
|
template_node_child: true
|
||||||
size_mode: SIZE_MODE_MANUAL
|
size_mode: SIZE_MODE_MANUAL
|
||||||
}
|
}
|
||||||
@ -1019,6 +1021,481 @@ nodes {
|
|||||||
text_leading: 1.0
|
text_leading: 1.0
|
||||||
text_tracking: 0.0
|
text_tracking: 0.0
|
||||||
}
|
}
|
||||||
|
nodes {
|
||||||
|
position {
|
||||||
|
x: 320.0
|
||||||
|
y: 56.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
rotation {
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
scale {
|
||||||
|
x: 1.0
|
||||||
|
y: 1.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_TEMPLATE
|
||||||
|
id: "pending"
|
||||||
|
layer: ""
|
||||||
|
inherit_alpha: true
|
||||||
|
alpha: 1.0
|
||||||
|
template: "/dirtylarry/button.gui"
|
||||||
|
template_node_child: false
|
||||||
|
}
|
||||||
|
nodes {
|
||||||
|
position {
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
rotation {
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
scale {
|
||||||
|
x: 1.0
|
||||||
|
y: 1.0
|
||||||
|
z: 1.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
size {
|
||||||
|
x: 200.0
|
||||||
|
y: 88.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
color {
|
||||||
|
x: 1.0
|
||||||
|
y: 1.0
|
||||||
|
z: 1.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
type: TYPE_BOX
|
||||||
|
blend_mode: BLEND_MODE_ALPHA
|
||||||
|
texture: "button/button_normal"
|
||||||
|
id: "pending/larrybutton"
|
||||||
|
xanchor: XANCHOR_NONE
|
||||||
|
yanchor: YANCHOR_NONE
|
||||||
|
pivot: PIVOT_CENTER
|
||||||
|
adjust_mode: ADJUST_MODE_FIT
|
||||||
|
parent: "pending"
|
||||||
|
layer: ""
|
||||||
|
inherit_alpha: true
|
||||||
|
slice9 {
|
||||||
|
x: 32.0
|
||||||
|
y: 32.0
|
||||||
|
z: 32.0
|
||||||
|
w: 32.0
|
||||||
|
}
|
||||||
|
clipping_mode: CLIPPING_MODE_NONE
|
||||||
|
clipping_visible: true
|
||||||
|
clipping_inverted: false
|
||||||
|
alpha: 1.0
|
||||||
|
overridden_fields: 4
|
||||||
|
template_node_child: true
|
||||||
|
size_mode: SIZE_MODE_MANUAL
|
||||||
|
}
|
||||||
|
nodes {
|
||||||
|
position {
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
rotation {
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
scale {
|
||||||
|
x: 1.0
|
||||||
|
y: 1.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: "Pending"
|
||||||
|
font: "larryfont"
|
||||||
|
id: "pending/larrylabel"
|
||||||
|
xanchor: XANCHOR_NONE
|
||||||
|
yanchor: YANCHOR_NONE
|
||||||
|
pivot: PIVOT_CENTER
|
||||||
|
outline {
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.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
|
||||||
|
parent: "pending/larrybutton"
|
||||||
|
layer: ""
|
||||||
|
inherit_alpha: true
|
||||||
|
alpha: 1.0
|
||||||
|
outline_alpha: 1.0
|
||||||
|
shadow_alpha: 1.0
|
||||||
|
overridden_fields: 8
|
||||||
|
template_node_child: true
|
||||||
|
text_leading: 1.0
|
||||||
|
text_tracking: 0.0
|
||||||
|
}
|
||||||
|
nodes {
|
||||||
|
position {
|
||||||
|
x: 40.0
|
||||||
|
y: 153.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
rotation {
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
scale {
|
||||||
|
x: 1.0
|
||||||
|
y: 1.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_TEMPLATE
|
||||||
|
id: "chk_finish"
|
||||||
|
layer: ""
|
||||||
|
inherit_alpha: true
|
||||||
|
alpha: 1.0
|
||||||
|
template: "/dirtylarry/checkbox_label.gui"
|
||||||
|
template_node_child: false
|
||||||
|
}
|
||||||
|
nodes {
|
||||||
|
position {
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
rotation {
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
scale {
|
||||||
|
x: 1.0
|
||||||
|
y: 1.0
|
||||||
|
z: 1.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
size {
|
||||||
|
x: 64.0
|
||||||
|
y: 68.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
color {
|
||||||
|
x: 1.0
|
||||||
|
y: 1.0
|
||||||
|
z: 1.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
type: TYPE_BOX
|
||||||
|
blend_mode: BLEND_MODE_ALPHA
|
||||||
|
texture: "checkbox/checkbox_normal"
|
||||||
|
id: "chk_finish/larrycheckbox"
|
||||||
|
xanchor: XANCHOR_NONE
|
||||||
|
yanchor: YANCHOR_NONE
|
||||||
|
pivot: PIVOT_CENTER
|
||||||
|
adjust_mode: ADJUST_MODE_FIT
|
||||||
|
parent: "chk_finish"
|
||||||
|
layer: ""
|
||||||
|
inherit_alpha: true
|
||||||
|
slice9 {
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
w: 0.0
|
||||||
|
}
|
||||||
|
clipping_mode: CLIPPING_MODE_NONE
|
||||||
|
clipping_visible: true
|
||||||
|
clipping_inverted: false
|
||||||
|
alpha: 1.0
|
||||||
|
template_node_child: true
|
||||||
|
size_mode: SIZE_MODE_MANUAL
|
||||||
|
}
|
||||||
|
nodes {
|
||||||
|
position {
|
||||||
|
x: 46.0
|
||||||
|
y: 3.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
rotation {
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
scale {
|
||||||
|
x: 1.0
|
||||||
|
y: 1.0
|
||||||
|
z: 1.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
size {
|
||||||
|
x: 200.0
|
||||||
|
y: 50.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: "Finish"
|
||||||
|
font: "larryfont"
|
||||||
|
id: "chk_finish/larrylabel"
|
||||||
|
xanchor: XANCHOR_NONE
|
||||||
|
yanchor: YANCHOR_NONE
|
||||||
|
pivot: PIVOT_W
|
||||||
|
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
|
||||||
|
parent: "chk_finish"
|
||||||
|
layer: ""
|
||||||
|
inherit_alpha: true
|
||||||
|
alpha: 1.0
|
||||||
|
outline_alpha: 1.0
|
||||||
|
shadow_alpha: 1.0
|
||||||
|
overridden_fields: 8
|
||||||
|
template_node_child: true
|
||||||
|
text_leading: 1.0
|
||||||
|
text_tracking: 0.0
|
||||||
|
}
|
||||||
|
nodes {
|
||||||
|
position {
|
||||||
|
x: 362.0
|
||||||
|
y: 153.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
rotation {
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
scale {
|
||||||
|
x: 1.0
|
||||||
|
y: 1.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_TEMPLATE
|
||||||
|
id: "chk_acknowledge"
|
||||||
|
layer: ""
|
||||||
|
inherit_alpha: true
|
||||||
|
alpha: 1.0
|
||||||
|
template: "/dirtylarry/checkbox_label.gui"
|
||||||
|
template_node_child: false
|
||||||
|
}
|
||||||
|
nodes {
|
||||||
|
position {
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
rotation {
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
scale {
|
||||||
|
x: 1.0
|
||||||
|
y: 1.0
|
||||||
|
z: 1.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
size {
|
||||||
|
x: 64.0
|
||||||
|
y: 68.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
color {
|
||||||
|
x: 1.0
|
||||||
|
y: 1.0
|
||||||
|
z: 1.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
type: TYPE_BOX
|
||||||
|
blend_mode: BLEND_MODE_ALPHA
|
||||||
|
texture: "checkbox/checkbox_normal"
|
||||||
|
id: "chk_acknowledge/larrycheckbox"
|
||||||
|
xanchor: XANCHOR_NONE
|
||||||
|
yanchor: YANCHOR_NONE
|
||||||
|
pivot: PIVOT_CENTER
|
||||||
|
adjust_mode: ADJUST_MODE_FIT
|
||||||
|
parent: "chk_acknowledge"
|
||||||
|
layer: ""
|
||||||
|
inherit_alpha: true
|
||||||
|
slice9 {
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
w: 0.0
|
||||||
|
}
|
||||||
|
clipping_mode: CLIPPING_MODE_NONE
|
||||||
|
clipping_visible: true
|
||||||
|
clipping_inverted: false
|
||||||
|
alpha: 1.0
|
||||||
|
template_node_child: true
|
||||||
|
size_mode: SIZE_MODE_MANUAL
|
||||||
|
}
|
||||||
|
nodes {
|
||||||
|
position {
|
||||||
|
x: 46.0
|
||||||
|
y: 3.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
rotation {
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
scale {
|
||||||
|
x: 1.0
|
||||||
|
y: 1.0
|
||||||
|
z: 1.0
|
||||||
|
w: 1.0
|
||||||
|
}
|
||||||
|
size {
|
||||||
|
x: 200.0
|
||||||
|
y: 50.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: "Acknowledge"
|
||||||
|
font: "larryfont"
|
||||||
|
id: "chk_acknowledge/larrylabel"
|
||||||
|
xanchor: XANCHOR_NONE
|
||||||
|
yanchor: YANCHOR_NONE
|
||||||
|
pivot: PIVOT_W
|
||||||
|
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
|
||||||
|
parent: "chk_acknowledge"
|
||||||
|
layer: ""
|
||||||
|
inherit_alpha: true
|
||||||
|
alpha: 1.0
|
||||||
|
outline_alpha: 1.0
|
||||||
|
shadow_alpha: 1.0
|
||||||
|
overridden_fields: 8
|
||||||
|
template_node_child: true
|
||||||
|
text_leading: 1.0
|
||||||
|
text_tracking: 0.0
|
||||||
|
}
|
||||||
material: "/builtins/materials/gui.material"
|
material: "/builtins/materials/gui.material"
|
||||||
adjust_reference: ADJUST_REFERENCE_PARENT
|
adjust_reference: ADJUST_REFERENCE_PARENT
|
||||||
max_nodes: 512
|
max_nodes: 512
|
||||||
|
@ -37,11 +37,21 @@ local function log(fmt, ...)
|
|||||||
gui.set_text(gui.get_node("log"), s)
|
gui.set_text(gui.get_node("log"), s)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function process_pending_transactions()
|
||||||
|
log("iap.process_pending_transactions()")
|
||||||
|
iap.process_pending_transactions()
|
||||||
|
end
|
||||||
|
|
||||||
local function buy(id)
|
local function buy(id)
|
||||||
log("iap.buy() " .. id)
|
log("iap.buy() " .. id)
|
||||||
iap.buy(id)
|
iap.buy(id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function restore()
|
||||||
|
log("iap.restore()")
|
||||||
|
iap.restore()
|
||||||
|
end
|
||||||
|
|
||||||
local function list()
|
local function list()
|
||||||
log("iap.list()")
|
log("iap.list()")
|
||||||
for item, button in pairs(item_buttons) do
|
for item, button in pairs(item_buttons) do
|
||||||
@ -68,25 +78,32 @@ end
|
|||||||
|
|
||||||
|
|
||||||
local function buy_listener(self, transaction, error)
|
local function buy_listener(self, transaction, error)
|
||||||
pprint(transaction, error)
|
pprint(transaction)
|
||||||
if error then
|
if error then
|
||||||
log("iap.buy() error %s - %s", tostring(error.error), tostring(error.reason))
|
log("iap.buy() error %s - %s", tostring(error.error), tostring(error.reason))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if iap.get_provider_id() == iap.PROVIDER_ID_GOOGLE and transaction.ident == NON_CONSUMABLE then
|
if iap.get_provider_id() == iap.PROVIDER_ID_GOOGLE and transaction.ident == NON_CONSUMABLE then
|
||||||
log("iap.buy() ok - google")
|
log("iap.buy() ok - google")
|
||||||
gui.set_color(gui.get_node("reset/larrylabel"), vmath.vector4(1,1,1,1))
|
gui.set_color(gui.get_node("reset/larrylabel"), vmath.vector4(1,1,1,1))
|
||||||
product_items["reset"] = transaction
|
product_items["reset"] = transaction
|
||||||
else
|
else
|
||||||
log("iap.buy() ok")
|
log("iap.buy() ok %s", transaction.ident)
|
||||||
log("iap.finish()")
|
if self.finish then
|
||||||
iap.finish(transaction)
|
log("iap.finish() %s", transaction.ident)
|
||||||
|
iap.finish(transaction)
|
||||||
|
elseif self.acknowledge then
|
||||||
|
log("iap.acknowledge() %s", transaction.ident)
|
||||||
|
iap.acknowledge(transaction)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function init(self)
|
function init(self)
|
||||||
self.log = {}
|
self.log = {}
|
||||||
|
self.finish = false
|
||||||
|
self.acknowledge = false
|
||||||
log("init()")
|
log("init()")
|
||||||
msg.post(".", "acquire_input_focus")
|
msg.post(".", "acquire_input_focus")
|
||||||
if not iap then
|
if not iap then
|
||||||
@ -110,5 +127,13 @@ function on_input(self, action_id, action)
|
|||||||
dirtylarry:button("list", action_id, action, function()
|
dirtylarry:button("list", action_id, action, function()
|
||||||
list()
|
list()
|
||||||
end)
|
end)
|
||||||
|
dirtylarry:button("restore", action_id, action, function()
|
||||||
|
restore()
|
||||||
|
end)
|
||||||
|
dirtylarry:button("pending", action_id, action, function()
|
||||||
|
process_pending_transactions()
|
||||||
|
end)
|
||||||
|
self.finish = dirtylarry:checkbox("chk_finish", action_id, action, self.finish)
|
||||||
|
self.acknowledge = dirtylarry:checkbox("chk_acknowledge", action_id, action, self.acknowledge)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user