19 Commits
1.2 ... 1.4.0

Author SHA1 Message Date
Mathias Westerdahl
e79b5cfb36 Merge pull request #22 from dyukorev/master
Pending transactions fix
2020-04-01 10:10:41 +02:00
Denis Dyukorev
9a40a8ea73 Add 'process_pending_transactions' function documentation 2020-03-31 17:22:07 +07:00
Denis Dyukorev
564df1e279 Rename function argument to match naming requirements. 2020-03-31 16:27:22 +07:00
Björn Ritzl
f1d53948e9 Merge pull request #20 from defold/Issue-18-crash-on-empty-product
Check if title or description is null
2020-03-09 12:33:48 +01:00
Björn Ritzl
607b4a117e Update iap_ios.mm 2020-03-09 11:57:14 +01:00
Björn Ritzl
38656e973c Check if title or description is null
Fixes #18
2020-03-09 11:46:01 +01:00
Björn Ritzl
4efe0c2a25 Merge pull request #19 from defold/Issue-3-handle-missing-google-play-store
Handle missing google play store
2020-03-09 11:43:28 +01:00
Björn Ritzl
3fb09c3b95 Missing import 2020-03-09 11:22:54 +01:00
Björn Ritzl
2843080690 Check that the Google Play Store exists on device
Fixes #3
2020-03-09 11:22:41 +01:00
Björn Ritzl
1c017cf4ed Merge pull request #17 from Filazapovich/master
iOS: Fix localized numbers in transaction date for some Arab countries
2020-03-05 06:29:00 +01:00
Denis Dyukorev
cbc1f659c1 Add transactions handler to init method according ios documentation it's important to add handler on application:didFinishLaunchingWithOptions: (https://developer.apple.com/documentation/storekit/in-app_purchase/setting_up_the_transaction_observer_for_the_payment_queue?language=objc), move observable commands to separate queue so it can be processed separately and after listener setup, add IAP_ProcessPendingTransactions function to resolve pending transactions on iOS. 2020-03-04 17:26:52 +03:00
Filazopovich Gennady
eda78dda4f Fix incorrect numbers in transaction date for some Arab countries 2020-03-04 15:39:30 +03:00
Denis Dyukorev
d6cc6f55f9 IAP command pointer to products list function to avoid crash on multiple calls products list 2020-02-15 15:00:15 +03:00
Björn Ritzl
8d9ea79d7d Fixed crash if buying when no listener was set 2020-02-10 08:25:48 +01:00
Björn Ritzl
c8d2d4e0d8 Merge branch 'master' of https://github.com/defold/extension-iap 2020-02-10 08:00:59 +01:00
Björn Ritzl
d2e2a640df Updated the example 2020-02-10 08:00:56 +01:00
Björn Ritzl
aa9448b5e9 Update index.md 2020-02-06 07:35:00 +01:00
Björn Ritzl
ce26b8fdd9 Merge pull request #15 from defold/crash_issue_fix
Fix crash related to lua stack
2020-01-17 06:20:18 +01:00
Alexey Gulev
1a778d0e98 fix crash 2020-01-16 23:16:21 +01:00
13 changed files with 938 additions and 296 deletions

View File

@@ -161,6 +161,13 @@
desc: value is `true` if current store supports handling
restored transactions, otherwise `false`.
#*****************************************************************************************************
- name: process_pending_transactions
type: function
desc: Process transactions still unprocessed from previous session if any. Transactions will be
processed with callback function set with `set_listener` function
#*****************************************************************************************************
- name: set_listener

View File

@@ -11,7 +11,7 @@ To use this library in your Defold project, add the following URL to your <code
https://github.com/defold/extension-iap/archive/master.zip
We recommend using a link to a zip file of a [https://github.com/defold/extension-iap/releases](specific release).
We recommend using a link to a zip file of a [specific release](https://github.com/defold/extension-iap/releases).
## Source code

View File

@@ -34,7 +34,6 @@ struct IAP
bool m_autoFinishTransactions;
int m_ProviderId;
dmScript::LuaCallbackInfo* m_ProductCallback;
dmScript::LuaCallbackInfo* m_Listener;
jobject m_IAP;
@@ -51,6 +50,12 @@ struct IAP
static IAP g_IAP;
static int IAP_ProcessPendingTransactions(lua_State* L)
{
//todo handle pending transactions if there is such thing on Android
return 0;
}
static int IAP_List(lua_State* L)
{
int top = lua_gettop(L);
@@ -61,14 +66,13 @@ static int IAP_List(lua_State* L)
return 0;
}
if (g_IAP.m_ProductCallback)
dmScript::DestroyCallback(g_IAP.m_ProductCallback);
g_IAP.m_ProductCallback = dmScript::CreateCallback(L, 2);
JNIEnv* env = Attach();
IAPCommand* cmd = new IAPCommand;
cmd->m_Callback = dmScript::CreateCallback(L, 2);
cmd->m_Command = IAP_PRODUCT_RESULT;
jstring products = env->NewStringUTF(buf);
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_List, products, g_IAP.m_IAPJNI);
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_List, products, g_IAP.m_IAPJNI, (jlong)cmd);
env->DeleteLocalRef(products);
Detach();
@@ -189,6 +193,7 @@ static const luaL_reg IAP_methods[] =
{"restore", IAP_Restore},
{"set_listener", IAP_SetListener},
{"get_provider_id", IAP_GetProviderId},
{"process_pending_transactions", IAP_ProcessPendingTransactions},
{0, 0}
};
@@ -198,7 +203,7 @@ extern "C" {
#endif
JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onProductsResult__ILjava_lang_String_2(JNIEnv* env, jobject, jint responseCode, jstring productList)
JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onProductsResult(JNIEnv* env, jobject, jint responseCode, jstring productList, jlong cmdHandle)
{
const char* pl = 0;
if (productList)
@@ -206,16 +211,14 @@ JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onProductsResult__ILjava_lang_
pl = env->GetStringUTFChars(productList, 0);
}
IAPCommand cmd;
cmd.m_Callback = g_IAP.m_ProductCallback;
cmd.m_Command = IAP_PRODUCT_RESULT;
cmd.m_ResponseCode = responseCode;
IAPCommand* cmd = (IAPCommand*)cmdHandle;
cmd->m_ResponseCode = responseCode;
if (pl)
{
cmd.m_Data = strdup(pl);
cmd->m_Data = strdup(pl);
env->ReleaseStringUTFChars(productList, pl);
}
IAP_Queue_Push(&g_IAP.m_CommandQueue, &cmd);
IAP_Queue_Push(&g_IAP.m_CommandQueue, cmd);
}
JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onPurchaseResult__ILjava_lang_String_2(JNIEnv* env, jobject, jint responseCode, jstring purchaseData)
@@ -244,6 +247,12 @@ JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onPurchaseResult__ILjava_lang_
static void HandleProductResult(const IAPCommand* cmd)
{
if (cmd->m_Callback == 0)
{
dmLogWarning("Received product list but no listener was set!");
return;
}
lua_State* L = dmScript::GetCallbackLuaContext(cmd->m_Callback);
int top = lua_gettop(L);
@@ -277,22 +286,22 @@ static void HandleProductResult(const IAPCommand* cmd)
IAP_PushError(L, "failed to fetch product", REASON_UNSPECIFIED);
}
int ret = lua_pcall(L, 3, 0, 0);
if (ret != 0) {
dmLogError("Error running callback: %s", lua_tostring(L, -1));
lua_pop(L, 1);
}
dmScript::PCall(L, 3, 0);
dmScript::TeardownCallback(cmd->m_Callback);
dmScript::DestroyCallback(cmd->m_Callback);
assert(g_IAP.m_ProductCallback == cmd->m_Callback);
g_IAP.m_ProductCallback = 0;
assert(top == lua_gettop(L));
}
static void HandlePurchaseResult(const IAPCommand* cmd)
{
if (cmd->m_Callback == 0)
{
dmLogWarning("Received purchase result but no listener was set!");
return;
}
lua_State* L = dmScript::GetCallbackLuaContext(cmd->m_Callback);
int top = lua_gettop(L);
@@ -335,11 +344,7 @@ static void HandlePurchaseResult(const IAPCommand* cmd)
IAP_PushError(L, "failed to buy product", REASON_UNSPECIFIED);
}
int ret = dmScript::PCall(L, 3, 0);
if (ret != 0) {
dmLogError("Error running callback: %s", lua_tostring(L, -1));
lua_pop(L, 1);
}
dmScript::PCall(L, 3, 0);
dmScript::TeardownCallback(cmd->m_Callback);
@@ -384,7 +389,7 @@ static dmExtension::Result InitializeIAP(dmExtension::Params* params)
jclass iap_jni_class = (jclass)env->CallObjectMethod(cls, find_class, str_class_name);
env->DeleteLocalRef(str_class_name);
g_IAP.m_List = env->GetMethodID(iap_class, "listItems", "(Ljava/lang/String;Lcom/defold/iap/IListProductsListener;)V");
g_IAP.m_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");

View File

@@ -71,13 +71,17 @@ static void IAPList_Callback(void* luacallback, const char* result_json)
IAP_PushError(L, "Got empty list result.", REASON_UNSPECIFIED);
}
int ret = dmScript::PCall(L, 3, 0);
(void)ret;
dmScript::PCall(L, 3, 0);
dmScript::DestroyCallback(callback);
dmScript::TeardownCallback(callback);
}
static int IAP_ProcessPendingTransactions(lua_State* L)
{
return 0;
}
static int IAP_List(lua_State* L)
{
DM_LUA_STACK_CHECK(L, 0);
@@ -144,8 +148,7 @@ static void IAPListener_Callback(void* luacallback, const char* result_json, int
}
}
int ret = dmScript::PCall(L, 3, 0);
(void)ret;
dmScript::PCall(L, 3, 0);
dmScript::TeardownCallback(callback);
}
@@ -212,6 +215,7 @@ static const luaL_reg IAP_methods[] =
{"restore", IAP_Restore},
{"set_listener", IAP_SetListener},
{"get_provider_id", IAP_GetProviderId},
{"process_pending_transactions", IAP_ProcessPendingTransactions},
{0, 0}
};

View File

@@ -29,6 +29,7 @@ struct IAP
NSMutableDictionary* m_PendingTransactions;
dmScript::LuaCallbackInfo* m_Listener;
IAPCommandQueue m_CommandQueue;
IAPCommandQueue m_ObservableQueue;
SKPaymentTransactionObserver* m_Observer;
};
@@ -119,8 +120,20 @@ static void IAP_FreeTransaction(IAPTransaction* transaction)
IAPProduct product = {0};
product.ident = strdup([p.productIdentifier UTF8String]);
product.title = strdup([p.localizedTitle UTF8String]);
product.description = strdup([p.localizedDescription UTF8String]);
if (p.localizedTitle) {
product.title = strdup([p.localizedTitle UTF8String]);
}
else {
dmLogWarning("Product %s has no localizedTitle", [p.productIdentifier UTF8String]);
product.title = "";
}
if (p.localizedDescription) {
product.description = strdup([p.localizedDescription UTF8String]);
}
else {
dmLogWarning("Product %s has no localizedDescription", [p.productIdentifier UTF8String]);
product.description = "";
}
product.currency_code = strdup([[p.priceLocale objectForKey:NSLocaleCurrencyCode] UTF8String]);
product.price = p.price.floatValue;
@@ -186,11 +199,7 @@ static void HandleProductResult(IAPCommand* cmd)
}
lua_pushnil(L);
int ret = dmScript::PCall(L, 3, 0);
if (ret != 0) {
dmLogError("%d: Error running callback: %s", __LINE__, lua_tostring(L, -1));
lua_pop(L, 1);
}
dmScript::PCall(L, 3, 0);
dmScript::TeardownCallback(cmd->m_Callback);
dmScript::DestroyCallback(cmd->m_Callback);
@@ -237,6 +246,7 @@ static void CopyTransaction(SKPaymentTransaction* transaction, IAPTransaction* o
}
NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
[dateFormatter setLocale: [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
[dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZ"];
out->ident = strdup([transaction.payment.productIdentifier UTF8String]);
@@ -318,11 +328,7 @@ static void HandlePurchaseResult(IAPCommand* cmd)
lua_pushnil(L);
}
int ret = dmScript::PCall(L, 3, 0);
if (ret != 0) {
dmLogError("%d: Error running callback: %s", __LINE__, lua_tostring(L, -1));
lua_pop(L, 1);
}
dmScript::PCall(L, 3, 0);
dmScript::TeardownCallback(cmd->m_Callback);
@@ -332,28 +338,25 @@ static void HandlePurchaseResult(IAPCommand* cmd)
assert(top == lua_gettop(L));
}
@implementation SKPaymentTransactionObserver
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction* transaction in transactions) {
if ((!self.m_IAP->m_AutoFinishTransactions) && (transaction.transactionState == SKPaymentTransactionStatePurchased)) {
static void processTransactions(IAP* iap, NSArray* transactions) {
for (SKPaymentTransaction* transaction in transactions) {
if ((!iap->m_AutoFinishTransactions) && (transaction.transactionState == SKPaymentTransactionStatePurchased)) {
NSData *data = [transaction.transactionIdentifier dataUsingEncoding:NSUTF8StringEncoding];
uint64_t trans_id_hash = dmHashBuffer64((const char*) [data bytes], [data length]);
[self.m_IAP->m_PendingTransactions setObject:transaction forKey:[NSNumber numberWithInteger:trans_id_hash] ];
[iap->m_PendingTransactions setObject:transaction forKey:[NSNumber numberWithInteger:trans_id_hash] ];
}
if (!self.m_IAP->m_Listener)
if (!iap->m_Listener)
continue;
IAPTransaction* iap_transaction = new IAPTransaction;
CopyTransaction(transaction, iap_transaction);
IAPCommand cmd;
cmd.m_Callback = self.m_IAP->m_Listener;
cmd.m_Callback = iap->m_Listener;
cmd.m_Command = IAP_PURCHASE_RESULT;
cmd.m_Data = iap_transaction;
IAP_Queue_Push(&self.m_IAP->m_CommandQueue, &cmd);
IAP_Queue_Push(&iap->m_ObservableQueue, &cmd);
switch (transaction.transactionState)
{
@@ -371,7 +374,19 @@ static void HandlePurchaseResult(IAPCommand* cmd)
default:
break;
}
}
}
}
static int IAP_ProcessPendingTransactions(lua_State* L)
{
processTransactions(&g_IAP, [SKPaymentQueue defaultQueue].transactions);
return 0;
}
@implementation SKPaymentTransactionObserver
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
processTransactions(self.m_IAP, transactions);
}
@end
@@ -485,18 +500,6 @@ static int IAP_SetListener(lua_State* L)
dmScript::DestroyCallback(iap->m_Listener);
iap->m_Listener = dmScript::CreateCallback(L, 1);
if (g_IAP.m_Observer == 0) {
SKPaymentTransactionObserver* observer = [[SKPaymentTransactionObserver alloc] init];
observer.m_IAP = &g_IAP;
// NOTE: We add the listener *after* a lua listener is set
// The payment queue is persistent and "old" transaction might be processed
// from previous session. We call "finishTransaction" when appropriate
// for all transaction and we must ensure that the result is delivered to lua.
[[SKPaymentQueue defaultQueue] addTransactionObserver: observer];
g_IAP.m_Observer = observer;
}
return 0;
}
@@ -514,6 +517,7 @@ static const luaL_reg IAP_methods[] =
{"restore", IAP_Restore},
{"set_listener", IAP_SetListener},
{"get_provider_id", IAP_GetProviderId},
{"process_pending_transactions", IAP_ProcessPendingTransactions},
{0, 0}
};
@@ -528,6 +532,7 @@ static dmExtension::Result InitializeIAP(dmExtension::Params* params)
g_IAP.m_InitCount++;
IAP_Queue_Create(&g_IAP.m_CommandQueue);
IAP_Queue_Create(&g_IAP.m_ObservableQueue);
lua_State*L = params->m_L;
int top = lua_gettop(L);
@@ -543,6 +548,12 @@ static dmExtension::Result InitializeIAP(dmExtension::Params* params)
lua_pop(L, 1);
assert(top == lua_gettop(L));
SKPaymentTransactionObserver* observer = [[SKPaymentTransactionObserver alloc] init];
observer.m_IAP = &g_IAP;
[[SKPaymentQueue defaultQueue] addTransactionObserver: observer];
g_IAP.m_Observer = observer;
return dmExtension::RESULT_OK;
}
@@ -565,6 +576,9 @@ static void IAP_OnCommand(IAPCommand* cmd, void*)
static dmExtension::Result UpdateIAP(dmExtension::Params* params)
{
IAP_Queue_Flush(&g_IAP.m_CommandQueue, IAP_OnCommand, 0);
if (g_IAP.m_Observer) {
IAP_Queue_Flush(&g_IAP.m_ObservableQueue, IAP_OnCommand, 0);
}
return dmExtension::RESULT_OK;
}
@@ -593,6 +607,7 @@ static dmExtension::Result FinalizeIAP(dmExtension::Params* params)
}
IAP_Queue_Destroy(&g_IAP.m_CommandQueue);
IAP_Queue_Destroy(&g_IAP.m_ObservableQueue);
return dmExtension::RESULT_OK;
}

View File

@@ -1,5 +1,5 @@
package com.defold.iap;
public interface IListProductsListener {
public void onProductsResult(int resultCode, String productList);
public void onProductsResult(int resultCode, String productList, long cmdHandle);
}

View File

@@ -35,6 +35,7 @@ public class IapAmazon implements PurchasingListener {
public static final String TAG = "iap";
private HashMap<RequestId, IListProductsListener> listProductsListeners;
private HashMap<RequestId, Long> listProductsCommandPtrs;
private HashMap<RequestId, IPurchaseListener> purchaseListeners;
private Activity activity;
@@ -54,7 +55,7 @@ public class IapAmazon implements PurchasingListener {
public void stop() {
}
public void listItems(final String skus, final IListProductsListener listener) {
public void listItems(final String skus, final IListProductsListener listener, final long commandPtr) {
final Set<String> skuSet = new HashSet<String>();
for (String x : skus.split(",")) {
if (x.trim().length() > 0) {
@@ -71,6 +72,7 @@ public class IapAmazon implements PurchasingListener {
RequestId req = PurchasingService.getProductData(skuSet);
if (req != null) {
listProductsListeners.put(req, listener);
listProductsCommandPtrs.put(req, commandPtr);
} else {
Log.e(TAG, "Did not expect a null requestId");
}
@@ -150,17 +152,21 @@ public class IapAmazon implements PurchasingListener {
public void onProductDataResponse(ProductDataResponse productDataResponse) {
RequestId reqId = productDataResponse.getRequestId();
IListProductsListener listener;
long commadPtr = 0;
synchronized (this.listProductsListeners) {
listener = this.listProductsListeners.get(reqId);
commadPtr = this.listProductsCommandPtrs.get(reqId);
this.listProductsListeners.remove(reqId);
this.listProductsCommandPtrs.remove(reqId);
if (listener == null) {
Log.e(TAG, "No listener found for request " + reqId.toString());
return;
}
this.listProductsListeners.remove(reqId);
}
if (productDataResponse.getRequestStatus() != ProductDataResponse.RequestStatus.SUCCESSFUL) {
listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null, commadPtr);
} else {
Map<String, Product> products = productDataResponse.getProductData();
try {
@@ -180,9 +186,9 @@ public class IapAmazon implements PurchasingListener {
}
data.put(key, item);
}
listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_OK, data.toString());
listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_OK, data.toString(), commadPtr);
} catch (JSONException e) {
listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null, commadPtr);
}
}
}

View File

@@ -17,6 +17,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.ResolveInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -67,7 +68,7 @@ public class IapGooglePlay implements Handler.Callback {
private boolean autoFinishTransactions;
private static interface ISkuRequestListener {
public void onProducts(int resultCode, JSONObject products);
public void onProducts(int resultCode, JSONObject products);
}
private static class SkuRequest {
@@ -155,10 +156,24 @@ public class IapGooglePlay implements Handler.Callback {
this.autoFinishTransactions = autoFinishTransactions;
}
private static boolean isPlayStoreInstalled(Context context){
try {
context.getPackageManager().getPackageInfo("com.android.vending", 0);
return true;
} catch (PackageManager.NameNotFoundException e) {
return false;
}
}
private void init() {
// NOTE: We must create Handler lazily as construction of
// handlers must be in the context of a "looper" on Android
if (!isPlayStoreInstalled(activity)) {
Log.e(TAG, "Unable to find Google Play Store (com.android.vending)");
return;
}
if (this.initialized)
return;
@@ -238,7 +253,7 @@ public class IapGooglePlay implements Handler.Callback {
});
}
public void listItems(final String skus, final IListProductsListener listener) {
public void listItems(final String skus, final IListProductsListener listener, final long commandPtr) {
ArrayList<String> skuList = new ArrayList<String>();
for (String x : skus.split(",")) {
if (x.trim().length() > 0) {
@@ -261,15 +276,15 @@ public class IapGooglePlay implements Handler.Callback {
products.put(key, convertProduct(product));
}
}
listener.onProductsResult(resultCode, products.toString());
listener.onProductsResult(resultCode, products.toString(), commandPtr);
}
catch(JSONException e) {
Log.wtf(TAG, "Failed to convert products", e);
listener.onProductsResult(resultCode, null);
listener.onProductsResult(resultCode, null, commandPtr);
}
}
else {
listener.onProductsResult(resultCode, null);
listener.onProductsResult(resultCode, null, commandPtr);
}
}
}));

View File

@@ -69,8 +69,8 @@ public class IapGooglePlayActivity extends Activity {
}
private void buy(String product, String productType) {
// Flush any pending items, in order to be able to buy the same (new) product again
processPendingConsumables();
// Flush any pending items, in order to be able to buy the same (new) product again
processPendingConsumables();
try {
Bundle buyIntentBundle = service.getBuyIntent(3, getPackageName(), product, productType, "");

View File

@@ -23,7 +23,7 @@ public class IapJNI implements IListProductsListener, IPurchaseListener {
}
@Override
public native void onProductsResult(int responseCode, String productList);
public native void onProductsResult(int responseCode, String productList, long cmdHandle);
@Override
public native void onPurchaseResult(int responseCode, String purchaseData);

View File

@@ -5,13 +5,13 @@ main_collection = /main/main.collectionc
shared_state = 1
[display]
width = 960
height = 640
width = 640
height = 1136
[android]
input_method = HiddenInputField
package = com.defold.iaprtestapp
version_code = 3
package = com.defold.extension.iap
version_code = 5
[project]
title = extension-iap
@@ -21,8 +21,11 @@ dependencies = https://github.com/andsve/dirtylarry/archive/master.zip
include_dirs = extension-iap
[ios]
bundle_identifier = com.defoldextension.push
bundle_identifier = com.defold.extension.iap
[iap]
auto_finish_transactions = 0
[osx]
bundle_identifier = com.defold.extension.iap

View File

@@ -41,7 +41,7 @@ nodes {
w: 1.0
}
type: TYPE_TEMPLATE
id: "consumable"
id: "goldbars_small"
layer: ""
inherit_alpha: true
alpha: 1.0
@@ -82,12 +82,12 @@ nodes {
type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA
texture: "button/button_normal"
id: "consumable/larrybutton"
id: "goldbars_small/larrybutton"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
adjust_mode: ADJUST_MODE_FIT
parent: "consumable"
parent: "goldbars_small"
layer: ""
inherit_alpha: true
slice9 {
@@ -129,16 +129,16 @@ nodes {
w: 1.0
}
color {
x: 0.6
y: 0.0
z: 0.0
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_TEXT
blend_mode: BLEND_MODE_ALPHA
text: "consumable"
text: "Goldbars - S"
font: "larryfont"
id: "consumable/larrylabel"
id: "goldbars_small/larrylabel"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
@@ -156,172 +156,12 @@ nodes {
}
adjust_mode: ADJUST_MODE_FIT
line_break: false
parent: "consumable/larrybutton"
parent: "goldbars_small/larrybutton"
layer: ""
inherit_alpha: true
alpha: 1.0
outline_alpha: 1.0
shadow_alpha: 1.0
overridden_fields: 5
overridden_fields: 8
template_node_child: true
text_leading: 1.0
text_tracking: 0.0
}
nodes {
position {
x: 171.0
y: 449.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
size {
x: 200.0
y: 100.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_TEMPLATE
id: "nonconsumable"
layer: ""
inherit_alpha: true
alpha: 1.0
template: "/dirtylarry/button.gui"
template_node_child: false
}
nodes {
position {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
size {
x: 300.0
y: 88.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA
texture: "button/button_normal"
id: "nonconsumable/larrybutton"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
adjust_mode: ADJUST_MODE_FIT
parent: "nonconsumable"
layer: ""
inherit_alpha: true
slice9 {
x: 32.0
y: 32.0
z: 32.0
w: 32.0
}
clipping_mode: CLIPPING_MODE_NONE
clipping_visible: true
clipping_inverted: false
alpha: 1.0
template_node_child: true
size_mode: SIZE_MODE_MANUAL
}
nodes {
position {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
size {
x: 200.0
y: 100.0
z: 0.0
w: 1.0
}
color {
x: 0.6
y: 0.0
z: 0.0
w: 1.0
}
type: TYPE_TEXT
blend_mode: BLEND_MODE_ALPHA
text: "non-consumable"
font: "larryfont"
id: "nonconsumable/larrylabel"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
outline {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
adjust_mode: ADJUST_MODE_FIT
line_break: false
parent: "nonconsumable/larrybutton"
layer: ""
inherit_alpha: true
alpha: 1.0
outline_alpha: 1.0
shadow_alpha: 1.0
overridden_fields: 5
overridden_fields: 8
template_node_child: true
text_leading: 1.0
@@ -330,7 +170,7 @@ nodes {
nodes {
position {
x: 485.0
y: 449.0
y: 56.0
z: 0.0
w: 1.0
}
@@ -447,14 +287,14 @@ nodes {
w: 1.0
}
color {
x: 0.6
y: 0.0
z: 0.0
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_TEXT
blend_mode: BLEND_MODE_ALPHA
text: "<- reset"
text: "Reset"
font: "larryfont"
id: "reset/larrylabel"
xanchor: XANCHOR_NONE
@@ -480,7 +320,700 @@ nodes {
alpha: 1.0
outline_alpha: 1.0
shadow_alpha: 1.0
overridden_fields: 5
overridden_fields: 8
template_node_child: true
text_leading: 1.0
text_tracking: 0.0
}
nodes {
position {
x: 20.0
y: 1124.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
size {
x: 600.0
y: 450.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_TEXT
blend_mode: BLEND_MODE_ALPHA
text: "<text>"
font: "system_font"
id: "log"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_NW
outline {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
adjust_mode: ADJUST_MODE_FIT
line_break: true
layer: ""
inherit_alpha: true
alpha: 1.0
outline_alpha: 1.0
shadow_alpha: 1.0
template_node_child: false
text_leading: 1.0
text_tracking: 0.0
}
nodes {
position {
x: 171.0
y: 465.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
size {
x: 200.0
y: 100.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_TEMPLATE
id: "goldbars_medium"
layer: ""
inherit_alpha: true
alpha: 1.0
template: "/dirtylarry/button.gui"
template_node_child: false
}
nodes {
position {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
size {
x: 300.0
y: 88.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA
texture: "button/button_normal"
id: "goldbars_medium/larrybutton"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
adjust_mode: ADJUST_MODE_FIT
parent: "goldbars_medium"
layer: ""
inherit_alpha: true
slice9 {
x: 32.0
y: 32.0
z: 32.0
w: 32.0
}
clipping_mode: CLIPPING_MODE_NONE
clipping_visible: true
clipping_inverted: false
alpha: 1.0
template_node_child: true
size_mode: SIZE_MODE_MANUAL
}
nodes {
position {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
size {
x: 200.0
y: 100.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_TEXT
blend_mode: BLEND_MODE_ALPHA
text: "Goldbars - M"
font: "larryfont"
id: "goldbars_medium/larrylabel"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
outline {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
adjust_mode: ADJUST_MODE_FIT
line_break: false
parent: "goldbars_medium/larrybutton"
layer: ""
inherit_alpha: true
alpha: 1.0
outline_alpha: 1.0
shadow_alpha: 1.0
overridden_fields: 8
template_node_child: true
text_leading: 1.0
text_tracking: 0.0
}
nodes {
position {
x: 171.0
y: 357.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
size {
x: 200.0
y: 100.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_TEMPLATE
id: "goldbars_large"
layer: ""
inherit_alpha: true
alpha: 1.0
template: "/dirtylarry/button.gui"
template_node_child: false
}
nodes {
position {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
size {
x: 300.0
y: 88.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA
texture: "button/button_normal"
id: "goldbars_large/larrybutton"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
adjust_mode: ADJUST_MODE_FIT
parent: "goldbars_large"
layer: ""
inherit_alpha: true
slice9 {
x: 32.0
y: 32.0
z: 32.0
w: 32.0
}
clipping_mode: CLIPPING_MODE_NONE
clipping_visible: true
clipping_inverted: false
alpha: 1.0
template_node_child: true
size_mode: SIZE_MODE_MANUAL
}
nodes {
position {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
size {
x: 200.0
y: 100.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_TEXT
blend_mode: BLEND_MODE_ALPHA
text: "Goldbars - L"
font: "larryfont"
id: "goldbars_large/larrylabel"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
outline {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
adjust_mode: ADJUST_MODE_FIT
line_break: false
parent: "goldbars_large/larrybutton"
layer: ""
inherit_alpha: true
alpha: 1.0
outline_alpha: 1.0
shadow_alpha: 1.0
overridden_fields: 8
template_node_child: true
text_leading: 1.0
text_tracking: 0.0
}
nodes {
position {
x: 159.0
y: 56.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
size {
x: 200.0
y: 100.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_TEMPLATE
id: "list"
layer: ""
inherit_alpha: true
alpha: 1.0
template: "/dirtylarry/button.gui"
template_node_child: false
}
nodes {
position {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
size {
x: 300.0
y: 88.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA
texture: "button/button_normal"
id: "list/larrybutton"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
adjust_mode: ADJUST_MODE_FIT
parent: "list"
layer: ""
inherit_alpha: true
slice9 {
x: 32.0
y: 32.0
z: 32.0
w: 32.0
}
clipping_mode: CLIPPING_MODE_NONE
clipping_visible: true
clipping_inverted: false
alpha: 1.0
template_node_child: true
size_mode: SIZE_MODE_MANUAL
}
nodes {
position {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
size {
x: 200.0
y: 100.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_TEXT
blend_mode: BLEND_MODE_ALPHA
text: "List"
font: "larryfont"
id: "list/larrylabel"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
outline {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
adjust_mode: ADJUST_MODE_FIT
line_break: false
parent: "list/larrybutton"
layer: ""
inherit_alpha: true
alpha: 1.0
outline_alpha: 1.0
shadow_alpha: 1.0
overridden_fields: 8
template_node_child: true
text_leading: 1.0
text_tracking: 0.0
}
nodes {
position {
x: 171.0
y: 251.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
size {
x: 200.0
y: 100.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_TEMPLATE
id: "subscription"
layer: ""
inherit_alpha: true
alpha: 1.0
template: "/dirtylarry/button.gui"
template_node_child: false
}
nodes {
position {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
size {
x: 300.0
y: 88.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA
texture: "button/button_normal"
id: "subscription/larrybutton"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
adjust_mode: ADJUST_MODE_FIT
parent: "subscription"
layer: ""
inherit_alpha: true
slice9 {
x: 32.0
y: 32.0
z: 32.0
w: 32.0
}
clipping_mode: CLIPPING_MODE_NONE
clipping_visible: true
clipping_inverted: false
alpha: 1.0
template_node_child: true
size_mode: SIZE_MODE_MANUAL
}
nodes {
position {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
size {
x: 200.0
y: 100.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_TEXT
blend_mode: BLEND_MODE_ALPHA
text: "Subscription"
font: "larryfont"
id: "subscription/larrylabel"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
outline {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
adjust_mode: ADJUST_MODE_FIT
line_break: false
parent: "subscription/larrybutton"
layer: ""
inherit_alpha: true
alpha: 1.0
outline_alpha: 1.0
shadow_alpha: 1.0
overridden_fields: 8
template_node_child: true
text_leading: 1.0

View File

@@ -1,59 +1,113 @@
local dirtylarry = require "dirtylarry/dirtylarry"
local GOLDBARS_SMALL = "com.defold.iap.goldbar.small"
local GOLDBARS_MEDIUM = "com.defold.iap.goldbar.medium"
local GOLDBARS_LARGE = "com.defold.iap.goldbar.large"
local SUBSCRIPTION = "com.defold.iap.subscription"
local items = {
"consumable",
"nonconsumable"
GOLDBARS_SMALL,
GOLDBARS_MEDIUM,
GOLDBARS_LARGE,
SUBSCRIPTION,
}
local product_items = {}
-- mapping between product id and button name
local item_buttons = {
[GOLDBARS_SMALL] = "goldbars_small",
[GOLDBARS_MEDIUM] = "goldbars_medium",
[GOLDBARS_LARGE] = "goldbars_large",
[SUBSCRIPTION] = "subscription",
}
local function list_callback(self, products, error)
if error == nil then
for k,p in pairs(products) do
product_items[p.ident] = p
local name = p.ident.."/larrylabel"
gui.set_color(gui.get_node(name), vmath.vector4(1,1,1,1))
end
else
print(error.error)
local available_items = {}
local LOG = {}
local function log(fmt, ...)
if not fmt then return end
local line = fmt:format(...)
print(line)
table.insert(LOG, line)
if #LOG > 10 then
table.remove(LOG, 1)
end
local s = table.concat(LOG, "\n")
gui.set_text(gui.get_node("log"), s)
end
local function buy(id)
log("iap.buy() " .. id)
iap.buy(id)
end
local function list()
log("iap.list()")
for item, button in pairs(item_buttons) do
gui.set_color(gui.get_node(button.."/larrylabel"), vmath.vector4(1,1,1,0.5))
end
iap.list(items, function(self, products, error)
if error then
log(error.error)
return
end
for k,p in pairs(products) do
available_items[p.ident] = p
log("Item %s", p.ident)
local button = item_buttons[p.ident]
if button then
gui.set_color(gui.get_node(button.."/larrylabel"), vmath.vector4(1,1,1,1))
else
log("Unable to find button for %s", tostring(p.ident))
end
end
end)
end
local function buy_listener(self, transaction, error)
pprint(transaction, error)
if not error and iap.get_provider_id() == iap.PROVIDER_ID_GOOGLE and transaction.ident == "nonconsumable" then
local name = "reset/larrylabel"
gui.set_color(gui.get_node(name), vmath.vector4(1,1,1,1))
if error then
log("iap.buy() error %s - %s", tostring(error.error), tostring(error.reason))
return
end
if iap.get_provider_id() == iap.PROVIDER_ID_GOOGLE and transaction.ident == NON_CONSUMABLE then
log("iap.buy() ok - google")
gui.set_color(gui.get_node("reset/larrylabel"), vmath.vector4(1,1,1,1))
product_items["reset"] = transaction
elseif not error then
else
log("iap.buy() ok")
log("iap.finish()")
iap.finish(transaction)
end
end
function init(self)
self.log = {}
log("init()")
msg.post(".", "acquire_input_focus")
iap.list(items, list_callback)
if not iap then
log("In-App Purchases not supported")
return
end
list()
iap.set_listener(buy_listener)
end
function on_input(self, action_id, action)
if product_items["consumable"] then
dirtylarry:button("consumable", action_id, action, function()
iap.buy("consumable")
if action_id then
for item, button in pairs(item_buttons) do
if available_items[item] then
dirtylarry:button(button, action_id, action, function()
buy(item)
end)
end
end
dirtylarry:button("list", action_id, action, function()
list()
end)
end
if product_items["nonconsumable"] then
dirtylarry:button("nonconsumable", action_id, action, function()
iap.buy("nonconsumable")
end)
end
if product_items["reset"] then
dirtylarry:button("reset", action_id, action, function()
iap.finish(product_items["reset"])
product_items["reset"] = nil
gui.set_color(gui.get_node("reset/larrylabel"), vmath.vector4(1,0,0,1))
end)
end
end
end