• Hey Guest! Ever feel like entering a Game Jam, but the time limit is always too much pressure? We get it... You lead a hectic life and dedicating 3 whole days to make a game just doesn't work for you! So, why not enter the GMC SLOW JAM? Take your time! Kick back and make your game over 4 months! Interested? Then just click here!

Android Only able to purchase consumable IAP once

Warspite2

Member
Ok im lost here. I have 3 consumable iap in my android game to purchase coins. These coins can be purchased in a room at the end of each level. Once I complete the first level I can purchase each of them but...only once. At the end of other levels I can't purchase them anymore. If I restart the game, I can purchase them again, but only once. I am using the test purchase for google play that always approves and doesn't charge. If it were to charge, I would be broke by now from testing the the past 9 hours. So I have a single persistant object to handle all my iap, push notifications, and leaderboard stuff through the levels. I have nearly the same identical code in a different game without issues, which is nearly the same as in the latest yoyo examples. I can post some code in a spoiler if anyone is able to help? It's almost like it's treating the consumable iap as durable iap but I can't see a single thing wrong with the code. It seems like I have to make it so it refreshes and allows purchasing again in the same game without starting over.
 

Roderick

Member
Edit: Ignore that, I realize that you already looked at what I'd posted.

I've never done IAPs myself, but looking over the article covering them, you use different commands to handle consumable IAPs. Are you using GPBilling_ConsumeProduct() or GPBilling_AcknowledgePurchase()?
 
Last edited:

pipebkOT

Member
You need to consume the consumable IAP right after you bought it, if you don't consume it, you can't buy the same again.
 

Warspite2

Member
Thanks for the replies guys. That is what I am having issues with. It seems like it has something to do with consuming it. My code is literally a copy and paste of this yoyo example with only a couple added consumables...


What really confuses the life out of me is I have another game that uses this same example without issues. It works like a charm I tell you. I looked and compared the two over and over and didnt see issues in the code. Only difference is the game not working is using the a persistent object that handles all the iap between all rooms. Maybe somehow that is causing a problem. I wouldn't think it would be since the article mentions using the first object at game start or create. I use it at game start in the very first room. I will post some of my code in a spolier here in a little while when get back at my desk.
 

Warspite2

Member
So I figured out what the issue was, it was in the case gpb_iap_receipt but im still left with another issue. The other game which was working only used consumables, the game not working used both consumables (to purchase coins) and a durable (to remove ads). Check out this code...

///////////////////////////////////////////
case gpb_iap_receipt:
// Get the JSON object response string
var _json = async_load[? "response_json"];
var _map = json_decode(_json);
// Check the response to see if it succeeded
if _map[? "success"] == true
{
// Check the purchases key for any outstanding product purchases
if ds_map_exists(_map, "purchases")
{
// Loop through the purchases list and parse each
// entry to get the purchase data DS map
var _plist = ds_map_find_value(_map, "purchases");
for (var i = 0; i < ds_list_size(_plist); ++i ; )
{
var _pmap = _plist[| i];
var _ptoken = _pmap[? "purchaseToken"];
var _sig = GPBilling_Purchase_GetSignature(_ptoken);
var _pjson = GPBilling_Purchase_GetOriginalJson(_ptoken);
// Verify the purchase before consuming or acknowledging it
if GPBilling_Purchase_VerifySignature(_pjson, _sig)
{
GPBilling_ConsumeProduct(_ptoken);
// If it is a durable product then you'd call
// GPBilling_AcknowledgePurchase();
ds_list_add(global.CurrentTokens, _ptoken);
ds_list_add(global.CurrentProducts, _pmap[? "productId"]);
}
}
}
}
ds_map_destroy(_map);
break;
//////////////////////////////////////////

So this is in an async iap in my switch statement. Now what the problem was, notice above it says...

// If it is a durable produce then you'd call
// GPBilling_AcknowledgePurchase();

Since I had both types, I just added GPBilling_AcknowledgePurchase(); below the GPBilling_ConsumeProduct(_ptoken); in the code above, so basically I just removed the //. This was why the consumables were not working. This leads to my new issue now, how do I call the GPBilling_AcknowledgePurchase(); for the remove ads iap? I see nothing in that code that distinguishes between a consumable and durable. I guess I am not undertanding the way that snip of code is handling it. To sum up my question, in that above code how do I make it recognize if it's a consumable or durable? Maybe Mark Alexander or someone else here that knows can shed some light on this? Thanks and I know I am getting closer to getting this resolved. As long as this issue remains, I am losing revenue and it's holding up a fairly large update.
 
Last edited:

pipebkOT

Member
@Warspite2
you are right, the yoyoexample tutorial doesn't have code to distinguish between consumable and durable, I would put a switch statement in there like this

GML:
if GPBilling_Purchase_VerifySignature(_pjson, _sig)
{
var productid=_pmap[? "productId"]);
switch (productid)
{
case "coins":
GPBilling_ConsumeProduct(_ptoken);
ds_list_add(global.CurrentTokens, _ptoken);
ds_list_add(global.CurrentProducts, _pmap[? "productId"]);
break;
case "noads":
GPBilling_AcknowledgePurchase();
ds_list_add(global.CurrentTokens, _ptoken);
ds_list_add(global.CurrentProducts, _pmap[? "productId"]);
break;
  }
}
}
}
ds_map_destroy(_map);
break;
//////////////////////////////////////////
I'm just guessing cuz I don't know how iaps really works in gms2
 

Warspite2

Member
Thanks for the response! I was able to change up the code some to get it working. Your example looks great and i believe that would of worked too. I am going to post up what i added and after extensive testing i was able to 100% confirm it works. I have over 14 consumable iap in the game now. Thanks again.
 

Warspite2

Member
Maybe I talked to soon, my consumables work like a charm but I realized the non_consumable is not working which is a single iap to remove ads. I thought it was working until I tested it on a non testing device and realized the charge goes through, charges the customer but doesn't acknowledge it. I even went as far as creating two test iap products just to test this single thing. The thing is, I purchased this same remove ads iap in October of 19, and it certainly acknowledged it because recently on the testing device using the same account no ads are shown in game and the remove ads option is not there as I coded it to do. Back then we were not using the new GPBilling, we were still using the gms2 functions that are now depreciated such as iap_acquire, iap_consume, iap_purchased, etc. So obviously back then it worked and acknowledged durable iap without issue. I have searched these forums and find many threads regarding iap unanswered, some months old. It seems there is just little help regarding all of the iap stuff. I find this strange since sadly iap and proper monitization is critical for success on Google Play. Trust me when I say, if I post on here regarding something like this, that means I have totally exhausted my efforts troubleshooting and searching for answers so this is like my last hope. If I ever get all of this figured out I will make a good tutorial video to cover all of this and share on youtube. So now let me get to the code, notice that 2, 4 and 5 are the non-consumables, it's all in the spoiler below. If anyone can see anything wrong with why it will charge the non-consumable but not acknowledge it that would be great and much appreciated because I have been troubleshooting all this iap stuff since friday and can't see anything wrong here. Also note I left many of the yoyo comments in there for reference...

in the create event of the very first object in my game which is persistent...

ini_open( "savedata.ini" );
global.NoAds = ini_read_real( "save1", "no_ads", false );
global.test1 = ini_read_real( "save1", "test1", false);
global.test2 = false;
ini_close();

In a game start event of that game object...

global.IAP_Enabled = false;
global.IAP_PurchaseID[0] = "5000_coins";
global.IAP_PurchaseID[1] = "10000_coins";
global.IAP_PurchaseID[2] = "no_ads";
global.IAP_PurchaseID[3] = "12000_coins";
global.IAP_PurchaseID[4] = "iap_001";
global.IAP_PurchaseID[5] = "iap_002";
global.CurrentTokens = ds_list_create();
global.CurrentProducts = ds_list_create();
// Attempt to connect to the store
var _init = GPBilling_ConnectToStore();
if _init == gpb_error_unknown
{
show_debug_message("ERROR - Billing API Has Not Connected!");
alarm[0] = room_speed * 10;
}

In the async iap event of that same object...

/// @description Insert description here
// You can write your code in this editor
var _eventId = async_load[? "id"];
switch (_eventId)
{
case gpb_store_connect:
// Store has connected so here you would generally add the products
global.IAP_Enabled = false;
GPBilling_AddProduct(global.IAP_PurchaseID[0]);
GPBilling_AddProduct(global.IAP_PurchaseID[1]);
GPBilling_AddProduct(global.IAP_PurchaseID[2]);//non-consumable remove ads
GPBilling_AddProduct(global.IAP_PurchaseID[3]);
GPBilling_AddProduct(global.IAP_PurchaseID[4]);//test iap non-consumable
GPBilling_AddProduct(global.IAP_PurchaseID[5]);//test iap non-consumable
// Etc… for all products
GPBilling_QueryProducts();
// Here you would also add any subscription products
// using the function GooglePlayBilling_AddSubscription().
// However, you would NOT call the function GPBilling_QuerySubscriptions()
// here if you have already queried products, but instead query
// the subscription in the appropriate Async Event (see the "Querying
// Products" section, below)
break;
case gpb_store_connect_failed:
// Store has failed to connect, so try again periodically
alarm[0] = room_speed * 10;
break;
case gpb_product_data_response:
// Retrieve the JSON object response string
var _json = async_load[? "response_json"];
var _map = json_decode(_json);
// Check if the query was successful
if _map[? "success"] == true
{
// Get the DS list of products and loop through them
var _plist = _map[? "skuDetails"];
for (var i = 0; i < ds_list_size(_plist); ++i;
{
// The skuDetails key contains a DS list where
// each list entry corresponds to a single
// product in DS map form. This DS map can be parsed
// to extract details like title, description and
// price, as shown in the example, below:
var _pmap = _plist[| i];
var _num = 0;
while(_pmap[? "productId"] != global.IAP_PurchaseID[_num])
{
++_num;
}
global.IAP_ProductData[_num, 0] = _pmap[? "productId"];
global.IAP_ProductData[_num, 1] = _pmap[? "price"];
global.IAP_ProductData[_num, 2] = _pmap[? "title"];
global.IAP_ProductData[_num, 3] = _pmap[? "decription"];
}
// Call the query function for subscriptions here, if required:
// GPBilling_QuerySubscriptions();
// If not required then query purchase data (this would be done
// in the subscription product query if those have been queried.
// Basically, all queries must be made sequentially with the
// purchase queries being done last.
var purchase_json = GPBilling_QueryPurchases(gpb_purchase_skutype_inapp);
global.IAP_Enabled = true;
var _plist = _map[? "skuDetails"];
for (var i = 0; i < ds_list_size(_plist); ++i;
{
// Any code required to store query information goes here
}
var _purchase_json = GPBilling_QueryPurchases(gpb_purchase_skutype_inapp);
var _purchase_map = json_decode(_purchase_json);
if _purchase_map[? "success"] == true
{
var _list = _purchase_map[? "purchases"];
var _sz = ds_list_size(_list);
for (var i = 0; i < _sz; ++i;
{
var _map = _list[| i];
if _map[? "purchaseState"] == 0
{
// Purchase has been made, so now get the product ID
// and unique "token" string to identify the purchase
var _pid = _map[? "productId"];
var _token = _map[? "purchaseToken"];
var _add = false;
// Check against existing purchase IDs
if _pid == global.IAP_PurchaseID[0]
{
// It's a consumable purchase that hasn't been used yet
// so call the consume function on it:
GPBilling_ConsumeProduct(_token);
_add = true;
}
if _pid == global.IAP_PurchaseID[1]
{
// It's a consumable purchase that hasn't been used yet
// so call the consume function on it:
GPBilling_ConsumeProduct(_token);
_add = true;
}
if _pid == global.IAP_PurchaseID[3]
{
// It's a consumable purchase that hasn't been used yet
// so call the consume function on it:
GPBilling_ConsumeProduct(_token);
_add = true;
}

if _pid == global.IAP_PurchaseID[2]//non-consumable remove ads
{
// It's a non-consumable purchase so check and see
// if it's been acknowledged yet:
if _map[? "acknowledged"] == 0
{
// It hasn't been acknowledged, so do that now:
GPBilling_AcknowledgePurchase(_token);
_add = true;
}
else
{
// Purchase has been acknowledged so here you
// you would check it and set any variables,
// for example "global.NoAds".
global.NoAds = true;
}
}
if _pid == global.IAP_PurchaseID[4]//TEST non-consumable remove ads
{
// It's a non-consumable purchase so check and see
// if it's been acknowledged yet:
if _map[? "acknowledged"] == 0
{
// It hasn't been acknowledged, so do that now:
GPBilling_AcknowledgePurchase(_token);
_add = true;
}
else
{
// Purchase has been acknowledged so here you
// you would check it and set any variables,
// for example "global.NoAds".
global.test1 = true;
}
}
if _pid == global.IAP_PurchaseID[5]//TEST non-consumable remove ads
{
// It's a non-consumable purchase so check and see
// if it's been acknowledged yet:
if _map[? "acknowledged"] == 0
{
// It hasn't been acknowledged, so do that now:
GPBilling_AcknowledgePurchase(_token);
_add = true;
}
else
{
// Purchase has been acknowledged so here you
// you would check it and set any variables,
// for example "global.NoAds".
global.test2 = true;
}
}
if _add
{
// add all purchase IDs and tokens into the relevant
// DS lists so they can be confirmed later
ds_list_add(global.CurrentTokens, _token);
ds_list_add(global.CurrentProducts, _pid);
}
}
}
}
ds_map_destroy(_purchase_map);
}
ds_map_destroy(_map);
break;
case gpb_iap_receipt:
// Get the JSON object response string
var _json = async_load[? "response_json"];
var _map = json_decode(_json);
// Check the response to see if it succeeded
if _map[? "success"] == true
{
// Check the purchases key for any outstanding product purchases
if ds_map_exists(_map, "purchases")
{
// Loop through the purchases list and parse each
// entry to get the purchase data DS map
var _plist = ds_map_find_value(_map, "purchases");
for (var i = 0; i < ds_list_size(_plist); ++i;
{
var _pmap = _plist[| i];
var _ptoken = _pmap[? "purchaseToken"];
var _sig = GPBilling_Purchase_GetSignature(_ptoken);
var _pjson = GPBilling_Purchase_GetOriginalJson(_ptoken);
var _pid = _map[? "productId"];
// Verify the purchase before consuming or acknowledging it
if GPBilling_Purchase_VerifySignature(_pjson, _sig)
{
if (_pid != global.IAP_PurchaseID[2] && _pid != global.IAP_PurchaseID[4] && _pid != global.IAP_PurchaseID[5])
{
GPBilling_ConsumeProduct(_ptoken);
}
if _pid == global.IAP_PurchaseID[2] then GPBilling_AcknowledgePurchase(_ptoken);
if _pid == global.IAP_PurchaseID[4] then GPBilling_AcknowledgePurchase(_ptoken);
if _pid == global.IAP_PurchaseID[5] then GPBilling_AcknowledgePurchase(); //i tried not putting _ptoken in this one to see if it made a difference
// If it is a durable product then you'd call
// GPBilling_AcknowledgePurchase();
ds_list_add(global.CurrentTokens, _ptoken);
ds_list_add(global.CurrentProducts, _pmap[? "productId"]);
}
}
}
}
ds_map_destroy(_map);
break;
case gpb_product_consume_response:
// Get the JSON object response string
var _json = async_load[? "response_json"];
var _map = json_decode(_json);
var _num = -1;
// Get the purchase token for the product that has been purchased
if ds_map_exists(_map, "purchaseToken")
{
// compare the response purchase token against the list
// of purchase token requests
for (var i = 0; i < ds_list_size(global.CurrentTokens); ++i;
{
// the response matches a token in the purchase check list
if _map[? "purchaseToken"] == global.CurrentTokens[| i]
{
// Find out what product the token refers to
if global.CurrentProducts[| i] == global.IAP_PurchaseID[0]
{
// Assign a reward according to the product being purchased
global.coins += 5000;
_num = i;
break;
}
if global.CurrentProducts[| i] == global.IAP_PurchaseID[1]
{
// Assign a reward according to the product being purchased
global.coins += 10000;
_num = i;
break;
}
if global.CurrentProducts[| i] == global.IAP_PurchaseID[3]
{
// Assign a reward according to the product being purchased
global.coins += 12000;
_num = i;
break;
}

// Check any other products here…
}
}
// Remove the purchased product and its purchase token
// from the appropriate check lists
if _num > -1
{
ds_list_delete(global.CurrentProducts, _num);
ds_list_delete(global.CurrentTokens, _num);
}
}
else
{
// Parse the error response codes here
// and react appropriately
}
ds_map_destroy(_map);
break;

case gpb_acknowledge_purchase_response:
var _map = json_decode(async_load[? "response_json"]);
var _num = -1;
// Check the response code to see if it has been successfully acknowledged
if _map[? "responseCode"] == 0
{
var _sz = ds_list_size(global.CurrentProducts);
// Loop through the products on the consumed/purchase list
// to find which one triggered this event
for (var i = 0; i < _sz; ++i;
{
if global.CurrentProducts[| i] == global.IAP_PurchaseID[2]
{
// The product has been found so enable/disable
// the feature it corresponds to
global.NoAds = true;
ini_open( "savedata.ini" );
ini_write_real( "save1", "no_ads", global.NoAds);
ini_close();
_num = i;
break;
}
if global.CurrentProducts[| i] == global.IAP_PurchaseID[4]
{
// The product has been found so enable/disable
// the feature it corresponds to
global.test1 = true;
ini_open( "savedata.ini" );
ini_write_real( "save1", "test1", global.test1);
ini_close();
_num = i;
break;
}
if global.CurrentProducts[| i] == global.IAP_PurchaseID[5]
{
// The product has been found so enable/disable
// the feature it corresponds to
global.test2 = true;
ini_open( "savedata.ini" );
ini_write_real( "save1", "test2", global.test2);
ini_close();
_num = i;
break;
}
// Add further checks for other products here if required…
}
// Remove the purchased product and its purchase token
// from the appropriate check lists
if _num > -1
{
ds_list_delete(global.CurrentProducts, _num);
ds_list_delete(global.CurrentTokens, _num);
}
}
else
{
// Parse the other response codes here
// and react appropriately
}
ds_map_destroy(_map);
break;

}

This is in the mouse_click event for the actual button to remove ads...

audio_play_sound (snd_clickbutton, 10, false);
if GPBilling_IsStoreConnected() && global.IAP_Enabled
{
var _chk = GPBilling_PurchaseProduct(global.IAP_PurchaseID[2]);
// alternatively, for subscriptions:
// var _chk = GPBilling_PurchaseSubscription(global.IAP_PurchaseID[0]);
if _chk != gpb_no_error
{
// Purchase unavailable, add failsafe code if required
}
}

if GPBilling_IsStoreConnected() && global.IAP_Enabled
{
var _chk = GPBilling_PurchaseProduct(global.IAP_PurchaseID[4]);
// alternatively, for subscriptions:
// var _chk = GPBilling_PurchaseSubscription(global.IAP_PurchaseID[0]);
if _chk != gpb_no_error
{
// Purchase unavailable, add failsafe code if required
}
}

if GPBilling_IsStoreConnected() && global.IAP_Enabled
{
var _chk = GPBilling_PurchaseProduct(global.IAP_PurchaseID[5]);
// alternatively, for subscriptions:
// var _chk = GPBilling_PurchaseSubscription(global.IAP_PurchaseID[0]);
if _chk != gpb_no_error
{
// Purchase unavailable, add failsafe code if required
}
}
 
Hi @Warspite2, just curious - how did you solve this in the end? I'm now dealing with a similar problem, only from the other end - I only had durable products first, which worked fine - then I added consumables too and it kind of broke both :D
 
Top