mirror of
https://github.com/defold/extension-iap
synced 2025-09-27 17:12:18 +02:00
Compare commits
57 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
fb3d6b78aa | ||
|
c4b177a24d | ||
|
09544baf1c | ||
|
6d3ca89b65 | ||
|
0df5a8e968 | ||
|
88028fd29f | ||
|
c0e35039f4 | ||
|
2e17b2f413 | ||
|
f398677d7b | ||
|
d1c4e88562 | ||
|
6f06d7f08f | ||
|
0852e42977 | ||
|
33174f25ea | ||
|
ba0e1b645a | ||
|
801179288e | ||
|
1c27130eef | ||
|
195ef400b5 | ||
|
68ef7f4615 | ||
|
1a34582603 | ||
|
09f5060d44 | ||
|
649a8a1ebf | ||
|
5c09447e37 | ||
|
ad06de7b9c | ||
|
d74c97d5c7 | ||
|
f97a7ee6b6 | ||
|
5f3f43fb2e | ||
|
caff4397d8 | ||
|
501af9c90d | ||
|
961a43f732 | ||
|
47f03108ab | ||
|
c0e1a9aef1 | ||
|
f4294a9eb5 | ||
|
8799860198 | ||
|
40648f4773 | ||
|
3d3433e07f | ||
|
52493d8c83 | ||
|
c09aa64ab5 | ||
|
d0ca1e05fd | ||
|
05e9001404 | ||
|
a26bb45791 | ||
|
0a10ff0878 | ||
|
508a4f8681 | ||
|
5e1738d353 | ||
|
df7f05c1a6 | ||
|
3ecd6ef587 | ||
|
92d90bb4de | ||
|
e6e2f2de63 | ||
|
3468b52658 | ||
|
bda785ef08 | ||
|
b3b79006d7 | ||
|
8b2c7b0bca | ||
|
78d1c80c19 | ||
|
beb89d507f | ||
|
1a6d2f01e5 | ||
|
1fe29ad24a | ||
|
276437b981 | ||
|
246fcd3179 |
7
.github/workflows/bob.yml
vendored
Normal file
7
.github/workflows/bob.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
name: Build with bob
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
uses: defold/github-actions-common/.github/workflows/bob.yml@master
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,3 +9,5 @@ Thumbs.db
|
|||||||
.cproject
|
.cproject
|
||||||
builtins
|
builtins
|
||||||
_site
|
_site
|
||||||
|
manifest.private.der
|
||||||
|
manifest.public.der
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
[](https://github.com/defold/extension-iap/actions)
|
||||||
|
|
||||||
# In-app purchase extension for Defold
|
# In-app purchase extension for Defold
|
||||||
|
|
||||||
Defold [native extension](https://www.defold.com/manuals/extensions/) which provides access to In-app purchase functionality on iOS, Android (Google Play and Amazon) and Facebook Canvas platforms.
|
Defold [native extension](https://www.defold.com/manuals/extensions/) which provides access to In-app purchase functionality on iOS, Android (Google Play and Amazon) and Facebook Canvas platforms.
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Defold In-app purchase extension API documentation
|
title: Defold In-app purchase extension API documentation
|
||||||
brief: This manual covers how to setup and use Google Play Game Services in Defold.
|
brief: This manual covers how to setup and use In-App Purchases in Defold.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Defold In-app purchase extension API documentation
|
# Defold In-app purchase extension API documentation
|
||||||
@@ -8,7 +8,7 @@ brief: This manual covers how to setup and use Google Play Game Services in Defo
|
|||||||
This extension provides a unified, simple to use interface to several different stores for in-app purchase:
|
This extension provides a unified, simple to use interface to several different stores for in-app purchase:
|
||||||
|
|
||||||
* Apple’s iOS Appstore - StoreKit
|
* Apple’s iOS Appstore - StoreKit
|
||||||
* Google Play Billing 3.0
|
* Google Play Billing 5.0
|
||||||
* Amazon 'in-app billing' 2.0.61
|
* Amazon 'in-app billing' 2.0.61
|
||||||
* Facebook Canvas 'game payments'
|
* Facebook Canvas 'game payments'
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ Detailed documentation from Apple, Google, Amazon and Facebook can be found here
|
|||||||
## Installation
|
## Installation
|
||||||
To use this library in your Defold project, add the following URL to your `game.project` dependencies:
|
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](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).
|
||||||
|
|
||||||
@@ -186,7 +186,11 @@ IAP supports auto-completion, where fulfilment is automatically signalled to the
|
|||||||
|
|
||||||
### Consumable vs non-consumable products
|
### Consumable vs non-consumable products
|
||||||
|
|
||||||
The Google Play store does only support consumable products. If you need non-consumable products it is recommended to use manual fulfilment of purchases and never finish purchases for products that should be non-consumable. As long as a purchase isn't finished it will be returned as an active purchase when `iap.set_listener()` is called. If you do not call `iap.finish()` on a purchase you still need to indicate to Google Play that the purchase has been handled. You can do this by calling `iap.acknowledge()`. If you do not call `iap.acknowledge()` the purchase will be automatically refunded by Google after a few days.
|
#### Google Play
|
||||||
|
|
||||||
|
It is recommended to use manual fulfilment of purchases and never finish purchases for products that should be non-consumable. As long as a purchase isn't finished it will be returned as an active purchase when `iap.set_listener()` is called. If you do not call `iap.finish()` on a purchase you still need to indicate to Google Play that the purchase has been handled. You can do this by calling `iap.acknowledge()`. If you do not call `iap.acknowledge()` the purchase will be automatically refunded by Google after a few days.
|
||||||
|
|
||||||
|
#### App Store
|
||||||
|
|
||||||
The Apple App Store supports non-consumable products which means that you need to finish all purchases when you provide products to your users. You can do it automatically by keeping the default behavior in the game project settings or manually (if you want to do that after server validation, for example) using `iap.finish()`.
|
The Apple App Store supports non-consumable products which means that you need to finish all purchases when you provide products to your users. You can do it automatically by keeping the default behavior in the game project settings or manually (if you want to do that after server validation, for example) using `iap.finish()`.
|
||||||
|
|
||||||
@@ -234,6 +238,3 @@ On iOS, the "price_string" field contains '~' characters
|
|||||||
## 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)
|
||||||
|
|
||||||
|
|
||||||
## API reference
|
|
||||||
|
@@ -21,8 +21,12 @@
|
|||||||
members:
|
members:
|
||||||
- name: request_id
|
- name: request_id
|
||||||
type: string
|
type: string
|
||||||
desc: Facebook only. [icon:facebook] Optional custom unique request id to
|
desc: Facebook only [icon:facebook]. Optional custom unique request id to
|
||||||
set for this transaction. The id becomes attached to the payment within the Graph API.
|
set for this transaction. The id becomes attached to the payment within the Graph API.
|
||||||
|
- name: token
|
||||||
|
type: string
|
||||||
|
desc: Google Play only [icon:googleplay]. Which subscription offer to use when buying a subscription. The token can be retrieved from
|
||||||
|
the subscriptions table returned when calling iap.list()
|
||||||
|
|
||||||
examples:
|
examples:
|
||||||
- desc: |-
|
- desc: |-
|
||||||
@@ -128,15 +132,56 @@
|
|||||||
- name: price
|
- name: price
|
||||||
type: number
|
type: number
|
||||||
desc: The price of the product.
|
desc: The price of the product.
|
||||||
|
For Google Play [icon:googleplay] this is used only for in-app products
|
||||||
|
|
||||||
- name: price_string
|
- name: price_string
|
||||||
type: string
|
type: string
|
||||||
desc: The price of the product, as a formatted string (amount and currency symbol).
|
desc: The price of the product, as a formatted string (amount and currency symbol).
|
||||||
|
For Google Play [icon:googleplay] this is used only for in-app products
|
||||||
|
|
||||||
- name: currency_code
|
- name: currency_code
|
||||||
type: string
|
type: string
|
||||||
desc: The currency code. On Google Play, this reflects the merchant's locale, instead of the user's.
|
desc: The currency code.
|
||||||
[icon:ios] [icon:googleplay] [icon:facebook]
|
For Google Play [icon:googleplay] this is the merchant's locale, instead of the user's.
|
||||||
|
For Google Play [icon:googleplay] this is used only for in-app products
|
||||||
|
|
||||||
|
- name: subscriptions
|
||||||
|
type: table
|
||||||
|
desc: Only available for Google Play [icon:googleplay]. List of subscription offers.
|
||||||
|
Each offer contains a token and a list of price and billing options.
|
||||||
|
See https://developer.android.com/reference/com/android/billingclient/api/ProductDetails.PricingPhase
|
||||||
|
members:
|
||||||
|
- name: token
|
||||||
|
type: string
|
||||||
|
desc: The token associated with the pricing phases for the subscription.
|
||||||
|
|
||||||
|
- name: pricing
|
||||||
|
type: table
|
||||||
|
desc: The pricing phases for the subscription.
|
||||||
|
members:
|
||||||
|
- name: price_string
|
||||||
|
type: string
|
||||||
|
desc: Formatted price for the payment cycle, including currency sign.
|
||||||
|
|
||||||
|
- name: price
|
||||||
|
type: number
|
||||||
|
desc: Price of the payment cycle in micro-units.
|
||||||
|
|
||||||
|
- name: currency_code
|
||||||
|
type: string
|
||||||
|
desc: ISO 4217 currency code
|
||||||
|
|
||||||
|
- name: billing_period
|
||||||
|
type: string
|
||||||
|
desc: Billing period of the payment cycle, specified in ISO 8601 format
|
||||||
|
|
||||||
|
- name: billing_cycle_count
|
||||||
|
type: number
|
||||||
|
desc: Number of cycles for which the billing period is applied.
|
||||||
|
|
||||||
|
- name: recurrence_mode
|
||||||
|
type: string
|
||||||
|
desc: FINITE, INFINITE or NONE
|
||||||
|
|
||||||
- name: error
|
- name: error
|
||||||
type: table
|
type: table
|
||||||
@@ -224,32 +269,36 @@
|
|||||||
|
|
||||||
- name: original_trans
|
- name: original_trans
|
||||||
type: string
|
type: string
|
||||||
desc: Apple only[icon:apple]. The original transaction. This field is only set when `state` is `TRANS_STATE_RESTORED`.
|
desc: Apple only [icon:apple]. The original transaction. This field is only set when `state` is `TRANS_STATE_RESTORED`.
|
||||||
|
|
||||||
|
- name: original_json
|
||||||
|
type: string
|
||||||
|
desc: Android only [icon:android]. The purchase order details in JSON format.
|
||||||
|
|
||||||
- name: signature
|
- name: signature
|
||||||
type: string
|
type: string
|
||||||
desc: Google Play only[icon:googleplay]. A string containing the signature of the purchase data that was signed with the private key of the developer.
|
desc: Google Play only [icon:googleplay]. A string containing the signature of the purchase data that was signed with the private key of the developer.
|
||||||
|
|
||||||
- name: request_id
|
- name: request_id
|
||||||
type: string
|
type: string
|
||||||
desc: Facebook only[icon:facebook]. This field is set to the optional custom unique request id `request_id` if set in the `iap.buy()` call parameters.
|
desc: Facebook only [icon:facebook]. This field is set to the optional custom unique request id `request_id` if set in the `iap.buy()` call parameters.
|
||||||
|
|
||||||
- name: user_id
|
- name: user_id
|
||||||
type: string
|
type: string
|
||||||
desc: Amazon Pay only[icon:amazon]. The user ID.
|
desc: Amazon Pay only [icon:amazon]. The user ID.
|
||||||
|
|
||||||
- name: is_sandbox_mode
|
- name: is_sandbox_mode
|
||||||
type: boolean
|
type: boolean
|
||||||
desc: Amazon Pay only[icon:amazon]. If `true`, the SDK is running in Sandbox mode.
|
desc: Amazon Pay only [icon:amazon]. If `true`, the SDK is running in Sandbox mode.
|
||||||
This only allows interactions with the Amazon AppTester. Use this mode only for testing locally.
|
This only allows interactions with the Amazon AppTester. Use this mode only for testing locally.
|
||||||
|
|
||||||
- name: cancel_date
|
- name: cancel_date
|
||||||
type: string
|
type: string
|
||||||
desc: Amazon Pay only[icon:amazon]. The cancel date for the purchase. This field is only set if the purchase is canceled.
|
desc: Amazon Pay only [icon:amazon]. The cancel date for the purchase. This field is only set if the purchase is canceled.
|
||||||
|
|
||||||
- name: canceled
|
- name: canceled
|
||||||
type: string
|
type: string
|
||||||
desc: Amazon Pay only[icon:amazon]. Is set to `true` if the receipt was canceled or has expired; otherwise `false`.
|
desc: Amazon Pay only [icon:amazon]. Is set to `true` if the receipt was canceled or has expired; otherwise `false`.
|
||||||
|
|
||||||
- name: error
|
- name: error
|
||||||
type: table
|
type: table
|
||||||
|
@@ -1,10 +1,6 @@
|
|||||||
name: IAPExt
|
name: IAPExt
|
||||||
|
|
||||||
platforms:
|
platforms:
|
||||||
armv7-ios:
|
|
||||||
context:
|
|
||||||
weakFrameworks: ['StoreKit', 'UIKit', 'Foundation']
|
|
||||||
|
|
||||||
arm64-ios:
|
arm64-ios:
|
||||||
context:
|
context:
|
||||||
weakFrameworks: ['StoreKit', 'UIKit', 'Foundation']
|
weakFrameworks: ['StoreKit', 'UIKit', 'Foundation']
|
||||||
|
11
extension-iap/ext.properties
Normal file
11
extension-iap/ext.properties
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[iap]
|
||||||
|
title = IAP
|
||||||
|
help = Settings for In-App Purchases extension
|
||||||
|
group = Runtime
|
||||||
|
|
||||||
|
iap_provider.type = string
|
||||||
|
iap_provider.default = GooglePlay
|
||||||
|
iap_provider.options = GooglePlay, Amazon
|
||||||
|
|
||||||
|
auto_finish_transactions.type = bool
|
||||||
|
auto_finish_transactions.default = 1
|
Binary file not shown.
@@ -71,7 +71,7 @@ var LibraryFacebookIAP = {
|
|||||||
|
|
||||||
if(url_index == product_count-1) {
|
if(url_index == product_count-1) {
|
||||||
var productsJSON = JSON.stringify(products);
|
var productsJSON = JSON.stringify(products);
|
||||||
var res_buf = allocate(intArrayFromString(productsJSON), 'i8', ALLOC_STACK);
|
var res_buf = stringToUTF8OnStack(productsJSON);
|
||||||
{{{ makeDynCall('vii', 'callback')}}}(lua_callback, res_buf);
|
{{{ makeDynCall('vii', 'callback')}}}(lua_callback, res_buf);
|
||||||
} else {
|
} else {
|
||||||
var xmlhttp = new XMLHttpRequest();
|
var xmlhttp = new XMLHttpRequest();
|
||||||
@@ -143,7 +143,7 @@ var LibraryFacebookIAP = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var productsJSON = JSON.stringify(result)
|
var productsJSON = JSON.stringify(result)
|
||||||
var res_buf = allocate(intArrayFromString(productsJSON), 'i8', ALLOC_STACK);
|
var res_buf = stringToUTF8OnStack(productsJSON);
|
||||||
{{{ makeDynCall('viii', 'callback')}}}(lua_callback, res_buf, 0);
|
{{{ makeDynCall('viii', 'callback')}}}(lua_callback, res_buf, 0);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -166,4 +166,4 @@ var LibraryFacebookIAP = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
autoAddDeps(LibraryFacebookIAP, '$FBinner');
|
autoAddDeps(LibraryFacebookIAP, '$FBinner');
|
||||||
mergeInto(LibraryManager.library, LibraryFacebookIAP);
|
addToLibrary(LibraryFacebookIAP);
|
||||||
|
@@ -4,9 +4,9 @@
|
|||||||
<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 Amazon IAP -->
|
<!-- For Amazon IAP -->
|
||||||
<receiver android:name="com.amazon.device.iap.ResponseReceiver" >
|
<receiver android:exported="true" android:name="com.amazon.device.iap.ResponseReceiver" android:permission="com.amazon.inapp.purchasing.Permission.NOTIFY">
|
||||||
<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" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
</application>
|
</application>
|
||||||
|
@@ -1,3 +1,7 @@
|
|||||||
dependencies {
|
repositories {
|
||||||
implementation 'com.android.billingclient:billing:3.0.0'
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'com.android.billingclient:billing:7.0.0'
|
||||||
}
|
}
|
||||||
|
@@ -31,6 +31,7 @@ enum BillingResponse
|
|||||||
BILLING_RESPONSE_RESULT_ERROR = 6,
|
BILLING_RESPONSE_RESULT_ERROR = 6,
|
||||||
BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7,
|
BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7,
|
||||||
BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8,
|
BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8,
|
||||||
|
BILLING_RESPONSE_RESULT_NETWORK_ERROR = 9,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ProviderId
|
enum ProviderId
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
#if defined(DM_PLATFORM_ANDROID)
|
#if defined(DM_PLATFORM_ANDROID)
|
||||||
|
|
||||||
#include <dmsdk/sdk.h>
|
#include <dmsdk/sdk.h>
|
||||||
#include <jni.h>
|
#include <dmsdk/dlib/android.h>
|
||||||
|
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include "iap.h"
|
#include "iap.h"
|
||||||
@@ -9,19 +10,6 @@
|
|||||||
|
|
||||||
#define LIB_NAME "iap"
|
#define LIB_NAME "iap"
|
||||||
|
|
||||||
static JNIEnv* Attach()
|
|
||||||
{
|
|
||||||
JNIEnv* env;
|
|
||||||
dmGraphics::GetNativeAndroidJavaVM()->AttachCurrentThread(&env, NULL);
|
|
||||||
return env;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void Detach()
|
|
||||||
{
|
|
||||||
dmGraphics::GetNativeAndroidJavaVM()->DetachCurrentThread();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
struct IAP
|
struct IAP
|
||||||
{
|
{
|
||||||
IAP()
|
IAP()
|
||||||
@@ -52,24 +40,27 @@ static IAP g_IAP;
|
|||||||
|
|
||||||
static int IAP_ProcessPendingTransactions(lua_State* L)
|
static int IAP_ProcessPendingTransactions(lua_State* L)
|
||||||
{
|
{
|
||||||
JNIEnv* env = Attach();
|
DM_LUA_STACK_CHECK(L, 0);
|
||||||
|
|
||||||
|
dmAndroid::ThreadAttacher threadAttacher;
|
||||||
|
JNIEnv* env = threadAttacher.GetEnv();
|
||||||
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_ProcessPendingConsumables, g_IAP.m_IAPJNI);
|
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_ProcessPendingConsumables, g_IAP.m_IAPJNI);
|
||||||
Detach();
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int IAP_List(lua_State* L)
|
static int IAP_List(lua_State* L)
|
||||||
{
|
{
|
||||||
int top = lua_gettop(L);
|
DM_LUA_STACK_CHECK(L, 0);
|
||||||
|
|
||||||
char* buf = IAP_List_CreateBuffer(L);
|
char* buf = IAP_List_CreateBuffer(L);
|
||||||
if( buf == 0 )
|
if( buf == 0 )
|
||||||
{
|
{
|
||||||
assert(top == lua_gettop(L));
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEnv* env = Attach();
|
dmAndroid::ThreadAttacher threadAttacher;
|
||||||
|
JNIEnv* env = threadAttacher.GetEnv();
|
||||||
IAPCommand* cmd = new IAPCommand;
|
IAPCommand* cmd = new IAPCommand;
|
||||||
cmd->m_Callback = dmScript::CreateCallback(L, 2);
|
cmd->m_Callback = dmScript::CreateCallback(L, 2);
|
||||||
cmd->m_Command = IAP_PRODUCT_RESULT;
|
cmd->m_Command = IAP_PRODUCT_RESULT;
|
||||||
@@ -77,39 +68,48 @@ static int IAP_List(lua_State* L)
|
|||||||
jstring products = env->NewStringUTF(buf);
|
jstring products = env->NewStringUTF(buf);
|
||||||
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_List, products, g_IAP.m_IAPJNI, (jlong)cmd);
|
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_List, products, g_IAP.m_IAPJNI, (jlong)cmd);
|
||||||
env->DeleteLocalRef(products);
|
env->DeleteLocalRef(products);
|
||||||
Detach();
|
|
||||||
|
|
||||||
free(buf);
|
free(buf);
|
||||||
assert(top == lua_gettop(L));
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int IAP_Buy(lua_State* L)
|
static int IAP_Buy(lua_State* L)
|
||||||
{
|
{
|
||||||
|
DM_LUA_STACK_CHECK(L, 0);
|
||||||
|
|
||||||
int top = lua_gettop(L);
|
int top = lua_gettop(L);
|
||||||
|
|
||||||
const char* id = luaL_checkstring(L, 1);
|
const char* id = luaL_checkstring(L, 1);
|
||||||
|
const char* token = "";
|
||||||
|
|
||||||
JNIEnv* env = Attach();
|
if (top >= 2 && lua_istable(L, 2)) {
|
||||||
|
luaL_checktype(L, 2, LUA_TTABLE);
|
||||||
|
lua_pushvalue(L, 2);
|
||||||
|
lua_getfield(L, -1, "token");
|
||||||
|
token = lua_isnil(L, -1) ? "" : luaL_checkstring(L, -1);
|
||||||
|
lua_pop(L, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
dmAndroid::ThreadAttacher threadAttacher;
|
||||||
|
JNIEnv* env = threadAttacher.GetEnv();
|
||||||
jstring ids = env->NewStringUTF(id);
|
jstring ids = env->NewStringUTF(id);
|
||||||
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_Buy, ids, g_IAP.m_IAPJNI);
|
jstring tokens = env->NewStringUTF(token);
|
||||||
|
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_Buy, ids, tokens, g_IAP.m_IAPJNI);
|
||||||
env->DeleteLocalRef(ids);
|
env->DeleteLocalRef(ids);
|
||||||
Detach();
|
env->DeleteLocalRef(tokens);
|
||||||
|
|
||||||
assert(top == lua_gettop(L));
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int IAP_Finish(lua_State* L)
|
static int IAP_Finish(lua_State* L)
|
||||||
{
|
{
|
||||||
|
DM_LUA_STACK_CHECK(L, 0);
|
||||||
|
|
||||||
if(g_IAP.m_autoFinishTransactions)
|
if(g_IAP.m_autoFinishTransactions)
|
||||||
{
|
{
|
||||||
dmLogWarning("Calling iap.finish when autofinish transactions is enabled. Ignored.");
|
dmLogWarning("Calling iap.finish when autofinish transactions is enabled. Ignored.");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int top = lua_gettop(L);
|
|
||||||
|
|
||||||
luaL_checktype(L, 1, LUA_TTABLE);
|
luaL_checktype(L, 1, LUA_TTABLE);
|
||||||
|
|
||||||
lua_getfield(L, -1, "state");
|
lua_getfield(L, -1, "state");
|
||||||
@@ -119,7 +119,6 @@ static int IAP_Finish(lua_State* L)
|
|||||||
{
|
{
|
||||||
dmLogError("Invalid transaction state (must be iap.TRANS_STATE_PURCHASED).");
|
dmLogError("Invalid transaction state (must be iap.TRANS_STATE_PURCHASED).");
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
assert(top == lua_gettop(L));
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,20 +134,19 @@ static int IAP_Finish(lua_State* L)
|
|||||||
const char * receipt = lua_tostring(L, -1);
|
const char * receipt = lua_tostring(L, -1);
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
|
|
||||||
JNIEnv* env = Attach();
|
dmAndroid::ThreadAttacher threadAttacher;
|
||||||
|
JNIEnv* env = threadAttacher.GetEnv();
|
||||||
jstring receiptUTF = env->NewStringUTF(receipt);
|
jstring receiptUTF = env->NewStringUTF(receipt);
|
||||||
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_FinishTransaction, receiptUTF, g_IAP.m_IAPJNI);
|
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_FinishTransaction, receiptUTF, g_IAP.m_IAPJNI);
|
||||||
env->DeleteLocalRef(receiptUTF);
|
env->DeleteLocalRef(receiptUTF);
|
||||||
Detach();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(top == lua_gettop(L));
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int IAP_Acknowledge(lua_State* L)
|
static int IAP_Acknowledge(lua_State* L)
|
||||||
{
|
{
|
||||||
int top = lua_gettop(L);
|
DM_LUA_STACK_CHECK(L, 0);
|
||||||
|
|
||||||
luaL_checktype(L, 1, LUA_TTABLE);
|
luaL_checktype(L, 1, LUA_TTABLE);
|
||||||
|
|
||||||
@@ -159,7 +157,6 @@ static int IAP_Acknowledge(lua_State* L)
|
|||||||
{
|
{
|
||||||
dmLogError("Invalid transaction state (must be iap.TRANS_STATE_PURCHASED).");
|
dmLogError("Invalid transaction state (must be iap.TRANS_STATE_PURCHASED).");
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
assert(top == lua_gettop(L));
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,14 +172,13 @@ static int IAP_Acknowledge(lua_State* L)
|
|||||||
const char * receipt = lua_tostring(L, -1);
|
const char * receipt = lua_tostring(L, -1);
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
|
|
||||||
JNIEnv* env = Attach();
|
dmAndroid::ThreadAttacher threadAttacher;
|
||||||
|
JNIEnv* env = threadAttacher.GetEnv();
|
||||||
jstring receiptUTF = env->NewStringUTF(receipt);
|
jstring receiptUTF = env->NewStringUTF(receipt);
|
||||||
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_AcknowledgeTransaction, receiptUTF, g_IAP.m_IAPJNI);
|
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_AcknowledgeTransaction, receiptUTF, g_IAP.m_IAPJNI);
|
||||||
env->DeleteLocalRef(receiptUTF);
|
env->DeleteLocalRef(receiptUTF);
|
||||||
Detach();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(top == lua_gettop(L));
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,13 +186,11 @@ static int IAP_Restore(lua_State* L)
|
|||||||
{
|
{
|
||||||
// TODO: Missing callback here for completion/error
|
// TODO: Missing callback here for completion/error
|
||||||
// See iap_ios.mm
|
// See iap_ios.mm
|
||||||
|
DM_LUA_STACK_CHECK(L, 1);
|
||||||
|
|
||||||
int top = lua_gettop(L);
|
dmAndroid::ThreadAttacher threadAttacher;
|
||||||
JNIEnv* env = Attach();
|
JNIEnv* env = threadAttacher.GetEnv();
|
||||||
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_Restore, g_IAP.m_IAPJNI);
|
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_Restore, g_IAP.m_IAPJNI);
|
||||||
Detach();
|
|
||||||
|
|
||||||
assert(top == lua_gettop(L));
|
|
||||||
|
|
||||||
lua_pushboolean(L, 1);
|
lua_pushboolean(L, 1);
|
||||||
return 1;
|
return 1;
|
||||||
@@ -204,6 +198,8 @@ static int IAP_Restore(lua_State* L)
|
|||||||
|
|
||||||
static int IAP_SetListener(lua_State* L)
|
static int IAP_SetListener(lua_State* L)
|
||||||
{
|
{
|
||||||
|
DM_LUA_STACK_CHECK(L, 0);
|
||||||
|
|
||||||
IAP* iap = &g_IAP;
|
IAP* iap = &g_IAP;
|
||||||
|
|
||||||
bool had_previous = iap->m_Listener != 0;
|
bool had_previous = iap->m_Listener != 0;
|
||||||
@@ -215,15 +211,17 @@ static int IAP_SetListener(lua_State* L)
|
|||||||
|
|
||||||
// On first set listener, trigger process old ones.
|
// On first set listener, trigger process old ones.
|
||||||
if (!had_previous) {
|
if (!had_previous) {
|
||||||
JNIEnv* env = Attach();
|
dmAndroid::ThreadAttacher threadAttacher;
|
||||||
|
JNIEnv* env = threadAttacher.GetEnv();
|
||||||
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_ProcessPendingConsumables, g_IAP.m_IAPJNI);
|
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_ProcessPendingConsumables, g_IAP.m_IAPJNI);
|
||||||
Detach();
|
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int IAP_GetProviderId(lua_State* L)
|
static int IAP_GetProviderId(lua_State* L)
|
||||||
{
|
{
|
||||||
|
DM_LUA_STACK_CHECK(L, 1);
|
||||||
|
|
||||||
lua_pushinteger(L, g_IAP.m_ProviderId);
|
lua_pushinteger(L, g_IAP.m_ProviderId);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -308,23 +306,9 @@ static void HandleProductResult(const IAPCommand* cmd)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cmd->m_ResponseCode == BILLING_RESPONSE_RESULT_OK) {
|
if (cmd->m_ResponseCode == BILLING_RESPONSE_RESULT_OK) {
|
||||||
dmJson::Document doc;
|
const char* json = (const char*)cmd->m_Data;
|
||||||
dmJson::Result r = dmJson::Parse((const char*) cmd->m_Data, &doc);
|
dmScript::JsonToLua(L, json, strlen(json)); // throws lua error if it fails
|
||||||
if (r == dmJson::RESULT_OK && doc.m_NodeCount > 0) {
|
lua_pushnil(L);
|
||||||
char err_str[128];
|
|
||||||
if (dmScript::JsonToLua(L, &doc, 0, err_str, sizeof(err_str)) < 0) {
|
|
||||||
dmLogError("Failed converting product result JSON to Lua; %s", err_str);
|
|
||||||
lua_pushnil(L);
|
|
||||||
IAP_PushError(L, "failed to convert JSON to Lua for product response", REASON_UNSPECIFIED);
|
|
||||||
} else {
|
|
||||||
lua_pushnil(L);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dmLogError("Failed to parse product response (%d)", r);
|
|
||||||
lua_pushnil(L);
|
|
||||||
IAP_PushError(L, "failed to parse product response", REASON_UNSPECIFIED);
|
|
||||||
}
|
|
||||||
dmJson::Free(&doc);
|
|
||||||
} else {
|
} else {
|
||||||
dmLogError("IAP error %d", cmd->m_ResponseCode);
|
dmLogError("IAP error %d", cmd->m_ResponseCode);
|
||||||
lua_pushnil(L);
|
lua_pushnil(L);
|
||||||
@@ -358,23 +342,9 @@ static void HandlePurchaseResult(const IAPCommand* cmd)
|
|||||||
|
|
||||||
if (cmd->m_ResponseCode == BILLING_RESPONSE_RESULT_OK) {
|
if (cmd->m_ResponseCode == BILLING_RESPONSE_RESULT_OK) {
|
||||||
if (cmd->m_Data != 0) {
|
if (cmd->m_Data != 0) {
|
||||||
dmJson::Document doc;
|
const char* json = (const char*)cmd->m_Data;
|
||||||
dmJson::Result r = dmJson::Parse((const char*) cmd->m_Data, &doc);
|
dmScript::JsonToLua(L, json, strlen(json)); // throws lua error if it fails
|
||||||
if (r == dmJson::RESULT_OK && doc.m_NodeCount > 0) {
|
lua_pushnil(L);
|
||||||
char err_str[128];
|
|
||||||
if (dmScript::JsonToLua(L, &doc, 0, err_str, sizeof(err_str)) < 0) {
|
|
||||||
dmLogError("Failed converting purchase JSON result to Lua; %s", err_str);
|
|
||||||
lua_pushnil(L);
|
|
||||||
IAP_PushError(L, "failed to convert purchase response JSON to Lua", REASON_UNSPECIFIED);
|
|
||||||
} else {
|
|
||||||
lua_pushnil(L);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dmLogError("Failed to parse purchase response (%d)", r);
|
|
||||||
lua_pushnil(L);
|
|
||||||
IAP_PushError(L, "failed to parse purchase response", REASON_UNSPECIFIED);
|
|
||||||
}
|
|
||||||
dmJson::Free(&doc);
|
|
||||||
} else {
|
} else {
|
||||||
dmLogError("IAP error, purchase response was null");
|
dmLogError("IAP error, purchase response was null");
|
||||||
lua_pushnil(L);
|
lua_pushnil(L);
|
||||||
@@ -402,13 +372,8 @@ static dmExtension::Result InitializeIAP(dmExtension::Params* params)
|
|||||||
|
|
||||||
g_IAP.m_autoFinishTransactions = dmConfigFile::GetInt(params->m_ConfigFile, "iap.auto_finish_transactions", 1) == 1;
|
g_IAP.m_autoFinishTransactions = dmConfigFile::GetInt(params->m_ConfigFile, "iap.auto_finish_transactions", 1) == 1;
|
||||||
|
|
||||||
JNIEnv* env = Attach();
|
dmAndroid::ThreadAttacher threadAttacher;
|
||||||
|
JNIEnv* env = threadAttacher.GetEnv();
|
||||||
jclass activity_class = env->FindClass("android/app/NativeActivity");
|
|
||||||
jmethodID get_class_loader = env->GetMethodID(activity_class,"getClassLoader", "()Ljava/lang/ClassLoader;");
|
|
||||||
jobject cls = env->CallObjectMethod(dmGraphics::GetNativeAndroidActivity(), get_class_loader);
|
|
||||||
jclass class_loader = env->FindClass("java/lang/ClassLoader");
|
|
||||||
jmethodID find_class = env->GetMethodID(class_loader, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
|
|
||||||
|
|
||||||
const char* provider = dmConfigFile::GetString(params->m_ConfigFile, "android.iap_provider", "GooglePlay");
|
const char* provider = dmConfigFile::GetString(params->m_ConfigFile, "android.iap_provider", "GooglePlay");
|
||||||
const char* class_name = "com.defold.iap.IapGooglePlay";
|
const char* class_name = "com.defold.iap.IapGooglePlay";
|
||||||
@@ -422,17 +387,11 @@ static dmExtension::Result InitializeIAP(dmExtension::Params* params)
|
|||||||
dmLogWarning("Unknown IAP provider name [%s], defaulting to GooglePlay", provider);
|
dmLogWarning("Unknown IAP provider name [%s], defaulting to GooglePlay", provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
jstring str_class_name = env->NewStringUTF(class_name);
|
jclass iap_class = dmAndroid::LoadClass(env, class_name);
|
||||||
|
jclass iap_jni_class = dmAndroid::LoadClass(env, "com.defold.iap.IapJNI");
|
||||||
jclass iap_class = (jclass)env->CallObjectMethod(cls, find_class, str_class_name);
|
|
||||||
env->DeleteLocalRef(str_class_name);
|
|
||||||
|
|
||||||
str_class_name = env->NewStringUTF("com.defold.iap.IapJNI");
|
|
||||||
jclass iap_jni_class = (jclass)env->CallObjectMethod(cls, find_class, str_class_name);
|
|
||||||
env->DeleteLocalRef(str_class_name);
|
|
||||||
|
|
||||||
g_IAP.m_List = env->GetMethodID(iap_class, "listItems", "(Ljava/lang/String;Lcom/defold/iap/IListProductsListener;J)V");
|
g_IAP.m_List = env->GetMethodID(iap_class, "listItems", "(Ljava/lang/String;Lcom/defold/iap/IListProductsListener;J)V");
|
||||||
g_IAP.m_Buy = env->GetMethodID(iap_class, "buy", "(Ljava/lang/String;Lcom/defold/iap/IPurchaseListener;)V");
|
g_IAP.m_Buy = env->GetMethodID(iap_class, "buy", "(Ljava/lang/String;Ljava/lang/String;Lcom/defold/iap/IPurchaseListener;)V");
|
||||||
g_IAP.m_Restore = env->GetMethodID(iap_class, "restore", "(Lcom/defold/iap/IPurchaseListener;)V");
|
g_IAP.m_Restore = env->GetMethodID(iap_class, "restore", "(Lcom/defold/iap/IPurchaseListener;)V");
|
||||||
g_IAP.m_Stop = env->GetMethodID(iap_class, "stop", "()V");
|
g_IAP.m_Stop = env->GetMethodID(iap_class, "stop", "()V");
|
||||||
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");
|
||||||
@@ -440,13 +399,11 @@ static dmExtension::Result InitializeIAP(dmExtension::Params* params)
|
|||||||
g_IAP.m_AcknowledgeTransaction = env->GetMethodID(iap_class, "acknowledgeTransaction", "(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, threadAttacher.GetActivity()->clazz, g_IAP.m_autoFinishTransactions));
|
||||||
|
|
||||||
jni_constructor = env->GetMethodID(iap_jni_class, "<init>", "()V");
|
jni_constructor = env->GetMethodID(iap_jni_class, "<init>", "()V");
|
||||||
g_IAP.m_IAPJNI = env->NewGlobalRef(env->NewObject(iap_jni_class, jni_constructor));
|
g_IAP.m_IAPJNI = env->NewGlobalRef(env->NewObject(iap_jni_class, jni_constructor));
|
||||||
|
|
||||||
Detach();
|
|
||||||
|
|
||||||
lua_State*L = params->m_L;
|
lua_State*L = params->m_L;
|
||||||
int top = lua_gettop(L);
|
int top = lua_gettop(L);
|
||||||
luaL_register(L, LIB_NAME, IAP_methods);
|
luaL_register(L, LIB_NAME, IAP_methods);
|
||||||
@@ -494,11 +451,11 @@ static dmExtension::Result FinalizeIAP(dmExtension::Params* params)
|
|||||||
g_IAP.m_Listener = 0;
|
g_IAP.m_Listener = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEnv* env = Attach();
|
dmAndroid::ThreadAttacher threadAttacher;
|
||||||
|
JNIEnv* env = threadAttacher.GetEnv();
|
||||||
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_Stop);
|
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_Stop);
|
||||||
env->DeleteGlobalRef(g_IAP.m_IAP);
|
env->DeleteGlobalRef(g_IAP.m_IAP);
|
||||||
env->DeleteGlobalRef(g_IAP.m_IAPJNI);
|
env->DeleteGlobalRef(g_IAP.m_IAPJNI);
|
||||||
Detach();
|
|
||||||
g_IAP.m_IAP = NULL;
|
g_IAP.m_IAP = NULL;
|
||||||
return dmExtension::RESULT_OK;
|
return dmExtension::RESULT_OK;
|
||||||
}
|
}
|
||||||
|
@@ -46,23 +46,8 @@ static void IAPList_Callback(void* luacallback, const char* result_json)
|
|||||||
|
|
||||||
if(result_json != 0)
|
if(result_json != 0)
|
||||||
{
|
{
|
||||||
dmJson::Document doc;
|
dmScript::JsonToLua(L, result_json, strlen(result_json)); // throws lua error if it fails
|
||||||
dmJson::Result r = dmJson::Parse(result_json, &doc);
|
lua_pushnil(L);
|
||||||
if (r == dmJson::RESULT_OK && doc.m_NodeCount > 0) {
|
|
||||||
char err_str[128];
|
|
||||||
if (dmScript::JsonToLua(L, &doc, 0, err_str, sizeof(err_str)) < 0) {
|
|
||||||
dmLogError("Failed converting list result JSON to Lua; %s", err_str);
|
|
||||||
lua_pushnil(L);
|
|
||||||
IAP_PushError(L, "Failed converting list result JSON to Lua", REASON_UNSPECIFIED);
|
|
||||||
} else {
|
|
||||||
lua_pushnil(L);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dmLogError("Failed to parse list result JSON (%d)", r);
|
|
||||||
lua_pushnil(L);
|
|
||||||
IAP_PushError(L, "Failed to parse list result JSON", REASON_UNSPECIFIED);
|
|
||||||
}
|
|
||||||
dmJson::Free(&doc);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -112,23 +97,8 @@ static void IAPListener_Callback(void* luacallback, const char* result_json, int
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result_json) {
|
if (result_json) {
|
||||||
dmJson::Document doc;
|
dmScript::JsonToLua(L, result_json, strlen(result_json)); // throws lua error if it fails
|
||||||
dmJson::Result r = dmJson::Parse(result_json, &doc);
|
lua_pushnil(L);
|
||||||
if (r == dmJson::RESULT_OK && doc.m_NodeCount > 0) {
|
|
||||||
char err_str[128];
|
|
||||||
if (dmScript::JsonToLua(L, &doc, 0, err_str, sizeof(err_str)) < 0) {
|
|
||||||
dmLogError("Failed converting purchase result JSON to Lua; %s", err_str);
|
|
||||||
lua_pushnil(L);
|
|
||||||
IAP_PushError(L, "failed converting purchase result JSON to Lua", REASON_UNSPECIFIED);
|
|
||||||
} else {
|
|
||||||
lua_pushnil(L);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dmLogError("Failed to parse purchase response (%d)", r);
|
|
||||||
lua_pushnil(L);
|
|
||||||
IAP_PushError(L, "failed to parse purchase response", REASON_UNSPECIFIED);
|
|
||||||
}
|
|
||||||
dmJson::Free(&doc);
|
|
||||||
} else {
|
} else {
|
||||||
lua_pushnil(L);
|
lua_pushnil(L);
|
||||||
switch(error_code)
|
switch(error_code)
|
||||||
|
@@ -593,7 +593,11 @@ static dmExtension::Result UpdateIAP(dmExtension::Params* params)
|
|||||||
|
|
||||||
static dmExtension::Result FinalizeIAP(dmExtension::Params* params)
|
static dmExtension::Result FinalizeIAP(dmExtension::Params* params)
|
||||||
{
|
{
|
||||||
dmScript::DestroyCallback(g_IAP.m_Listener);
|
if (g_IAP.m_Listener)
|
||||||
|
{
|
||||||
|
dmScript::DestroyCallback(g_IAP.m_Listener);
|
||||||
|
}
|
||||||
|
|
||||||
g_IAP.m_Listener = 0;
|
g_IAP.m_Listener = 0;
|
||||||
|
|
||||||
if (g_IAP.m_PendingTransactions) {
|
if (g_IAP.m_PendingTransactions) {
|
||||||
|
@@ -121,18 +121,22 @@ void IAP_Queue_Push(IAPCommandQueue* queue, IAPCommand* cmd)
|
|||||||
void IAP_Queue_Flush(IAPCommandQueue* queue, IAPCommandFn fn, void* ctx)
|
void IAP_Queue_Flush(IAPCommandQueue* queue, IAPCommandFn fn, void* ctx)
|
||||||
{
|
{
|
||||||
assert(fn != 0);
|
assert(fn != 0);
|
||||||
|
|
||||||
if (queue->m_Commands.Empty())
|
if (queue->m_Commands.Empty())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DM_MUTEX_SCOPED_LOCK(queue->m_Mutex);
|
dmArray<IAPCommand> tmp;
|
||||||
|
|
||||||
for(uint32_t i = 0; i != queue->m_Commands.Size(); ++i)
|
|
||||||
{
|
{
|
||||||
fn(&queue->m_Commands[i], ctx);
|
DM_MUTEX_SCOPED_LOCK(queue->m_Mutex);
|
||||||
|
tmp.Swap(queue->m_Commands);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(uint32_t i = 0; i != tmp.Size(); ++i)
|
||||||
|
{
|
||||||
|
fn(&tmp[i], ctx);
|
||||||
}
|
}
|
||||||
queue->m_Commands.SetSize(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // DM_PLATFORM_HTML5 || DM_PLATFORM_ANDROID || DM_PLATFORM_IOS
|
#endif // DM_PLATFORM_HTML5 || DM_PLATFORM_ANDROID || DM_PLATFORM_IOS
|
||||||
|
@@ -7,8 +7,8 @@
|
|||||||
|
|
||||||
enum EIAPCommand
|
enum EIAPCommand
|
||||||
{
|
{
|
||||||
IAP_PRODUCT_RESULT,
|
IAP_PRODUCT_RESULT,
|
||||||
IAP_PURCHASE_RESULT,
|
IAP_PURCHASE_RESULT,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DM_ALIGNED(16) IAPCommand
|
struct DM_ALIGNED(16) IAPCommand
|
||||||
@@ -21,7 +21,7 @@ struct DM_ALIGNED(16) IAPCommand
|
|||||||
// Used for storing eventual callback info (if needed)
|
// Used for storing eventual callback info (if needed)
|
||||||
dmScript::LuaCallbackInfo* m_Callback;
|
dmScript::LuaCallbackInfo* m_Callback;
|
||||||
|
|
||||||
// THe actual command payload
|
// The actual command payload
|
||||||
int32_t m_Command;
|
int32_t m_Command;
|
||||||
int32_t m_ResponseCode;
|
int32_t m_ResponseCode;
|
||||||
void* m_Data;
|
void* m_Data;
|
||||||
|
@@ -11,6 +11,7 @@ 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.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -80,7 +81,7 @@ public class IapAmazon implements PurchasingListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void buy(final String product, final IPurchaseListener listener) {
|
public void buy(final String product, final String token, final IPurchaseListener listener) {
|
||||||
synchronized (purchaseListeners) {
|
synchronized (purchaseListeners) {
|
||||||
RequestId req = PurchasingService.purchase(product);
|
RequestId req = PurchasingService.purchase(product);
|
||||||
if (req != null) {
|
if (req != null) {
|
||||||
@@ -173,9 +174,13 @@ public class IapAmazon implements PurchasingListener {
|
|||||||
if (productDataResponse.getRequestStatus() != ProductDataResponse.RequestStatus.SUCCESSFUL) {
|
if (productDataResponse.getRequestStatus() != ProductDataResponse.RequestStatus.SUCCESSFUL) {
|
||||||
listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null, commadPtr);
|
listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null, commadPtr);
|
||||||
} else {
|
} else {
|
||||||
|
for (final String s : productDataResponse.getUnavailableSkus()) {
|
||||||
|
Log.v(TAG, "Unavailable SKU: " + s);
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, Product> products = productDataResponse.getProductData();
|
Map<String, Product> products = productDataResponse.getProductData();
|
||||||
try {
|
try {
|
||||||
JSONObject data = new JSONObject();
|
JSONArray data = new JSONArray();
|
||||||
for (Map.Entry<String, Product> entry : products.entrySet()) {
|
for (Map.Entry<String, Product> entry : products.entrySet()) {
|
||||||
String key = entry.getKey();
|
String key = entry.getKey();
|
||||||
Product product = entry.getValue();
|
Product product = entry.getValue();
|
||||||
@@ -189,7 +194,7 @@ public class IapAmazon implements PurchasingListener {
|
|||||||
// Based on return values from getPrice: https://developer.amazon.com/public/binaries/content/assets/javadoc/in-app-purchasing-api/com/amazon/inapp/purchasing/item.html
|
// Based on return values from getPrice: https://developer.amazon.com/public/binaries/content/assets/javadoc/in-app-purchasing-api/com/amazon/inapp/purchasing/item.html
|
||||||
item.put("price", priceString.replaceAll("[^0-9.,]", ""));
|
item.put("price", priceString.replaceAll("[^0-9.,]", ""));
|
||||||
}
|
}
|
||||||
data.put(key, item);
|
data.put(item);
|
||||||
}
|
}
|
||||||
listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_OK, data.toString(), commadPtr);
|
listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_OK, data.toString(), commadPtr);
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
|
@@ -19,26 +19,35 @@ import android.util.Log;
|
|||||||
|
|
||||||
import com.android.billingclient.api.BillingClient;
|
import com.android.billingclient.api.BillingClient;
|
||||||
import com.android.billingclient.api.BillingClient.BillingResponseCode;
|
import com.android.billingclient.api.BillingClient.BillingResponseCode;
|
||||||
import com.android.billingclient.api.BillingClient.SkuType;
|
import com.android.billingclient.api.BillingClient.ProductType;
|
||||||
import com.android.billingclient.api.BillingResult;
|
import com.android.billingclient.api.BillingResult;
|
||||||
|
import com.android.billingclient.api.PendingPurchasesParams;
|
||||||
import com.android.billingclient.api.Purchase;
|
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.Purchase.PurchaseState;
|
||||||
import com.android.billingclient.api.SkuDetails;
|
import com.android.billingclient.api.ProductDetails;
|
||||||
|
import com.android.billingclient.api.ProductDetails.OneTimePurchaseOfferDetails;
|
||||||
|
import com.android.billingclient.api.ProductDetails.PricingPhases;
|
||||||
|
import com.android.billingclient.api.ProductDetails.PricingPhase;
|
||||||
|
import com.android.billingclient.api.ProductDetails.RecurrenceMode;
|
||||||
|
import com.android.billingclient.api.ProductDetails.SubscriptionOfferDetails;
|
||||||
import com.android.billingclient.api.ConsumeParams;
|
import com.android.billingclient.api.ConsumeParams;
|
||||||
import com.android.billingclient.api.BillingFlowParams;
|
import com.android.billingclient.api.BillingFlowParams;
|
||||||
import com.android.billingclient.api.SkuDetailsParams;
|
import com.android.billingclient.api.BillingFlowParams.ProductDetailsParams;
|
||||||
|
import com.android.billingclient.api.QueryPurchasesParams;
|
||||||
|
import com.android.billingclient.api.QueryProductDetailsParams;
|
||||||
|
import com.android.billingclient.api.QueryProductDetailsParams.Product;
|
||||||
import com.android.billingclient.api.AcknowledgePurchaseParams;
|
import com.android.billingclient.api.AcknowledgePurchaseParams;
|
||||||
import com.android.billingclient.api.PurchasesUpdatedListener;
|
import com.android.billingclient.api.PurchasesUpdatedListener;
|
||||||
import com.android.billingclient.api.BillingClientStateListener;
|
import com.android.billingclient.api.BillingClientStateListener;
|
||||||
import com.android.billingclient.api.ConsumeResponseListener;
|
import com.android.billingclient.api.ConsumeResponseListener;
|
||||||
import com.android.billingclient.api.SkuDetailsResponseListener;
|
import com.android.billingclient.api.PurchasesResponseListener;
|
||||||
|
import com.android.billingclient.api.ProductDetailsResponseListener;
|
||||||
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
|
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
|
||||||
|
|
||||||
public class IapGooglePlay implements PurchasesUpdatedListener {
|
public class IapGooglePlay implements PurchasesUpdatedListener {
|
||||||
public static final String TAG = "IapGooglePlay";
|
public static final String TAG = "IapGooglePlay";
|
||||||
|
|
||||||
private Map<String, SkuDetails> products = new HashMap<String, SkuDetails>();
|
private Map<String, ProductDetails> products = new HashMap<String, ProductDetails>();
|
||||||
private BillingClient billingClient;
|
private BillingClient billingClient;
|
||||||
private IPurchaseListener purchaseListener;
|
private IPurchaseListener purchaseListener;
|
||||||
private boolean autoFinishTransactions;
|
private boolean autoFinishTransactions;
|
||||||
@@ -48,7 +57,8 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
|
|||||||
this.activity = activity;
|
this.activity = activity;
|
||||||
this.autoFinishTransactions = autoFinishTransactions;
|
this.autoFinishTransactions = autoFinishTransactions;
|
||||||
|
|
||||||
billingClient = BillingClient.newBuilder(activity).setListener(this).enablePendingPurchases().build();
|
PendingPurchasesParams pendingPurchasesParams = PendingPurchasesParams.newBuilder().enableOneTimeProducts().build();
|
||||||
|
billingClient = BillingClient.newBuilder(activity).setListener(this).enablePendingPurchases(pendingPurchasesParams).build();
|
||||||
billingClient.startConnection(new BillingClientStateListener() {
|
billingClient.startConnection(new BillingClientStateListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onBillingSetupFinished(BillingResult billingResult) {
|
public void onBillingSetupFinished(BillingResult billingResult) {
|
||||||
@@ -102,6 +112,7 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
|
|||||||
p.put("date", toISO8601(new Date(purchase.getPurchaseTime())));
|
p.put("date", toISO8601(new Date(purchase.getPurchaseTime())));
|
||||||
p.put("receipt", purchase.getPurchaseToken());
|
p.put("receipt", purchase.getPurchaseToken());
|
||||||
p.put("signature", purchase.getSignature());
|
p.put("signature", purchase.getSignature());
|
||||||
|
p.put("original_json", purchase.getOriginalJson());
|
||||||
}
|
}
|
||||||
catch (JSONException e) {
|
catch (JSONException e) {
|
||||||
Log.wtf(TAG, "Failed to convert purchase", e);
|
Log.wtf(TAG, "Failed to convert purchase", e);
|
||||||
@@ -109,16 +120,67 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
|
|||||||
return p.toString();
|
return p.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private JSONObject convertSkuDetails(SkuDetails skuDetails) {
|
private JSONArray convertSubscriptionOfferPricingPhases(SubscriptionOfferDetails details) {
|
||||||
JSONObject p = new JSONObject();
|
JSONArray a = new JSONArray();
|
||||||
try {
|
try {
|
||||||
p.put("price_string", skuDetails.getPrice());
|
List<PricingPhase> pricingPhases = details.getPricingPhases().getPricingPhaseList();
|
||||||
p.put("ident", skuDetails.getSku());
|
for (PricingPhase pricingPhase : pricingPhases) {
|
||||||
p.put("currency_code", skuDetails.getPriceCurrencyCode());
|
JSONObject o = new JSONObject();
|
||||||
p.put("price", skuDetails.getPriceAmountMicros() * 0.000001);
|
o.put("price_string", pricingPhase.getFormattedPrice());
|
||||||
|
o.put("price", pricingPhase.getPriceAmountMicros() * 0.000001);
|
||||||
|
o.put("currency_code", pricingPhase.getPriceCurrencyCode());
|
||||||
|
o.put("billing_period", pricingPhase.getBillingPeriod());
|
||||||
|
o.put("billing_cycle_count", pricingPhase.getBillingCycleCount());
|
||||||
|
switch (pricingPhase.getRecurrenceMode()) {
|
||||||
|
case RecurrenceMode.FINITE_RECURRING:
|
||||||
|
o.put("recurrence_mode", "FINITE");
|
||||||
|
break;
|
||||||
|
case RecurrenceMode.INFINITE_RECURRING:
|
||||||
|
o.put("recurrence_mode", "INFINITE");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
case RecurrenceMode.NON_RECURRING:
|
||||||
|
o.put("recurrence_mode", "NONE");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
a.put(o);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch(JSONException e) {
|
catch(JSONException e) {
|
||||||
Log.wtf(TAG, "Failed to convert sku details", e);
|
Log.wtf(TAG, "Failed to convert subscription offer pricing phases", e);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject convertProductDetails(ProductDetails productDetails) {
|
||||||
|
JSONObject p = new JSONObject();
|
||||||
|
try {
|
||||||
|
p.put("ident", productDetails.getProductId());
|
||||||
|
p.put("title", productDetails.getTitle());
|
||||||
|
p.put("description", productDetails.getDescription());
|
||||||
|
if (productDetails.getProductType().equals(ProductType.INAPP)) {
|
||||||
|
OneTimePurchaseOfferDetails offerDetails = productDetails.getOneTimePurchaseOfferDetails();
|
||||||
|
p.put("price_string", offerDetails.getFormattedPrice());
|
||||||
|
p.put("currency_code", offerDetails.getPriceCurrencyCode());
|
||||||
|
p.put("price", offerDetails.getPriceAmountMicros() * 0.000001);
|
||||||
|
}
|
||||||
|
else if (productDetails.getProductType().equals(ProductType.SUBS)) {
|
||||||
|
List<SubscriptionOfferDetails> subscriptionOfferDetails = productDetails.getSubscriptionOfferDetails();
|
||||||
|
JSONArray a = new JSONArray();
|
||||||
|
for (SubscriptionOfferDetails offerDetails : subscriptionOfferDetails) {
|
||||||
|
JSONObject o = new JSONObject();
|
||||||
|
o.put("token", offerDetails.getOfferToken());
|
||||||
|
o.put("pricing", convertSubscriptionOfferPricingPhases(offerDetails));
|
||||||
|
a.put(o);
|
||||||
|
}
|
||||||
|
p.put("subscriptions", a);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log.i(TAG, "convertProductDetails() unknown product type " + productDetails.getProductType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(JSONException e) {
|
||||||
|
Log.wtf(TAG, "Failed to convert product details", e);
|
||||||
}
|
}
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
@@ -161,7 +223,6 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
|
|||||||
case BillingResponseCode.OK:
|
case BillingResponseCode.OK:
|
||||||
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_OK;
|
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_OK;
|
||||||
break;
|
break;
|
||||||
case BillingResponseCode.SERVICE_TIMEOUT:
|
|
||||||
case BillingResponseCode.SERVICE_UNAVAILABLE:
|
case BillingResponseCode.SERVICE_UNAVAILABLE:
|
||||||
case BillingResponseCode.SERVICE_DISCONNECTED:
|
case BillingResponseCode.SERVICE_DISCONNECTED:
|
||||||
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_SERVICE_UNAVAILABLE;
|
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_SERVICE_UNAVAILABLE;
|
||||||
@@ -169,6 +230,9 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
|
|||||||
case BillingResponseCode.USER_CANCELED:
|
case BillingResponseCode.USER_CANCELED:
|
||||||
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_USER_CANCELED;
|
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_USER_CANCELED;
|
||||||
break;
|
break;
|
||||||
|
case BillingResponseCode.NETWORK_ERROR: // new in Play Billing Library 6.0.0
|
||||||
|
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_NETWORK_ERROR;
|
||||||
|
break;
|
||||||
case BillingResponseCode.FEATURE_NOT_SUPPORTED:
|
case BillingResponseCode.FEATURE_NOT_SUPPORTED:
|
||||||
case BillingResponseCode.ERROR:
|
case BillingResponseCode.ERROR:
|
||||||
default:
|
default:
|
||||||
@@ -183,19 +247,18 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
|
|||||||
return billingResponseCodeToDefoldResponse(result.getResponseCode());
|
return billingResponseCodeToDefoldResponse(result.getResponseCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void invokeOnPurchaseResultListener(IPurchaseListener purchaseListener, int billingResultCode, String purchaseData) {
|
||||||
* Query Google Play for purchases done within the app.
|
if (purchaseListener == null) {
|
||||||
*/
|
Log.w(TAG, "Received billing result but no listener has been set");
|
||||||
private List<Purchase> queryPurchases(final String type) {
|
return;
|
||||||
PurchasesResult result = billingClient.queryPurchases(type);
|
|
||||||
List<Purchase> purchases = result.getPurchasesList();
|
|
||||||
if (purchases == null) {
|
|
||||||
purchases = new ArrayList<Purchase>();
|
|
||||||
}
|
}
|
||||||
if (result.getBillingResult().getResponseCode() != BillingResponseCode.OK) {
|
purchaseListener.onPurchaseResult(billingResultCode, purchaseData);
|
||||||
Log.e(TAG, "Unable to query pending purchases: " + result.getBillingResult().getDebugMessage());
|
}
|
||||||
}
|
private void invokeOnPurchaseResultListener(IPurchaseListener purchaseListener, BillingResult billingResult, Purchase purchase) {
|
||||||
return purchases;
|
invokeOnPurchaseResultListener(purchaseListener, billingResultToDefoldResponse(billingResult), convertPurchase(purchase));
|
||||||
|
}
|
||||||
|
private void invokeOnPurchaseResultListener(IPurchaseListener purchaseListener, BillingResult billingResult) {
|
||||||
|
invokeOnPurchaseResultListener(purchaseListener, billingResultToDefoldResponse(billingResult), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -206,12 +269,34 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
|
|||||||
*/
|
*/
|
||||||
public void processPendingConsumables(final IPurchaseListener purchaseListener) {
|
public void processPendingConsumables(final IPurchaseListener purchaseListener) {
|
||||||
Log.d(TAG, "processPendingConsumables()");
|
Log.d(TAG, "processPendingConsumables()");
|
||||||
List<Purchase> purchasesList = new ArrayList<Purchase>();
|
|
||||||
purchasesList.addAll(queryPurchases(SkuType.INAPP));
|
|
||||||
purchasesList.addAll(queryPurchases(SkuType.SUBS));
|
PurchasesResponseListener purchasesListener = new PurchasesResponseListener() {
|
||||||
for (Purchase purchase : purchasesList) {
|
private List<Purchase> allPurchases = new ArrayList<Purchase>();
|
||||||
handlePurchase(purchase, purchaseListener);
|
private int queries = 2;
|
||||||
}
|
|
||||||
|
@Override
|
||||||
|
public void onQueryPurchasesResponse(BillingResult billingResult, List<Purchase> purchases) {
|
||||||
|
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
|
||||||
|
Log.e(TAG, "Unable to query pending purchases: " + billingResult.getDebugMessage());
|
||||||
|
}
|
||||||
|
if (purchases != null) {
|
||||||
|
allPurchases.addAll(purchases);
|
||||||
|
}
|
||||||
|
// we're finished when we have queried for both in-app and subs
|
||||||
|
queries--;
|
||||||
|
if (queries == 0) {
|
||||||
|
for (Purchase purchase : allPurchases) {
|
||||||
|
handlePurchase(purchase, purchaseListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
final QueryPurchasesParams inappParams = QueryPurchasesParams.newBuilder().setProductType(ProductType.INAPP).build();
|
||||||
|
final QueryPurchasesParams subsParams = QueryPurchasesParams.newBuilder().setProductType(ProductType.SUBS).build();
|
||||||
|
billingClient.queryPurchasesAsync(inappParams, purchasesListener);
|
||||||
|
billingClient.queryPurchasesAsync(subsParams, purchasesListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -220,7 +305,7 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
|
|||||||
*/
|
*/
|
||||||
private void consumePurchase(final String purchaseToken, final ConsumeResponseListener consumeListener) {
|
private void consumePurchase(final String purchaseToken, final ConsumeResponseListener consumeListener) {
|
||||||
Log.d(TAG, "consumePurchase() " + purchaseToken);
|
Log.d(TAG, "consumePurchase() " + purchaseToken);
|
||||||
ConsumeParams consumeParams = ConsumeParams.newBuilder()
|
final ConsumeParams consumeParams = ConsumeParams.newBuilder()
|
||||||
.setPurchaseToken(purchaseToken)
|
.setPurchaseToken(purchaseToken)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -239,7 +324,7 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
|
|||||||
// note: we only call the purchase listener if an error happens
|
// note: we only call the purchase listener if an error happens
|
||||||
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
|
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
|
||||||
Log.e(TAG, "Unable to consume purchase: " + billingResult.getDebugMessage());
|
Log.e(TAG, "Unable to consume purchase: " + billingResult.getDebugMessage());
|
||||||
purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
|
invokeOnPurchaseResultListener(purchaseListener, billingResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -262,7 +347,7 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
|
|||||||
// note: we only call the purchase listener if an error happens
|
// note: we only call the purchase listener if an error happens
|
||||||
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
|
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
|
||||||
Log.e(TAG, "Unable to acknowledge purchase: " + billingResult.getDebugMessage());
|
Log.e(TAG, "Unable to acknowledge purchase: " + billingResult.getDebugMessage());
|
||||||
purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
|
invokeOnPurchaseResultListener(purchaseListener, billingResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -272,7 +357,7 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
|
|||||||
* Handle a purchase. If the extension is configured to automatically
|
* Handle a purchase. If the extension is configured to automatically
|
||||||
* finish transactions the purchase will be immediately consumed. Otherwise
|
* finish transactions the purchase will be immediately consumed. Otherwise
|
||||||
* the product will be returned via the listener without being consumed.
|
* the product will be returned via the listener without being consumed.
|
||||||
* NOTE: Billing 3.0 requires purchases to be acknowledged within 3 days of
|
* NOTE: Billing 3.0+ requires purchases to be acknowledged within 3 days of
|
||||||
* purchase unless they are consumed.
|
* purchase unless they are consumed.
|
||||||
*/
|
*/
|
||||||
private void handlePurchase(final Purchase purchase, final IPurchaseListener purchaseListener) {
|
private void handlePurchase(final Purchase purchase, final IPurchaseListener purchaseListener) {
|
||||||
@@ -281,12 +366,12 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
|
|||||||
@Override
|
@Override
|
||||||
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
|
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
|
||||||
Log.d(TAG, "handlePurchase() response code " + billingResult.getResponseCode() + " purchaseToken: " + purchaseToken);
|
Log.d(TAG, "handlePurchase() response code " + billingResult.getResponseCode() + " purchaseToken: " + purchaseToken);
|
||||||
purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), convertPurchase(purchase));
|
invokeOnPurchaseResultListener(purchaseListener, billingResult, purchase);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
purchaseListener.onPurchaseResult(billingResponseCodeToDefoldResponse(BillingResponseCode.OK), convertPurchase(purchase));
|
invokeOnPurchaseResultListener(purchaseListener, billingResponseCodeToDefoldResponse(BillingResponseCode.OK), convertPurchase(purchase));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,13 +380,17 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
|
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
|
||||||
if (billingResult.getResponseCode() == BillingResponseCode.OK && purchases != null) {
|
if (billingResult.getResponseCode() == BillingResponseCode.OK) {
|
||||||
for (Purchase purchase : purchases) {
|
if (purchases != null && !purchases.isEmpty()) {
|
||||||
handlePurchase(purchase, this.purchaseListener);
|
for (Purchase purchase : purchases) {
|
||||||
|
if (purchase != null) {
|
||||||
|
handlePurchase(purchase, this.purchaseListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
|
invokeOnPurchaseResultListener(this.purchaseListener, billingResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,42 +398,53 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
|
|||||||
* Buy a product. This method stores the listener and uses it in the
|
* Buy a product. This method stores the listener and uses it in the
|
||||||
* onPurchasesUpdated() callback.
|
* onPurchasesUpdated() callback.
|
||||||
*/
|
*/
|
||||||
private void buyProduct(SkuDetails sku, final IPurchaseListener purchaseListener) {
|
private void buyProduct(ProductDetails pd, final String token, final IPurchaseListener purchaseListener) {
|
||||||
this.purchaseListener = purchaseListener;
|
this.purchaseListener = purchaseListener;
|
||||||
|
|
||||||
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
|
List<ProductDetailsParams> productDetailsParams = new ArrayList();
|
||||||
.setSkuDetails(sku)
|
if (pd.getProductType().equals(ProductType.SUBS)) {
|
||||||
.build();
|
productDetailsParams.add(ProductDetailsParams.newBuilder().setProductDetails(pd).setOfferToken(token).build());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
productDetailsParams.add(ProductDetailsParams.newBuilder().setProductDetails(pd).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
final BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder().setProductDetailsParamsList(productDetailsParams).build();
|
||||||
|
|
||||||
BillingResult billingResult = billingClient.launchBillingFlow(this.activity, billingFlowParams);
|
BillingResult billingResult = billingClient.launchBillingFlow(this.activity, billingFlowParams);
|
||||||
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
|
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
|
||||||
Log.e(TAG, "Purchase failed: " + billingResult.getDebugMessage());
|
Log.e(TAG, "Purchase failed: " + billingResult.getDebugMessage());
|
||||||
purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
|
invokeOnPurchaseResultListener(purchaseListener, billingResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called from Lua.
|
* Called from Lua.
|
||||||
*/
|
*/
|
||||||
public void buy(final String product, final IPurchaseListener purchaseListener) {
|
public void buy(final String product, final String token, final IPurchaseListener purchaseListener) {
|
||||||
Log.d(TAG, "buy()");
|
Log.d(TAG, "buy()");
|
||||||
|
|
||||||
SkuDetails sku = this.products.get(product);
|
ProductDetails pd = this.products.get(product);
|
||||||
if (sku != null) {
|
if (pd != null) {
|
||||||
buyProduct(sku, purchaseListener);
|
buyProduct(pd, token, purchaseListener);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
List<String> skuList = new ArrayList<String>();
|
List<String> productList = new ArrayList<String>();
|
||||||
skuList.add(product);
|
productList.add(product);
|
||||||
querySkuDetailsAsync(skuList, new SkuDetailsResponseListener() {
|
queryProductDetailsAsync(productList, new ProductDetailsResponseListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
|
public void onProductDetailsResponse(BillingResult billingResult, List<ProductDetails> productDetailsList) {
|
||||||
if (billingResult.getResponseCode() == BillingResponseCode.OK && (skuDetailsList != null) && !skuDetailsList.isEmpty()) {
|
if (billingResult.getResponseCode() == BillingResponseCode.OK && (productDetailsList != null) && !productDetailsList.isEmpty()) {
|
||||||
buyProduct(skuDetailsList.get(0), purchaseListener);
|
for (ProductDetails productDetails : productDetailsList) {
|
||||||
|
if (productDetails != null) {
|
||||||
|
buyProduct(productDetails, token, purchaseListener);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Log.e(TAG, "Unable to get product details before buying: " + billingResult.getDebugMessage());
|
Log.e(TAG, "Unable to get product details before buying: " + billingResult.getDebugMessage());
|
||||||
purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
|
invokeOnPurchaseResultListener(purchaseListener, billingResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -355,30 +455,45 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
|
|||||||
* Get details for a list of products. The products can be a mix of
|
* Get details for a list of products. The products can be a mix of
|
||||||
* in-app products and subscriptions.
|
* in-app products and subscriptions.
|
||||||
*/
|
*/
|
||||||
private void querySkuDetailsAsync(final List<String> skuList, final SkuDetailsResponseListener listener) {
|
private void queryProductDetailsAsync(final List<String> productList, final ProductDetailsResponseListener listener) {
|
||||||
SkuDetailsResponseListener detailsListener = new SkuDetailsResponseListener() {
|
ProductDetailsResponseListener detailsListener = new ProductDetailsResponseListener() {
|
||||||
private List<SkuDetails> allSkuDetails = new ArrayList<SkuDetails>();
|
private List<ProductDetails> allProductDetails = new ArrayList<ProductDetails>();
|
||||||
private int queries = 2;
|
private int queries = 2;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetails) {
|
public void onProductDetailsResponse(BillingResult billingResult, List<ProductDetails> productDetails) {
|
||||||
if (skuDetails != null) {
|
if (productDetails != null && !productDetails.isEmpty()) {
|
||||||
// cache skus (cache will be used to speed up buying)
|
// cache products (cache will be used to speed up buying)
|
||||||
for (SkuDetails sd : skuDetails) {
|
for (ProductDetails pd : productDetails) {
|
||||||
IapGooglePlay.this.products.put(sd.getSku(), sd);
|
if (pd != null) {
|
||||||
|
IapGooglePlay.this.products.put(pd.getProductId(), pd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// add to list of all sku details
|
// add to list of all product details
|
||||||
allSkuDetails.addAll(skuDetails);
|
allProductDetails.addAll(productDetails);
|
||||||
}
|
}
|
||||||
// we're finished when we have queried for both in-app and subs
|
// we're finished when we have queried for both in-app and subs
|
||||||
queries--;
|
queries--;
|
||||||
if (queries == 0) {
|
if (queries == 0) {
|
||||||
listener.onSkuDetailsResponse(billingResult, allSkuDetails);
|
listener.onProductDetailsResponse(billingResult, allProductDetails);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
billingClient.querySkuDetailsAsync(SkuDetailsParams.newBuilder().setSkusList(skuList).setType(SkuType.INAPP).build(), detailsListener);
|
|
||||||
billingClient.querySkuDetailsAsync(SkuDetailsParams.newBuilder().setSkusList(skuList).setType(SkuType.SUBS).build(), detailsListener);
|
// we don't know if a product is a subscription or inapp product type
|
||||||
|
// instread we create two product lists from the same set of products and use INAPP for one and SUBS for the other
|
||||||
|
List<Product> inappProductList = new ArrayList();
|
||||||
|
List<Product> subsProductList = new ArrayList();
|
||||||
|
for (String productId : productList) {
|
||||||
|
inappProductList.add(Product.newBuilder().setProductId(productId).setProductType(ProductType.INAPP).build());
|
||||||
|
subsProductList.add(Product.newBuilder().setProductId(productId).setProductType(ProductType.SUBS).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
// do one query per product type
|
||||||
|
final QueryProductDetailsParams inappParams = QueryProductDetailsParams.newBuilder().setProductList(inappProductList).build();
|
||||||
|
final QueryProductDetailsParams subsParams = QueryProductDetailsParams.newBuilder().setProductList(subsProductList).build();
|
||||||
|
billingClient.queryProductDetailsAsync(inappParams, detailsListener);
|
||||||
|
billingClient.queryProductDetailsAsync(subsParams, detailsListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -387,21 +502,23 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
|
|||||||
public void listItems(final String products, final IListProductsListener productsListener, final long commandPtr) {
|
public void listItems(final String products, final IListProductsListener productsListener, final long commandPtr) {
|
||||||
Log.d(TAG, "listItems()");
|
Log.d(TAG, "listItems()");
|
||||||
|
|
||||||
// create list of skus from comma separated string
|
// create list of product ids from comma separated string
|
||||||
List<String> skuList = new ArrayList<String>();
|
List<String> productList = new ArrayList<String>();
|
||||||
for (String p : products.split(",")) {
|
for (String p : products.split(",")) {
|
||||||
if (p.trim().length() > 0) {
|
if (p.trim().length() > 0) {
|
||||||
skuList.add(p);
|
productList.add(p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
querySkuDetailsAsync(skuList, new SkuDetailsResponseListener() {
|
queryProductDetailsAsync(productList, new ProductDetailsResponseListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetails) {
|
public void onProductDetailsResponse(BillingResult billingResult, List<ProductDetails> productDetails) {
|
||||||
JSONArray a = new JSONArray();
|
JSONArray a = new JSONArray();
|
||||||
if (billingResult.getResponseCode() == BillingResponseCode.OK) {
|
if ((billingResult.getResponseCode() == BillingResponseCode.OK) && (productDetails != null) && !productDetails.isEmpty()) {
|
||||||
for (SkuDetails sd : skuDetails) {
|
for (ProductDetails pd : productDetails) {
|
||||||
a.put(convertSkuDetails(sd));
|
if (pd != null) {
|
||||||
|
a.put(convertProductDetails(pd));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@@ -18,6 +18,7 @@ public class IapJNI implements IListProductsListener, IPurchaseListener {
|
|||||||
public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
|
public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
|
||||||
public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
|
public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
|
||||||
public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
|
public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
|
||||||
|
public static final int BILLING_RESPONSE_RESULT_NETWORK_ERROR = 9;
|
||||||
|
|
||||||
public IapJNI() {
|
public IapJNI() {
|
||||||
}
|
}
|
||||||
|
@@ -11,12 +11,12 @@ height = 1136
|
|||||||
[android]
|
[android]
|
||||||
input_method = HiddenInputField
|
input_method = HiddenInputField
|
||||||
package = com.defold.extension.iap
|
package = com.defold.extension.iap
|
||||||
version_code = 7
|
version_code = 9
|
||||||
target_sdk_version = 29
|
minimum_sdk_version = 21
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
title = extension-iap
|
title = extension-iap
|
||||||
dependencies = https://github.com/andsve/dirtylarry/archive/master.zip
|
dependencies#0 = https://github.com/andsve/dirtylarry/archive/master.zip
|
||||||
|
|
||||||
[library]
|
[library]
|
||||||
include_dirs = extension-iap
|
include_dirs = extension-iap
|
||||||
|
100
main/main.gui
100
main/main.gui
@@ -1,7 +1,7 @@
|
|||||||
script: "/main/main.gui_script"
|
script: "/main/main.gui_script"
|
||||||
fonts {
|
fonts {
|
||||||
name: "system_font"
|
name: "default"
|
||||||
font: "/builtins/fonts/system_font.font"
|
font: "/builtins/fonts/default.font"
|
||||||
}
|
}
|
||||||
background_color {
|
background_color {
|
||||||
x: 0.0
|
x: 0.0
|
||||||
@@ -47,6 +47,8 @@ nodes {
|
|||||||
alpha: 1.0
|
alpha: 1.0
|
||||||
template: "/dirtylarry/button.gui"
|
template: "/dirtylarry/button.gui"
|
||||||
template_node_child: false
|
template_node_child: false
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -102,6 +104,10 @@ nodes {
|
|||||||
alpha: 1.0
|
alpha: 1.0
|
||||||
template_node_child: true
|
template_node_child: true
|
||||||
size_mode: SIZE_MODE_MANUAL
|
size_mode: SIZE_MODE_MANUAL
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
|
visible: true
|
||||||
|
material: ""
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -166,6 +172,10 @@ nodes {
|
|||||||
template_node_child: true
|
template_node_child: true
|
||||||
text_leading: 1.0
|
text_leading: 1.0
|
||||||
text_tracking: 0.0
|
text_tracking: 0.0
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
|
visible: true
|
||||||
|
material: ""
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -205,6 +215,8 @@ nodes {
|
|||||||
alpha: 1.0
|
alpha: 1.0
|
||||||
template: "/dirtylarry/button.gui"
|
template: "/dirtylarry/button.gui"
|
||||||
template_node_child: false
|
template_node_child: false
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -261,6 +273,10 @@ nodes {
|
|||||||
overridden_fields: 4
|
overridden_fields: 4
|
||||||
template_node_child: true
|
template_node_child: true
|
||||||
size_mode: SIZE_MODE_MANUAL
|
size_mode: SIZE_MODE_MANUAL
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
|
visible: true
|
||||||
|
material: ""
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -325,6 +341,10 @@ nodes {
|
|||||||
template_node_child: true
|
template_node_child: true
|
||||||
text_leading: 1.0
|
text_leading: 1.0
|
||||||
text_tracking: 0.0
|
text_tracking: 0.0
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
|
visible: true
|
||||||
|
material: ""
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -360,7 +380,7 @@ nodes {
|
|||||||
type: TYPE_TEXT
|
type: TYPE_TEXT
|
||||||
blend_mode: BLEND_MODE_ALPHA
|
blend_mode: BLEND_MODE_ALPHA
|
||||||
text: "<text>"
|
text: "<text>"
|
||||||
font: "system_font"
|
font: "default"
|
||||||
id: "log"
|
id: "log"
|
||||||
xanchor: XANCHOR_NONE
|
xanchor: XANCHOR_NONE
|
||||||
yanchor: YANCHOR_NONE
|
yanchor: YANCHOR_NONE
|
||||||
@@ -387,6 +407,10 @@ nodes {
|
|||||||
template_node_child: false
|
template_node_child: false
|
||||||
text_leading: 1.0
|
text_leading: 1.0
|
||||||
text_tracking: 0.0
|
text_tracking: 0.0
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
|
visible: true
|
||||||
|
material: ""
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -426,6 +450,8 @@ nodes {
|
|||||||
alpha: 1.0
|
alpha: 1.0
|
||||||
template: "/dirtylarry/button.gui"
|
template: "/dirtylarry/button.gui"
|
||||||
template_node_child: false
|
template_node_child: false
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -481,6 +507,10 @@ nodes {
|
|||||||
alpha: 1.0
|
alpha: 1.0
|
||||||
template_node_child: true
|
template_node_child: true
|
||||||
size_mode: SIZE_MODE_MANUAL
|
size_mode: SIZE_MODE_MANUAL
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
|
visible: true
|
||||||
|
material: ""
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -545,6 +575,10 @@ nodes {
|
|||||||
template_node_child: true
|
template_node_child: true
|
||||||
text_leading: 1.0
|
text_leading: 1.0
|
||||||
text_tracking: 0.0
|
text_tracking: 0.0
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
|
visible: true
|
||||||
|
material: ""
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -584,6 +618,8 @@ nodes {
|
|||||||
alpha: 1.0
|
alpha: 1.0
|
||||||
template: "/dirtylarry/button.gui"
|
template: "/dirtylarry/button.gui"
|
||||||
template_node_child: false
|
template_node_child: false
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -639,6 +675,10 @@ nodes {
|
|||||||
alpha: 1.0
|
alpha: 1.0
|
||||||
template_node_child: true
|
template_node_child: true
|
||||||
size_mode: SIZE_MODE_MANUAL
|
size_mode: SIZE_MODE_MANUAL
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
|
visible: true
|
||||||
|
material: ""
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -703,6 +743,10 @@ nodes {
|
|||||||
template_node_child: true
|
template_node_child: true
|
||||||
text_leading: 1.0
|
text_leading: 1.0
|
||||||
text_tracking: 0.0
|
text_tracking: 0.0
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
|
visible: true
|
||||||
|
material: ""
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -742,6 +786,8 @@ nodes {
|
|||||||
alpha: 1.0
|
alpha: 1.0
|
||||||
template: "/dirtylarry/button.gui"
|
template: "/dirtylarry/button.gui"
|
||||||
template_node_child: false
|
template_node_child: false
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -798,6 +844,10 @@ nodes {
|
|||||||
overridden_fields: 4
|
overridden_fields: 4
|
||||||
template_node_child: true
|
template_node_child: true
|
||||||
size_mode: SIZE_MODE_MANUAL
|
size_mode: SIZE_MODE_MANUAL
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
|
visible: true
|
||||||
|
material: ""
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -862,6 +912,10 @@ nodes {
|
|||||||
template_node_child: true
|
template_node_child: true
|
||||||
text_leading: 1.0
|
text_leading: 1.0
|
||||||
text_tracking: 0.0
|
text_tracking: 0.0
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
|
visible: true
|
||||||
|
material: ""
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -901,6 +955,8 @@ nodes {
|
|||||||
alpha: 1.0
|
alpha: 1.0
|
||||||
template: "/dirtylarry/button.gui"
|
template: "/dirtylarry/button.gui"
|
||||||
template_node_child: false
|
template_node_child: false
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -956,6 +1012,10 @@ nodes {
|
|||||||
alpha: 1.0
|
alpha: 1.0
|
||||||
template_node_child: true
|
template_node_child: true
|
||||||
size_mode: SIZE_MODE_MANUAL
|
size_mode: SIZE_MODE_MANUAL
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
|
visible: true
|
||||||
|
material: ""
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -1020,6 +1080,10 @@ nodes {
|
|||||||
template_node_child: true
|
template_node_child: true
|
||||||
text_leading: 1.0
|
text_leading: 1.0
|
||||||
text_tracking: 0.0
|
text_tracking: 0.0
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
|
visible: true
|
||||||
|
material: ""
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -1059,6 +1123,8 @@ nodes {
|
|||||||
alpha: 1.0
|
alpha: 1.0
|
||||||
template: "/dirtylarry/button.gui"
|
template: "/dirtylarry/button.gui"
|
||||||
template_node_child: false
|
template_node_child: false
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -1115,6 +1181,10 @@ nodes {
|
|||||||
overridden_fields: 4
|
overridden_fields: 4
|
||||||
template_node_child: true
|
template_node_child: true
|
||||||
size_mode: SIZE_MODE_MANUAL
|
size_mode: SIZE_MODE_MANUAL
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
|
visible: true
|
||||||
|
material: ""
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -1179,6 +1249,10 @@ nodes {
|
|||||||
template_node_child: true
|
template_node_child: true
|
||||||
text_leading: 1.0
|
text_leading: 1.0
|
||||||
text_tracking: 0.0
|
text_tracking: 0.0
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
|
visible: true
|
||||||
|
material: ""
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -1218,6 +1292,8 @@ nodes {
|
|||||||
alpha: 1.0
|
alpha: 1.0
|
||||||
template: "/dirtylarry/checkbox_label.gui"
|
template: "/dirtylarry/checkbox_label.gui"
|
||||||
template_node_child: false
|
template_node_child: false
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -1273,6 +1349,10 @@ nodes {
|
|||||||
alpha: 1.0
|
alpha: 1.0
|
||||||
template_node_child: true
|
template_node_child: true
|
||||||
size_mode: SIZE_MODE_MANUAL
|
size_mode: SIZE_MODE_MANUAL
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
|
visible: true
|
||||||
|
material: ""
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -1337,6 +1417,10 @@ nodes {
|
|||||||
template_node_child: true
|
template_node_child: true
|
||||||
text_leading: 1.0
|
text_leading: 1.0
|
||||||
text_tracking: 0.0
|
text_tracking: 0.0
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
|
visible: true
|
||||||
|
material: ""
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -1376,6 +1460,8 @@ nodes {
|
|||||||
alpha: 1.0
|
alpha: 1.0
|
||||||
template: "/dirtylarry/checkbox_label.gui"
|
template: "/dirtylarry/checkbox_label.gui"
|
||||||
template_node_child: false
|
template_node_child: false
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -1431,6 +1517,10 @@ nodes {
|
|||||||
alpha: 1.0
|
alpha: 1.0
|
||||||
template_node_child: true
|
template_node_child: true
|
||||||
size_mode: SIZE_MODE_MANUAL
|
size_mode: SIZE_MODE_MANUAL
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
|
visible: true
|
||||||
|
material: ""
|
||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
position {
|
position {
|
||||||
@@ -1495,6 +1585,10 @@ nodes {
|
|||||||
template_node_child: true
|
template_node_child: true
|
||||||
text_leading: 1.0
|
text_leading: 1.0
|
||||||
text_tracking: 0.0
|
text_tracking: 0.0
|
||||||
|
custom_type: 0
|
||||||
|
enabled: true
|
||||||
|
visible: true
|
||||||
|
material: ""
|
||||||
}
|
}
|
||||||
material: "/builtins/materials/gui.material"
|
material: "/builtins/materials/gui.material"
|
||||||
adjust_reference: ADJUST_REFERENCE_PARENT
|
adjust_reference: ADJUST_REFERENCE_PARENT
|
||||||
|
@@ -3,7 +3,7 @@ local dirtylarry = require "dirtylarry/dirtylarry"
|
|||||||
local GOLDBARS_SMALL = "com.defold.iap.goldbar.small"
|
local GOLDBARS_SMALL = "com.defold.iap.goldbar.small"
|
||||||
local GOLDBARS_MEDIUM = "com.defold.iap.goldbar.medium"
|
local GOLDBARS_MEDIUM = "com.defold.iap.goldbar.medium"
|
||||||
local GOLDBARS_LARGE = "com.defold.iap.goldbar.large"
|
local GOLDBARS_LARGE = "com.defold.iap.goldbar.large"
|
||||||
local SUBSCRIPTION = "com.defold.iap.subscription"
|
local SUBSCRIPTION = "com.defold.iap.subscription.one"
|
||||||
local NON_CONSUMABLE = "com.defold.iap.removeads"
|
local NON_CONSUMABLE = "com.defold.iap.removeads"
|
||||||
|
|
||||||
local items = {
|
local items = {
|
||||||
@@ -44,7 +44,13 @@ end
|
|||||||
|
|
||||||
local function buy(id)
|
local function buy(id)
|
||||||
log("iap.buy() " .. id)
|
log("iap.buy() " .. id)
|
||||||
iap.buy(id)
|
local options = {}
|
||||||
|
local item = available_items[id]
|
||||||
|
if item.subscriptions then
|
||||||
|
local subscription = item.subscriptions[1]
|
||||||
|
options.token = subscription.token
|
||||||
|
end
|
||||||
|
iap.buy(id, options)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function restore()
|
local function restore()
|
||||||
@@ -66,6 +72,7 @@ local function list()
|
|||||||
for k,p in pairs(products) do
|
for k,p in pairs(products) do
|
||||||
available_items[p.ident] = p
|
available_items[p.ident] = p
|
||||||
log("Item %s", p.ident)
|
log("Item %s", p.ident)
|
||||||
|
pprint(p)
|
||||||
local button = item_buttons[p.ident]
|
local button = item_buttons[p.ident]
|
||||||
if button then
|
if button then
|
||||||
gui.set_color(gui.get_node(button.."/larrylabel"), vmath.vector4(1,1,1,1))
|
gui.set_color(gui.get_node(button.."/larrylabel"), vmath.vector4(1,1,1,1))
|
||||||
|
Reference in New Issue
Block a user