33 Commits

Author SHA1 Message Date
Björn Ritzl
fb3d6b78aa Merge pull request #76 from ekharkunov/bob-reusable-workflow
Use reusable workflow
2025-09-09 22:18:37 +02:00
Kharkunov Eugene
c4b177a24d Use reusable workflow 2025-09-09 09:25:24 +03:00
vlaaad
09544baf1c Merge pull request #75 from defold/vlaaad-patch-1
Create ext.properties
2025-08-22 16:27:16 +02:00
vlaaad
6d3ca89b65 Create ext.properties 2025-08-22 13:41:25 +02:00
Björn Ritzl
0df5a8e968 Update index.md 2025-07-06 14:54:16 +02:00
Björn Ritzl
88028fd29f Update index.md 2025-06-04 07:54:03 +02:00
Björn Ritzl
c0e35039f4 Merge pull request #66 from defold/dev-update-to-android-billing-7-0-0
Update to Google Play Billing 7 0 0
2025-02-11 22:23:42 +01:00
Björn Ritzl
2e17b2f413 Merge branch 'master' into dev-update-to-android-billing-7-0-0 2025-02-11 13:58:34 +01:00
Björn Ritzl
f398677d7b Merge pull request #68 from defold/dev-update-to-android-billing-6-0-1
Update to android billing 6.2.1
2024-07-04 15:18:13 +02:00
Björn Ritzl
d1c4e88562 Update build.gradle 2024-07-04 15:04:32 +02:00
Björn Ritzl
6f06d7f08f Updated to 6.0.1 2024-07-04 14:52:58 +02:00
Björn Ritzl
0852e42977 Update .gitignore 2024-07-04 14:36:12 +02:00
Björn Ritzl
33174f25ea Merge branch 'master' into dev-update-to-android-billing-7-0-0 2024-07-03 19:27:05 +02:00
Björn Ritzl
ba0e1b645a Merge pull request #65 from defold/dev-update-to-android-billing-6-0-0
Updated to Play Billing 6.0.0
2024-07-03 19:26:34 +02:00
Björn Ritzl
801179288e Include the offer token when buying 2024-07-03 19:25:36 +02:00
Björn Ritzl
1c27130eef Updated to Billing 7.0.0 2024-07-03 19:06:50 +02:00
Björn Ritzl
195ef400b5 Play Billing 6.0.0 2024-07-02 22:33:09 +02:00
Alexey Gulev
68ef7f4615 Merge pull request #63 from ekharkunov/emscripten-update
Update js code according to new Emscripten version (3.1.55).
2024-04-10 11:08:22 +02:00
Kharkunov Eugene
1a34582603 Update js code according to new Emscripten version (3.1.55).
Update assets to latest Defold version.
Remove armv7-ios from manifest as unsupported platform.
2024-04-10 11:27:41 +03:00
Björn Ritzl
09f5060d44 Update index.md 2023-10-03 14:40:19 +02:00
Björn Ritzl
649a8a1ebf Merge pull request #58 from defold/Issue-47-index-out-of-bounds-exception
Added product and purchase null checks
2023-02-07 15:53:39 +01:00
Björn Ritzl
5c09447e37 Merge pull request #59 from defold/Issue-56-app-crash-on-slow-purchase
Check that the purchase listener is not null
2023-02-07 15:51:25 +01:00
Björn Ritzl
ad06de7b9c Check purchase listener 2023-02-06 23:11:24 +01:00
Björn Ritzl
d74c97d5c7 Added null checks 2023-02-06 15:56:03 +01:00
Björn Ritzl
f97a7ee6b6 Merge pull request #55 from defold/fix-json-migration-issue
Push nil as last argument if there is no error
2022-11-21 10:30:12 +01:00
Björn Ritzl
5f3f43fb2e Push nil as last argument if there is no error 2022-11-21 10:21:12 +01:00
Mathias Westerdahl
caff4397d8 Merge pull request #54 from defold/lua-to-json
Updated to use dmScript::LuaToJson()
2022-11-02 15:00:12 +01:00
JCash
501af9c90d Updated to use dmScript::LuaToJson() 2022-11-02 14:36:36 +01:00
Björn Ritzl
961a43f732 Update index.md 2022-09-27 14:46:24 +02:00
Björn Ritzl
47f03108ab Merge branch 'master' of https://github.com/defold/extension-iap 2022-09-23 16:21:36 +02:00
Björn Ritzl
c0e1a9aef1 Fixed YAML format errors 2022-09-23 16:21:27 +02:00
Alexey Gulev
f4294a9eb5 Merge pull request #53 from defold/AGulev-patch-1
Update index.md
2022-09-12 16:26:20 +02:00
Alexey Gulev
8799860198 Update index.md 2022-09-12 16:26:07 +02:00
16 changed files with 216 additions and 196 deletions

View File

@@ -1,76 +1,7 @@
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'
on: [push, pull_request]
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
build:
uses: defold/github-actions-common/.github/workflows/bob.yml@master

2
.gitignore vendored
View File

@@ -9,3 +9,5 @@ Thumbs.db
.cproject
builtins
_site
manifest.private.der
manifest.public.der

View File

@@ -1,6 +1,6 @@
---
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
@@ -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:
* Apples iOS Appstore - StoreKit
* Google Play Billing 3.0
* Google Play Billing 5.0
* Amazon 'in-app billing' 2.0.61
* Facebook Canvas 'game payments'
@@ -31,7 +31,7 @@ Detailed documentation from Apple, Google, Amazon and Facebook can be found here
## Installation
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).
@@ -186,7 +186,11 @@ IAP supports auto-completion, where fulfilment is automatically signalled to the
### 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()`.
@@ -234,6 +238,3 @@ On iOS, the "price_string" field contains '~' characters
## Source code
The source code is available on [GitHub](https://github.com/defold/extension-iap)
## API reference

View File

@@ -21,11 +21,11 @@
members:
- name: request_id
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.
- name: token
type: string
desc: [icon:googleplay] Which subscription offer to use when buying a subscription. The token can be retrieved from
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:
@@ -132,22 +132,22 @@
- name: price
type: number
desc: The price of the product.
[icon:googleplay]: Used only for in-app products
For Google Play [icon:googleplay] this is used only for in-app products
- name: price_string
type: string
desc: The price of the product, as a formatted string (amount and currency symbol).
[icon:googleplay]: Used only for in-app products
For Google Play [icon:googleplay] this is used only for in-app products
- name: currency_code
type: string
desc: [icon:ios] [icon:googleplay] [icon:facebook] The currency code.
[icon:googleplay]: The merchant's locale, instead of the user's
[icon:googleplay]: Used only for in-app products
desc: The currency code.
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: [icon:googleplay] List of subscription offers.
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:
@@ -269,36 +269,36 @@
- name: original_trans
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.
desc: Android only [icon:android]. The purchase order details in JSON format.
- name: signature
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
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
type: string
desc: Amazon Pay only[icon:amazon]. The user ID.
desc: Amazon Pay only [icon:amazon]. The user ID.
- name: is_sandbox_mode
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.
- name: cancel_date
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
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
type: table

View File

@@ -1,10 +1,6 @@
name: IAPExt
platforms:
armv7-ios:
context:
weakFrameworks: ['StoreKit', 'UIKit', 'Foundation']
arm64-ios:
context:
weakFrameworks: ['StoreKit', 'UIKit', 'Foundation']

View 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

View File

@@ -71,7 +71,7 @@ var LibraryFacebookIAP = {
if(url_index == product_count-1) {
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);
} else {
var xmlhttp = new XMLHttpRequest();
@@ -143,7 +143,7 @@ var LibraryFacebookIAP = {
}
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);
} else {
@@ -166,4 +166,4 @@ var LibraryFacebookIAP = {
}
autoAddDeps(LibraryFacebookIAP, '$FBinner');
mergeInto(LibraryManager.library, LibraryFacebookIAP);
addToLibrary(LibraryFacebookIAP);

View File

@@ -1,3 +1,7 @@
dependencies {
implementation 'com.android.billingclient:billing:5.0.0'
repositories {
mavenCentral()
}
dependencies {
implementation 'com.android.billingclient:billing:7.0.0'
}

View File

@@ -31,6 +31,7 @@ enum BillingResponse
BILLING_RESPONSE_RESULT_ERROR = 6,
BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7,
BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8,
BILLING_RESPONSE_RESULT_NETWORK_ERROR = 9,
};
enum ProviderId

View File

@@ -306,23 +306,9 @@ static void HandleProductResult(const IAPCommand* cmd)
}
if (cmd->m_ResponseCode == BILLING_RESPONSE_RESULT_OK) {
dmJson::Document doc;
dmJson::Result r = dmJson::Parse((const char*) cmd->m_Data, &doc);
if (r == dmJson::RESULT_OK && doc.m_NodeCount > 0) {
char err_str[128];
if (dmScript::JsonToLua(L, &doc, 0, err_str, sizeof(err_str)) < 0) {
dmLogError("Failed converting product result JSON to Lua; %s", err_str);
lua_pushnil(L);
IAP_PushError(L, "failed to convert JSON to Lua for product response", REASON_UNSPECIFIED);
} else {
lua_pushnil(L);
}
} else {
dmLogError("Failed to parse product response (%d)", r);
lua_pushnil(L);
IAP_PushError(L, "failed to parse product response", REASON_UNSPECIFIED);
}
dmJson::Free(&doc);
const char* json = (const char*)cmd->m_Data;
dmScript::JsonToLua(L, json, strlen(json)); // throws lua error if it fails
lua_pushnil(L);
} else {
dmLogError("IAP error %d", cmd->m_ResponseCode);
lua_pushnil(L);
@@ -356,23 +342,9 @@ static void HandlePurchaseResult(const IAPCommand* cmd)
if (cmd->m_ResponseCode == BILLING_RESPONSE_RESULT_OK) {
if (cmd->m_Data != 0) {
dmJson::Document doc;
dmJson::Result r = dmJson::Parse((const char*) cmd->m_Data, &doc);
if (r == dmJson::RESULT_OK && doc.m_NodeCount > 0) {
char err_str[128];
if (dmScript::JsonToLua(L, &doc, 0, err_str, sizeof(err_str)) < 0) {
dmLogError("Failed converting purchase JSON result to Lua; %s", err_str);
lua_pushnil(L);
IAP_PushError(L, "failed to convert purchase response JSON to Lua", REASON_UNSPECIFIED);
} else {
lua_pushnil(L);
}
} else {
dmLogError("Failed to parse purchase response (%d)", r);
lua_pushnil(L);
IAP_PushError(L, "failed to parse purchase response", REASON_UNSPECIFIED);
}
dmJson::Free(&doc);
const char* json = (const char*)cmd->m_Data;
dmScript::JsonToLua(L, json, strlen(json)); // throws lua error if it fails
lua_pushnil(L);
} else {
dmLogError("IAP error, purchase response was null");
lua_pushnil(L);

View File

@@ -46,23 +46,8 @@ static void IAPList_Callback(void* luacallback, const char* result_json)
if(result_json != 0)
{
dmJson::Document doc;
dmJson::Result r = dmJson::Parse(result_json, &doc);
if (r == dmJson::RESULT_OK && doc.m_NodeCount > 0) {
char err_str[128];
if (dmScript::JsonToLua(L, &doc, 0, err_str, sizeof(err_str)) < 0) {
dmLogError("Failed converting list result JSON to Lua; %s", err_str);
lua_pushnil(L);
IAP_PushError(L, "Failed converting list result JSON to Lua", REASON_UNSPECIFIED);
} else {
lua_pushnil(L);
}
} else {
dmLogError("Failed to parse list result JSON (%d)", r);
lua_pushnil(L);
IAP_PushError(L, "Failed to parse list result JSON", REASON_UNSPECIFIED);
}
dmJson::Free(&doc);
dmScript::JsonToLua(L, result_json, strlen(result_json)); // throws lua error if it fails
lua_pushnil(L);
}
else
{
@@ -112,23 +97,8 @@ static void IAPListener_Callback(void* luacallback, const char* result_json, int
}
if (result_json) {
dmJson::Document doc;
dmJson::Result r = dmJson::Parse(result_json, &doc);
if (r == dmJson::RESULT_OK && doc.m_NodeCount > 0) {
char err_str[128];
if (dmScript::JsonToLua(L, &doc, 0, err_str, sizeof(err_str)) < 0) {
dmLogError("Failed converting purchase result JSON to Lua; %s", err_str);
lua_pushnil(L);
IAP_PushError(L, "failed converting purchase result JSON to Lua", REASON_UNSPECIFIED);
} else {
lua_pushnil(L);
}
} else {
dmLogError("Failed to parse purchase response (%d)", r);
lua_pushnil(L);
IAP_PushError(L, "failed to parse purchase response", REASON_UNSPECIFIED);
}
dmJson::Free(&doc);
dmScript::JsonToLua(L, result_json, strlen(result_json)); // throws lua error if it fails
lua_pushnil(L);
} else {
lua_pushnil(L);
switch(error_code)

View File

@@ -21,6 +21,7 @@ import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClient.BillingResponseCode;
import com.android.billingclient.api.BillingClient.ProductType;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.PendingPurchasesParams;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.Purchase.PurchaseState;
import com.android.billingclient.api.ProductDetails;
@@ -56,7 +57,8 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
this.activity = activity;
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() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
@@ -221,7 +223,6 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
case BillingResponseCode.OK:
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_OK;
break;
case BillingResponseCode.SERVICE_TIMEOUT:
case BillingResponseCode.SERVICE_UNAVAILABLE:
case BillingResponseCode.SERVICE_DISCONNECTED:
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_SERVICE_UNAVAILABLE;
@@ -229,6 +230,9 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
case BillingResponseCode.USER_CANCELED:
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_USER_CANCELED;
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.ERROR:
default:
@@ -243,6 +247,20 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
return billingResponseCodeToDefoldResponse(result.getResponseCode());
}
private void invokeOnPurchaseResultListener(IPurchaseListener purchaseListener, int billingResultCode, String purchaseData) {
if (purchaseListener == null) {
Log.w(TAG, "Received billing result but no listener has been set");
return;
}
purchaseListener.onPurchaseResult(billingResultCode, purchaseData);
}
private void invokeOnPurchaseResultListener(IPurchaseListener purchaseListener, BillingResult billingResult, Purchase purchase) {
invokeOnPurchaseResultListener(purchaseListener, billingResultToDefoldResponse(billingResult), convertPurchase(purchase));
}
private void invokeOnPurchaseResultListener(IPurchaseListener purchaseListener, BillingResult billingResult) {
invokeOnPurchaseResultListener(purchaseListener, billingResultToDefoldResponse(billingResult), "");
}
/**
* This method is called either explicitly from Lua or from extension code
* when "set_listener()" is called from Lua.
@@ -306,7 +324,7 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
// note: we only call the purchase listener if an error happens
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
Log.e(TAG, "Unable to consume purchase: " + billingResult.getDebugMessage());
purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
invokeOnPurchaseResultListener(purchaseListener, billingResult);
}
}
});
@@ -329,7 +347,7 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
// note: we only call the purchase listener if an error happens
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
Log.e(TAG, "Unable to acknowledge purchase: " + billingResult.getDebugMessage());
purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
invokeOnPurchaseResultListener(purchaseListener, billingResult);
}
}
});
@@ -348,12 +366,12 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
@Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
Log.d(TAG, "handlePurchase() response code " + billingResult.getResponseCode() + " purchaseToken: " + purchaseToken);
purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), convertPurchase(purchase));
invokeOnPurchaseResultListener(purchaseListener, billingResult, purchase);
}
});
}
else {
purchaseListener.onPurchaseResult(billingResponseCodeToDefoldResponse(BillingResponseCode.OK), convertPurchase(purchase));
invokeOnPurchaseResultListener(purchaseListener, billingResponseCodeToDefoldResponse(BillingResponseCode.OK), convertPurchase(purchase));
}
}
@@ -362,13 +380,17 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
*/
@Override
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
if (billingResult.getResponseCode() == BillingResponseCode.OK && purchases != null) {
for (Purchase purchase : purchases) {
handlePurchase(purchase, this.purchaseListener);
if (billingResult.getResponseCode() == BillingResponseCode.OK) {
if (purchases != null && !purchases.isEmpty()) {
for (Purchase purchase : purchases) {
if (purchase != null) {
handlePurchase(purchase, this.purchaseListener);
}
}
}
}
else {
this.purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
invokeOnPurchaseResultListener(this.purchaseListener, billingResult);
}
}
@@ -392,7 +414,7 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
BillingResult billingResult = billingClient.launchBillingFlow(this.activity, billingFlowParams);
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
Log.e(TAG, "Purchase failed: " + billingResult.getDebugMessage());
purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
invokeOnPurchaseResultListener(purchaseListener, billingResult);
}
}
@@ -413,11 +435,16 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
@Override
public void onProductDetailsResponse(BillingResult billingResult, List<ProductDetails> productDetailsList) {
if (billingResult.getResponseCode() == BillingResponseCode.OK && (productDetailsList != null) && !productDetailsList.isEmpty()) {
buyProduct(productDetailsList.get(0), token, purchaseListener);
for (ProductDetails productDetails : productDetailsList) {
if (productDetails != null) {
buyProduct(productDetails, token, purchaseListener);
break;
}
}
}
else {
Log.e(TAG, "Unable to get product details before buying: " + billingResult.getDebugMessage());
purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
invokeOnPurchaseResultListener(purchaseListener, billingResult);
}
}
});
@@ -435,10 +462,12 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
@Override
public void onProductDetailsResponse(BillingResult billingResult, List<ProductDetails> productDetails) {
if (productDetails != null) {
if (productDetails != null && !productDetails.isEmpty()) {
// cache products (cache will be used to speed up buying)
for (ProductDetails pd : productDetails) {
IapGooglePlay.this.products.put(pd.getProductId(), pd);
if (pd != null) {
IapGooglePlay.this.products.put(pd.getProductId(), pd);
}
}
// add to list of all product details
allProductDetails.addAll(productDetails);
@@ -485,9 +514,11 @@ public class IapGooglePlay implements PurchasesUpdatedListener {
@Override
public void onProductDetailsResponse(BillingResult billingResult, List<ProductDetails> productDetails) {
JSONArray a = new JSONArray();
if (billingResult.getResponseCode() == BillingResponseCode.OK) {
if ((billingResult.getResponseCode() == BillingResponseCode.OK) && (productDetails != null) && !productDetails.isEmpty()) {
for (ProductDetails pd : productDetails) {
a.put(convertProductDetails(pd));
if (pd != null) {
a.put(convertProductDetails(pd));
}
}
}
else {

View File

@@ -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_ITEM_ALREADY_OWNED = 7;
public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
public static final int BILLING_RESPONSE_RESULT_NETWORK_ERROR = 9;
public IapJNI() {
}

View File

@@ -11,12 +11,12 @@ height = 1136
[android]
input_method = HiddenInputField
package = com.defold.extension.iap
version_code = 7
target_sdk_version = 29
version_code = 9
minimum_sdk_version = 21
[project]
title = extension-iap
dependencies = https://github.com/andsve/dirtylarry/archive/master.zip
dependencies#0 = https://github.com/andsve/dirtylarry/archive/master.zip
[library]
include_dirs = extension-iap

View File

@@ -1,7 +1,7 @@
script: "/main/main.gui_script"
fonts {
name: "system_font"
font: "/builtins/fonts/system_font.font"
name: "default"
font: "/builtins/fonts/default.font"
}
background_color {
x: 0.0
@@ -47,6 +47,8 @@ nodes {
alpha: 1.0
template: "/dirtylarry/button.gui"
template_node_child: false
custom_type: 0
enabled: true
}
nodes {
position {
@@ -102,6 +104,10 @@ nodes {
alpha: 1.0
template_node_child: true
size_mode: SIZE_MODE_MANUAL
custom_type: 0
enabled: true
visible: true
material: ""
}
nodes {
position {
@@ -166,6 +172,10 @@ nodes {
template_node_child: true
text_leading: 1.0
text_tracking: 0.0
custom_type: 0
enabled: true
visible: true
material: ""
}
nodes {
position {
@@ -205,6 +215,8 @@ nodes {
alpha: 1.0
template: "/dirtylarry/button.gui"
template_node_child: false
custom_type: 0
enabled: true
}
nodes {
position {
@@ -261,6 +273,10 @@ nodes {
overridden_fields: 4
template_node_child: true
size_mode: SIZE_MODE_MANUAL
custom_type: 0
enabled: true
visible: true
material: ""
}
nodes {
position {
@@ -325,6 +341,10 @@ nodes {
template_node_child: true
text_leading: 1.0
text_tracking: 0.0
custom_type: 0
enabled: true
visible: true
material: ""
}
nodes {
position {
@@ -360,7 +380,7 @@ nodes {
type: TYPE_TEXT
blend_mode: BLEND_MODE_ALPHA
text: "<text>"
font: "system_font"
font: "default"
id: "log"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
@@ -387,6 +407,10 @@ nodes {
template_node_child: false
text_leading: 1.0
text_tracking: 0.0
custom_type: 0
enabled: true
visible: true
material: ""
}
nodes {
position {
@@ -426,6 +450,8 @@ nodes {
alpha: 1.0
template: "/dirtylarry/button.gui"
template_node_child: false
custom_type: 0
enabled: true
}
nodes {
position {
@@ -481,6 +507,10 @@ nodes {
alpha: 1.0
template_node_child: true
size_mode: SIZE_MODE_MANUAL
custom_type: 0
enabled: true
visible: true
material: ""
}
nodes {
position {
@@ -545,6 +575,10 @@ nodes {
template_node_child: true
text_leading: 1.0
text_tracking: 0.0
custom_type: 0
enabled: true
visible: true
material: ""
}
nodes {
position {
@@ -584,6 +618,8 @@ nodes {
alpha: 1.0
template: "/dirtylarry/button.gui"
template_node_child: false
custom_type: 0
enabled: true
}
nodes {
position {
@@ -639,6 +675,10 @@ nodes {
alpha: 1.0
template_node_child: true
size_mode: SIZE_MODE_MANUAL
custom_type: 0
enabled: true
visible: true
material: ""
}
nodes {
position {
@@ -703,6 +743,10 @@ nodes {
template_node_child: true
text_leading: 1.0
text_tracking: 0.0
custom_type: 0
enabled: true
visible: true
material: ""
}
nodes {
position {
@@ -742,6 +786,8 @@ nodes {
alpha: 1.0
template: "/dirtylarry/button.gui"
template_node_child: false
custom_type: 0
enabled: true
}
nodes {
position {
@@ -798,6 +844,10 @@ nodes {
overridden_fields: 4
template_node_child: true
size_mode: SIZE_MODE_MANUAL
custom_type: 0
enabled: true
visible: true
material: ""
}
nodes {
position {
@@ -862,6 +912,10 @@ nodes {
template_node_child: true
text_leading: 1.0
text_tracking: 0.0
custom_type: 0
enabled: true
visible: true
material: ""
}
nodes {
position {
@@ -901,6 +955,8 @@ nodes {
alpha: 1.0
template: "/dirtylarry/button.gui"
template_node_child: false
custom_type: 0
enabled: true
}
nodes {
position {
@@ -956,6 +1012,10 @@ nodes {
alpha: 1.0
template_node_child: true
size_mode: SIZE_MODE_MANUAL
custom_type: 0
enabled: true
visible: true
material: ""
}
nodes {
position {
@@ -1020,6 +1080,10 @@ nodes {
template_node_child: true
text_leading: 1.0
text_tracking: 0.0
custom_type: 0
enabled: true
visible: true
material: ""
}
nodes {
position {
@@ -1059,6 +1123,8 @@ nodes {
alpha: 1.0
template: "/dirtylarry/button.gui"
template_node_child: false
custom_type: 0
enabled: true
}
nodes {
position {
@@ -1115,6 +1181,10 @@ nodes {
overridden_fields: 4
template_node_child: true
size_mode: SIZE_MODE_MANUAL
custom_type: 0
enabled: true
visible: true
material: ""
}
nodes {
position {
@@ -1179,6 +1249,10 @@ nodes {
template_node_child: true
text_leading: 1.0
text_tracking: 0.0
custom_type: 0
enabled: true
visible: true
material: ""
}
nodes {
position {
@@ -1218,6 +1292,8 @@ nodes {
alpha: 1.0
template: "/dirtylarry/checkbox_label.gui"
template_node_child: false
custom_type: 0
enabled: true
}
nodes {
position {
@@ -1273,6 +1349,10 @@ nodes {
alpha: 1.0
template_node_child: true
size_mode: SIZE_MODE_MANUAL
custom_type: 0
enabled: true
visible: true
material: ""
}
nodes {
position {
@@ -1337,6 +1417,10 @@ nodes {
template_node_child: true
text_leading: 1.0
text_tracking: 0.0
custom_type: 0
enabled: true
visible: true
material: ""
}
nodes {
position {
@@ -1376,6 +1460,8 @@ nodes {
alpha: 1.0
template: "/dirtylarry/checkbox_label.gui"
template_node_child: false
custom_type: 0
enabled: true
}
nodes {
position {
@@ -1431,6 +1517,10 @@ nodes {
alpha: 1.0
template_node_child: true
size_mode: SIZE_MODE_MANUAL
custom_type: 0
enabled: true
visible: true
material: ""
}
nodes {
position {
@@ -1495,6 +1585,10 @@ nodes {
template_node_child: true
text_leading: 1.0
text_tracking: 0.0
custom_type: 0
enabled: true
visible: true
material: ""
}
material: "/builtins/materials/gui.material"
adjust_reference: ADJUST_REFERENCE_PARENT

View File

@@ -44,7 +44,13 @@ end
local function 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
local function restore()