41 Commits
2.0.1 ... 4.0.0

Author SHA1 Message Date
Björn Ritzl
40648f4773 Merge pull request #52 from defold/Issue-51-update-billing-library-to-version-5
Updated the Android billing library to version 5
2022-09-08 10:32:27 +02:00
Björn Ritzl
3d3433e07f Update iap.script_api 2022-09-08 10:32:04 +02:00
Björn Ritzl
52493d8c83 Improved API docs. Recurrence mode 2022-09-08 10:06:10 +02:00
Björn Ritzl
c09aa64ab5 Added title and description 2022-09-08 09:51:39 +02:00
Björn Ritzl
d0ca1e05fd Update iap.script_api 2022-09-08 09:51:28 +02:00
Björn Ritzl
05e9001404 Fixed issues with subscriptions 2022-09-07 11:58:19 +02:00
Björn Ritzl
a26bb45791 Initial update 2022-09-06 23:59:27 +02:00
Björn Ritzl
0a10ff0878 Merge pull request #49 from dapetcu21/master
Fix bundling for Android 12 SDK
2022-05-02 15:03:34 +02:00
Marius Petcu
508a4f8681 Keep a single property indentation style 2022-05-02 15:30:38 +03:00
Marius Petcu
5e1738d353 Fix bundling on Android 12
Bundling fails because the `<receiver>` tag needs `android:exported="true"`.
2022-05-02 15:28:44 +03:00
Alexey Gulev
df7f05c1a6 Merge pull request #48 from defold/fix/mutex-scope
Smaller mutex scope + use new SDK android methods (Defold 1.2.188)
2021-10-31 22:01:05 +01:00
Alexey Gulev
3ecd6ef587 Smaller mutex scope + use new SDK android methods (Defold 1.2.188) 2021-10-31 13:22:18 +01:00
JCash
92d90bb4de Fixed nul pointer deferencing 2021-10-06 09:55:49 +02:00
Björn Ritzl
e6e2f2de63 Update iap.script_api 2021-09-09 13:26:08 +02:00
Björn Ritzl
3468b52658 Merge pull request #41 from BigButtonCo/master
Add original_json to transaction for validating purchases on android
2021-09-09 13:23:13 +02:00
Mathias Westerdahl
bda785ef08 Merge pull request #44 from defold/buffer-threading-fix
Fixed threading issue of checking if command buffer is empty
2021-09-07 14:34:35 +02:00
JCash
b3b79006d7 Fixed threading issue of checking if command buffer is empty 2021-08-02 10:10:00 +02:00
Björn Ritzl
8b2c7b0bca Removed old Amazon SDK version 2021-07-22 19:58:18 +02:00
Björn Ritzl
78d1c80c19 Merge pull request #42 from SalavatR/master
Brought stores to a common view,  Amazon iap_lib update
2021-07-22 14:32:30 +02:00
Salavat
beb89d507f Brought stores to a common view
Amazon iap_lib update
2021-07-20 02:32:13 +06:00
BigButtonCo
1a6d2f01e5 Merge branch 'defold:master' into master 2021-06-03 20:05:02 +02:00
Björn Ritzl
1fe29ad24a Autobuilder 2021-05-28 11:25:20 +02:00
Björn Ritzl
112744c6bb Merge pull request #39 from defold/dev-remove-init-count
Do not track init count for multiple Lua states
2021-04-28 10:08:40 +02:00
Björn Ritzl
6e975b67de Merge branch 'master' into dev-remove-init-count 2021-04-28 10:08:12 +02:00
Björn Ritzl
f3ee2361ac Merge pull request #38 from defold/issue-35-crash-on-iap-oncommand
Get the Lua context from the callback and not the listener
2021-04-28 10:06:24 +02:00
Björn Ritzl
db2c7f0edb Merge pull request #40 from defold/issue-37-crash-when-receiving-products
Check that the extension hasn't been rebooted while fetching products
2021-04-28 10:05:58 +02:00
Björn Ritzl
c21a8ea984 Update iap_ios.mm 2021-04-28 09:46:23 +02:00
Björn Ritzl
1c829abbfb Check that the extension hasn't been recreated while fetching products 2021-04-28 08:59:16 +02:00
Björn Ritzl
80ba15bf22 Do not track init count for multiple Lua states
None of our other extensions support this
2021-04-28 08:54:38 +02:00
Björn Ritzl
5843f631af Get the Lua context from the callback and not the listener 2021-04-27 19:52:14 +02:00
BigButtonCo
276437b981 Update IapGooglePlay.java 2021-04-12 19:26:56 +02:00
BigButtonCo
246fcd3179 Added original_json 2021-04-12 19:22:33 +02:00
Björn Ritzl
257f95f1d3 Merge pull request #32 from defold/Issue-32-android-crash-on-restore-buy-finish
Make sure skuDetails exists and isn't empty
2021-01-25 23:20:24 +01:00
Björn Ritzl
1a16fcc795 Make sure skuDetails exists and isn't empty 2021-01-25 10:37:09 +01:00
Björn Ritzl
412a609738 Fixed incorrect script api doc for iap-buy() 2021-01-25 10:36:08 +01:00
Björn Ritzl
401e23562e Merge pull request #30 from defold/fix-for-new-emscripten
Method calls update
2020-12-29 19:43:01 +01:00
Alexey Gulev
0b455dd9da Method calls update 2020-12-29 19:09:32 +01:00
Björn Ritzl
aabfc49c1d Updated docs with iap.acknowledge() 2020-11-14 21:00:18 +01:00
Björn Ritzl
5c806a990d Merge pull request #28 from SkaterDad/Amazon-Fix-NPE
Fix crashes when using Amazon provider
2020-10-20 11:12:15 +02:00
Mike
ca33678486 Amazon: Stub out acknowledgeTransaction 2020-10-17 13:15:17 -05:00
Mike
faf62f2314 Amazon: Initialize listProductsCommandPtrs 2020-10-17 11:42:47 -05:00
15 changed files with 429 additions and 223 deletions

76
.github/workflows/bob.yml vendored Normal file
View File

@@ -0,0 +1,76 @@
name: Build with bob
on:
push:
pull_request_target:
schedule:
# nightly at 05:00 on the 1st and 15th
- cron: 0 5 1,15 * *
env:
VERSION_FILENAME: 'info.json'
BUILD_SERVER: 'https://build.defold.com'
jobs:
build_with_bob:
strategy:
matrix:
platform: [armv7-android, x86_64-linux, x86_64-win32, x86-win32, js-web]
runs-on: ubuntu-latest
name: Build
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: '11.0.2'
- name: Get Defold version
run: |
TMPVAR=`curl -s http://d.defold.com/stable/${{env.VERSION_FILENAME}} | jq -r '.sha1'`
echo "DEFOLD_VERSION=${TMPVAR}" >> $GITHUB_ENV
echo "Found version ${TMPVAR}"
- name: Download bob.jar
run: |
wget -q http://d.defold.com/archive/stable/${{env.DEFOLD_VERSION}}/bob/bob.jar
java -jar bob.jar --version
- name: Resolve libraries
run: java -jar bob.jar resolve --email a@b.com --auth 123456
- name: Build
run: java -jar bob.jar --platform=${{ matrix.platform }} build --archive --build-server=${{env.BUILD_SERVER}}
- name: Bundle
run: java -jar bob.jar --platform=${{ matrix.platform }} bundle
# macOS is not technically needed for building, but we want to test bundling as well, since we're also testing the manifest merging
build_with_bob_macos:
strategy:
matrix:
platform: [armv7-darwin, x86_64-darwin]
runs-on: macOS-latest
name: Build
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: '11.0.2'
- name: Get Defold version
run: |
TMPVAR=`curl -s http://d.defold.com/stable/${{env.VERSION_FILENAME}} | jq -r '.sha1'`
echo "DEFOLD_VERSION=${TMPVAR}" >> $GITHUB_ENV
echo "Found version ${TMPVAR}"
- name: Download bob.jar
run: |
wget -q http://d.defold.com/archive/stable/${{env.DEFOLD_VERSION}}/bob/bob.jar
java -jar bob.jar --version
- name: Resolve libraries
run: java -jar bob.jar resolve --email a@b.com --auth 123456
- name: Build
run: java -jar bob.jar --platform=${{ matrix.platform }} build --archive --build-server=${{env.BUILD_SERVER}}
- name: Bundle
run: java -jar bob.jar --platform=${{ matrix.platform }} bundle

View File

@@ -1,3 +1,5 @@
[![Actions Status Alpha](https://github.com/defold/extension-iap/actions/workflows/bob.yml/badge.svg)](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.

View File

@@ -173,17 +173,20 @@ Most payment providers only supports synchronous payments. This means that the c
## Asynchronous payments ## Asynchronous payments
Some payment providers require supporting asynchronous payments. This means that the client (your application) will only receive a notification when the payment is initiated. In order to verify completion of payment, further communication needs to be done between the developer server (or client) and the payment provider in order to verify. Some payment providers require supporting asynchronous payments. This means that the client (your application) will only receive a notification when the payment is initiated. In order to verify completion of payment, further communication needs to be done between the developer server (or client) and the payment provider in order to verify.
In the case of an initiated asynchronous payment the IAP listener will receive the state TRANS_STATE_UNVERIFIED to indicate this (as opposed to TRANS_STATE_PURCHASED). This is the final state of the payment, meaning no more callbacks will be done on this transaction. In the case of an initiated asynchronous payment the IAP listener will receive the state TRANS_STATE_UNVERIFIED to indicate this (as opposed to TRANS_STATE_PURCHASED). This is the final state of the payment, meaning no more callbacks will be done on this transaction.
## Purchase fulfilment ## Purchase fulfilment
In order to complete a purchase from a payment provider, the application needs to signal a purchase fulfilment to the provider telling the provider the purchase has gone through (for example by developer server-side verification). In order to complete a purchase from a payment provider, the application needs to signal a purchase fulfilment to the provider telling the provider the purchase has gone through (for example by developer server-side verification).
IAP supports auto-completion, where fulfilment is automatically signalled to the provider when a purchase is complete (this is the default behavior). You can also disable auto-completion in the game project settings. You are then required to call `iap.finish()` when the transaction is complete, which will signal purchase fulfilment to the provider. IAP supports auto-completion, where fulfilment is automatically signalled to the provider when a purchase is complete (this is the default behavior). You can also disable auto-completion in the game project settings. You are then required to call `iap.finish()` when the transaction is complete, which will signal purchase fulfilment to the provider.
### 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 fulfillment 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.
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.
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()`.
@@ -212,7 +215,7 @@ Android `iap.list()` returns "failed to fetch product"
: You need to upload and publish an *.apk* on the alpha or beta channels on the Google Play Developer Console. Also make sure that the _time and date_ on your device is correct. : You need to upload and publish an *.apk* on the alpha or beta channels on the Google Play Developer Console. Also make sure that the _time and date_ on your device is correct.
Android (Google Play) `iap.list()` never returns more than 20 products Android (Google Play) `iap.list()` never returns more than 20 products
: Google has an [limit of 20 products per request](https://github.com/android/play-billing-samples/blob/7a94c6905a9c125518354c216b5c3094fde47ce1/TrivialDrive/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl#L62). The solution is to make multiple calls to `iap.list()` and combine the results if the number of products exceeds 20. : Google has a [limit of 20 products per request](https://github.com/android/play-billing-samples/blob/7a94c6905a9c125518354c216b5c3094fde47ce1/TrivialDrive/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl#L62). The solution is to make multiple calls to `iap.list()` and combine the results if the number of products exceeds 20.
iOS `iap.list()` returns nothing iOS `iap.list()` returns nothing
: Make sure that youve requested an iOS Paid Applications account, and all proper documentation has been filed. Without proper authorization, your iOS app purchasing (even test purchases) will not work. : Make sure that youve requested an iOS Paid Applications account, and all proper documentation has been filed. Without proper authorization, your iOS app purchasing (even test purchases) will not work.

View File

@@ -9,7 +9,7 @@
- name: buy - name: buy
type: function type: function
desc: Sets the listener function for In-app purchase events. desc: Purchase a product.
parameters: parameters:
- name: id - name: id
type: string type: string
@@ -23,6 +23,10 @@
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: [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.
[icon:googleplay]: 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).
[icon:googleplay]: 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: [icon:ios] [icon:googleplay] [icon:facebook] The currency code.
[icon:ios] [icon:googleplay] [icon:facebook] [icon:googleplay]: The merchant's locale, instead of the user's
[icon:googleplay]: Used only for in-app products
- name: subscriptions
type: table
desc: [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
@@ -226,6 +271,10 @@
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.

View File

@@ -72,7 +72,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 = allocate(intArrayFromString(productsJSON), 'i8', ALLOC_STACK);
Runtime.dynCall('vii', callback, [lua_callback, res_buf]); {{{ makeDynCall('vii', 'callback')}}}(lua_callback, res_buf);
} else { } else {
var xmlhttp = new XMLHttpRequest(); var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() { xmlhttp.onreadystatechange = function() {
@@ -87,7 +87,7 @@ var LibraryFacebookIAP = {
}, },
dmIAPFBList: function(params, callback, lua_callback) { dmIAPFBList: function(params, callback, lua_callback) {
var product_ids = Pointer_stringify(params).trim().split(','); var product_ids = UTF8ToString(params).trim().split(',');
var product_count = product_ids.length; var product_count = product_ids.length;
if(product_count == 0) { if(product_count == 0) {
console.log("Calling iap.list with no item id's. Ignored."); console.log("Calling iap.list with no item id's. Ignored.");
@@ -105,7 +105,7 @@ var LibraryFacebookIAP = {
// https://developers.facebook.com/docs/javascript/reference/FB.ui // https://developers.facebook.com/docs/javascript/reference/FB.ui
dmIAPFBBuy: function(param_product_id, param_request_id, callback, lua_callback) { dmIAPFBBuy: function(param_product_id, param_request_id, callback, lua_callback) {
var product_id = Pointer_stringify(param_product_id); var product_id = UTF8ToString(param_product_id);
var buy_params = { var buy_params = {
method: 'pay', method: 'pay',
@@ -113,7 +113,7 @@ var LibraryFacebookIAP = {
product: product_id, product: product_id,
}; };
if(param_request_id != 0) { if(param_request_id != 0) {
buy_params.request_id = Pointer_stringify(param_request_id); buy_params.request_id = UTF8ToString(param_request_id);
} }
FB.ui(buy_params, FB.ui(buy_params,
@@ -144,7 +144,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 = allocate(intArrayFromString(productsJSON), 'i8', ALLOC_STACK);
Runtime.dynCall('viii', callback, [lua_callback, res_buf, 0]); {{{ makeDynCall('viii', 'callback')}}}(lua_callback, res_buf, 0);
} else { } else {
@@ -157,7 +157,7 @@ var LibraryFacebookIAP = {
reason = FBinner.BillingResponse.BILLING_RESPONSE_RESULT_ERROR; reason = FBinner.BillingResponse.BILLING_RESPONSE_RESULT_ERROR;
console.log("Unknown response: ", response); console.log("Unknown response: ", response);
} }
Runtime.dynCall('viii', callback, [lua_callback, 0, reason]); {{{ makeDynCall('viii', 'callback')}}}(lua_callback, 0, reason);
} }
} }
); );

View File

@@ -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>

View File

@@ -1,3 +1,3 @@
dependencies { dependencies {
implementation 'com.android.billingclient:billing:3.0.0' implementation 'com.android.billingclient:billing:5.0.0'
} }

View File

@@ -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()
@@ -30,7 +18,6 @@ struct IAP
m_autoFinishTransactions = true; m_autoFinishTransactions = true;
m_ProviderId = PROVIDER_ID_GOOGLE; m_ProviderId = PROVIDER_ID_GOOGLE;
} }
int m_InitCount;
bool m_autoFinishTransactions; bool m_autoFinishTransactions;
int m_ProviderId; int m_ProviderId;
@@ -53,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;
@@ -78,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");
@@ -120,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;
} }
} }
@@ -136,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);
@@ -160,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;
} }
} }
@@ -176,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;
} }
@@ -191,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;
@@ -205,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;
@@ -216,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;
} }
@@ -399,59 +396,41 @@ static void HandlePurchaseResult(const IAPCommand* cmd)
static dmExtension::Result InitializeIAP(dmExtension::Params* params) static dmExtension::Result InitializeIAP(dmExtension::Params* params)
{ {
// TODO: Life-cycle managaemnt is *budget*. No notion of "static initalization" IAP_Queue_Create(&g_IAP.m_CommandQueue);
// Extend extension functionality with per system initalization?
if (g_IAP.m_InitCount == 0) {
IAP_Queue_Create(&g_IAP.m_CommandQueue);
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"); const char* provider = dmConfigFile::GetString(params->m_ConfigFile, "android.iap_provider", "GooglePlay");
jmethodID get_class_loader = env->GetMethodID(activity_class,"getClassLoader", "()Ljava/lang/ClassLoader;"); const char* class_name = "com.defold.iap.IapGooglePlay";
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"); g_IAP.m_ProviderId = PROVIDER_ID_GOOGLE;
const char* class_name = "com.defold.iap.IapGooglePlay"; if (!strcmp(provider, "Amazon")) {
g_IAP.m_ProviderId = PROVIDER_ID_AMAZON;
g_IAP.m_ProviderId = PROVIDER_ID_GOOGLE; class_name = "com.defold.iap.IapAmazon";
if (!strcmp(provider, "Amazon")) {
g_IAP.m_ProviderId = PROVIDER_ID_AMAZON;
class_name = "com.defold.iap.IapAmazon";
}
else if (strcmp(provider, "GooglePlay")) {
dmLogWarning("Unknown IAP provider name [%s], defaulting to GooglePlay", provider);
}
jstring str_class_name = env->NewStringUTF(class_name);
jclass iap_class = (jclass)env->CallObjectMethod(cls, find_class, str_class_name);
env->DeleteLocalRef(str_class_name);
str_class_name = env->NewStringUTF("com.defold.iap.IapJNI");
jclass iap_jni_class = (jclass)env->CallObjectMethod(cls, find_class, str_class_name);
env->DeleteLocalRef(str_class_name);
g_IAP.m_List = env->GetMethodID(iap_class, "listItems", "(Ljava/lang/String;Lcom/defold/iap/IListProductsListener;J)V");
g_IAP.m_Buy = env->GetMethodID(iap_class, "buy", "(Ljava/lang/String;Lcom/defold/iap/IPurchaseListener;)V");
g_IAP.m_Restore = env->GetMethodID(iap_class, "restore", "(Lcom/defold/iap/IPurchaseListener;)V");
g_IAP.m_Stop = env->GetMethodID(iap_class, "stop", "()V");
g_IAP.m_ProcessPendingConsumables = env->GetMethodID(iap_class, "processPendingConsumables", "(Lcom/defold/iap/IPurchaseListener;)V");
g_IAP.m_FinishTransaction = env->GetMethodID(iap_class, "finishTransaction", "(Ljava/lang/String;Lcom/defold/iap/IPurchaseListener;)V");
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");
g_IAP.m_IAP = env->NewGlobalRef(env->NewObject(iap_class, jni_constructor, dmGraphics::GetNativeAndroidActivity(), g_IAP.m_autoFinishTransactions));
jni_constructor = env->GetMethodID(iap_jni_class, "<init>", "()V");
g_IAP.m_IAPJNI = env->NewGlobalRef(env->NewObject(iap_jni_class, jni_constructor));
Detach();
} }
g_IAP.m_InitCount++; else if (strcmp(provider, "GooglePlay")) {
dmLogWarning("Unknown IAP provider name [%s], defaulting to GooglePlay", provider);
}
jclass iap_class = dmAndroid::LoadClass(env, class_name);
jclass iap_jni_class = dmAndroid::LoadClass(env, "com.defold.iap.IapJNI");
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;Ljava/lang/String;Lcom/defold/iap/IPurchaseListener;)V");
g_IAP.m_Restore = env->GetMethodID(iap_class, "restore", "(Lcom/defold/iap/IPurchaseListener;)V");
g_IAP.m_Stop = env->GetMethodID(iap_class, "stop", "()V");
g_IAP.m_ProcessPendingConsumables = env->GetMethodID(iap_class, "processPendingConsumables", "(Lcom/defold/iap/IPurchaseListener;)V");
g_IAP.m_FinishTransaction = env->GetMethodID(iap_class, "finishTransaction", "(Ljava/lang/String;Lcom/defold/iap/IPurchaseListener;)V");
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");
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");
g_IAP.m_IAPJNI = env->NewGlobalRef(env->NewObject(iap_jni_class, jni_constructor));
lua_State*L = params->m_L; lua_State*L = params->m_L;
int top = lua_gettop(L); int top = lua_gettop(L);
@@ -494,21 +473,18 @@ static dmExtension::Result UpdateIAP(dmExtension::Params* params)
static dmExtension::Result FinalizeIAP(dmExtension::Params* params) static dmExtension::Result FinalizeIAP(dmExtension::Params* params)
{ {
IAP_Queue_Destroy(&g_IAP.m_CommandQueue); IAP_Queue_Destroy(&g_IAP.m_CommandQueue);
--g_IAP.m_InitCount;
if (params->m_L == dmScript::GetCallbackLuaContext(g_IAP.m_Listener)) { if (params->m_L == dmScript::GetCallbackLuaContext(g_IAP.m_Listener)) {
dmScript::DestroyCallback(g_IAP.m_Listener); dmScript::DestroyCallback(g_IAP.m_Listener);
g_IAP.m_Listener = 0; g_IAP.m_Listener = 0;
} }
if (g_IAP.m_InitCount == 0) { dmAndroid::ThreadAttacher threadAttacher;
JNIEnv* env = Attach(); 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;
} }

View File

@@ -24,7 +24,7 @@ struct IAP
memset(this, 0, sizeof(*this)); memset(this, 0, sizeof(*this));
m_AutoFinishTransactions = true; m_AutoFinishTransactions = true;
} }
int m_InitCount; int m_Version;
bool m_AutoFinishTransactions; bool m_AutoFinishTransactions;
NSMutableDictionary* m_PendingTransactions; NSMutableDictionary* m_PendingTransactions;
dmScript::LuaCallbackInfo* m_Listener; dmScript::LuaCallbackInfo* m_Listener;
@@ -103,11 +103,17 @@ static void IAP_FreeTransaction(IAPTransaction* transaction)
@interface SKProductsRequestDelegate : NSObject<SKProductsRequestDelegate> @interface SKProductsRequestDelegate : NSObject<SKProductsRequestDelegate>
@property dmScript::LuaCallbackInfo* m_Callback; @property dmScript::LuaCallbackInfo* m_Callback;
@property (assign) SKProductsRequest* m_Request; @property (assign) SKProductsRequest* m_Request;
@property int m_Version;
@end @end
@implementation SKProductsRequestDelegate @implementation SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{ - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
if (self.m_Version != g_IAP.m_Version) {
dmLogWarning("Received products but the extension has been restarted")
return;
}
if (!dmScript::IsCallbackValid(self.m_Callback)) { if (!dmScript::IsCallbackValid(self.m_Callback)) {
dmLogError("No callback set"); dmLogError("No callback set");
return; return;
@@ -162,7 +168,7 @@ static void HandleProductResult(IAPCommand* cmd)
IAPResponse* response = (IAPResponse*)cmd->m_Data; IAPResponse* response = (IAPResponse*)cmd->m_Data;
lua_State* L = dmScript::GetCallbackLuaContext(g_IAP.m_Listener); lua_State* L = dmScript::GetCallbackLuaContext(cmd->m_Callback);
int top = lua_gettop(L); int top = lua_gettop(L);
if (!dmScript::SetupCallback(cmd->m_Callback)) if (!dmScript::SetupCallback(cmd->m_Callback))
@@ -409,6 +415,7 @@ static int IAP_List(lua_State* L)
delegate.m_Callback = dmScript::CreateCallback(L, 2); delegate.m_Callback = dmScript::CreateCallback(L, 2);
delegate.m_Request = products_request; delegate.m_Request = products_request;
delegate.m_Version = g_IAP.m_Version;
products_request.delegate = delegate; products_request.delegate = delegate;
[products_request start]; [products_request start];
@@ -529,13 +536,9 @@ static const luaL_reg IAP_methods[] =
static dmExtension::Result InitializeIAP(dmExtension::Params* params) static dmExtension::Result InitializeIAP(dmExtension::Params* params)
{ {
// TODO: Life-cycle managaemnt is *budget*. No notion of "static initalization" g_IAP.m_AutoFinishTransactions = dmConfigFile::GetInt(params->m_ConfigFile, "iap.auto_finish_transactions", 1) == 1;
// Extend extension functionality with per system initalization? g_IAP.m_PendingTransactions = [[NSMutableDictionary alloc]initWithCapacity:2];
if (g_IAP.m_InitCount == 0) { g_IAP.m_Version++;
g_IAP.m_AutoFinishTransactions = dmConfigFile::GetInt(params->m_ConfigFile, "iap.auto_finish_transactions", 1) == 1;
g_IAP.m_PendingTransactions = [[NSMutableDictionary alloc]initWithCapacity:2];
}
g_IAP.m_InitCount++;
IAP_Queue_Create(&g_IAP.m_CommandQueue); IAP_Queue_Create(&g_IAP.m_CommandQueue);
IAP_Queue_Create(&g_IAP.m_ObservableQueue); IAP_Queue_Create(&g_IAP.m_ObservableQueue);
@@ -590,26 +593,22 @@ static dmExtension::Result UpdateIAP(dmExtension::Params* params)
static dmExtension::Result FinalizeIAP(dmExtension::Params* params) static dmExtension::Result FinalizeIAP(dmExtension::Params* params)
{ {
--g_IAP.m_InitCount; if (g_IAP.m_Listener)
{
// TODO: Should we support one listener per lua-state?
// Or just use a single lua-state...?
if (params->m_L == dmScript::GetCallbackLuaContext(g_IAP.m_Listener)) {
dmScript::DestroyCallback(g_IAP.m_Listener); dmScript::DestroyCallback(g_IAP.m_Listener);
g_IAP.m_Listener = 0;
} }
if (g_IAP.m_InitCount == 0) { g_IAP.m_Listener = 0;
if (g_IAP.m_PendingTransactions) {
[g_IAP.m_PendingTransactions release];
g_IAP.m_PendingTransactions = 0;
}
if (g_IAP.m_Observer) { if (g_IAP.m_PendingTransactions) {
[[SKPaymentQueue defaultQueue] removeTransactionObserver: g_IAP.m_Observer]; [g_IAP.m_PendingTransactions release];
[g_IAP.m_Observer release]; g_IAP.m_PendingTransactions = 0;
g_IAP.m_Observer = 0; }
}
if (g_IAP.m_Observer) {
[[SKPaymentQueue defaultQueue] removeTransactionObserver: g_IAP.m_Observer];
[g_IAP.m_Observer release];
g_IAP.m_Observer = 0;
} }
IAP_Queue_Destroy(&g_IAP.m_CommandQueue); IAP_Queue_Destroy(&g_IAP.m_CommandQueue);

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
@@ -45,6 +46,7 @@ public class IapAmazon implements PurchasingListener {
this.activity = activity; this.activity = activity;
this.autoFinishTransactions = autoFinishTransactions; this.autoFinishTransactions = autoFinishTransactions;
this.listProductsListeners = new HashMap<RequestId, IListProductsListener>(); this.listProductsListeners = new HashMap<RequestId, IListProductsListener>();
this.listProductsCommandPtrs = new HashMap<RequestId, Long>();
this.purchaseListeners = new HashMap<RequestId, IPurchaseListener>(); this.purchaseListeners = new HashMap<RequestId, IPurchaseListener>();
PurchasingService.registerListener(activity, this); PurchasingService.registerListener(activity, this);
} }
@@ -79,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) {
@@ -96,6 +98,10 @@ public class IapAmazon implements PurchasingListener {
} }
PurchasingService.notifyFulfillment(receipt, FulfillmentResult.FULFILLED); PurchasingService.notifyFulfillment(receipt, FulfillmentResult.FULFILLED);
} }
public void acknowledgeTransaction(final String purchaseToken, final IPurchaseListener purchaseListener) {
// Stub to prevent errors.
}
private void doGetPurchaseUpdates(final IPurchaseListener listener, final boolean reset) { private void doGetPurchaseUpdates(final IPurchaseListener listener, final boolean reset) {
synchronized (purchaseListeners) { synchronized (purchaseListeners) {
@@ -168,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();
@@ -184,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) {

View File

@@ -19,26 +19,34 @@ 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.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;
@@ -102,6 +110,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 +118,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;
} }
@@ -183,21 +243,6 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
return billingResponseCodeToDefoldResponse(result.getResponseCode()); return billingResponseCodeToDefoldResponse(result.getResponseCode());
} }
/**
* Query Google Play for purchases done within the app.
*/
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;
}
/** /**
* This method is called either explicitly from Lua or from extension code * This method is called either explicitly from Lua or from extension code
* when "set_listener()" is called from Lua. * when "set_listener()" is called from Lua.
@@ -206,12 +251,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 +287,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();
@@ -272,7 +339,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) {
@@ -309,12 +376,18 @@ 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) {
@@ -326,21 +399,21 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
/** /**
* 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) { if (billingResult.getResponseCode() == BillingResponseCode.OK && (productDetailsList != null) && !productDetailsList.isEmpty()) {
buyProduct(skuDetailsList.get(0), purchaseListener); buyProduct(productDetailsList.get(0), token, purchaseListener);
} }
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());
@@ -355,30 +428,43 @@ 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) {
// 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); 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 +473,21 @@ 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) {
for (SkuDetails sd : skuDetails) { for (ProductDetails pd : productDetails) {
a.put(convertSkuDetails(sd)); a.put(convertProductDetails(pd));
} }
} }
else { else {

View File

@@ -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 = {
@@ -66,6 +66,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))