• Hey! Guest! The 39th GMC Jam will take place between November 26th, 12:00 UTC and November 30th, 12:00 UTC. Why not join in! Click here to find out more!
  • Hello [name]! Thanks for joining the GMC. Before making any posts in the Tech Support forum, can we suggest you read the forum rules? These are simple guidelines that we ask you to follow so that you can get the best help possible for your issue.

iOS IAP not working

yakmoon

Member
Hi, after following the IAP documentation and carefully examining the IOS IAP extenstion I couldn't find why the extension is not working. I searched on this forum to see if others are having the same issue. and yes there is another topic with IAP problem. but nobody even cared to answer the question to why it's not working.
this is the issue I have:
create event
Code:
global.IAP_Enabled = ios_iap_IsAuthorisedForPayment();
        IAP_PID0="Product1";
        IAP_PID1="Product2";
        global.IAP_PurchaseID[0, 0] = IAP_PID0;
        global.IAP_PurchaseID[1, 0] = IAP_PID1;
        if global.IAP_Enabled
        {
            ios_iap_AddProduct(IAP_PID0);
            ios_iap_AddProduct(IAP_PID1);
            ios_iap_QueryProducts();
        }
the problem is that ios_iap_QueryProducts is not working properly. because in the social event I'm receiving an empty list of products. I wonder why?!
and when executing ios_iap_PurchaseProduct(); nothing happens, you know why? because the list doesn't exist! so is there any solution to this, or I should forget about ios?
note: I've submitted the game to app store and the testers there rejected my game because the IAP is not working.
this is their response:
We found that your in-app purchase products exhibited one or more bugs when reviewed on iPad running iOS 13.2 on Wi-Fi.

Specifically, the “BUY” button was not responsive.

Next Steps

When validating receipts on your server, your server needs to be able to handle a production-signed app getting its receipts from Apple’s test environment. The recommended approach is for your production server to always validate receipts against the production App Store first. If validation fails with the error code "Sandbox receipt used in production," you should validate against the test environment instead.

Resources

You can learn more about testing in-app purchase products in your development sandbox environment in App Store Connect Developer Help.

For more information on receipt validation, please see What url should I use to verify my receipt? in the In-App Purchase FAQ.

Learn how to generate a receipt validation code in App Store Connect Developer Help.



Please see attached screenshot for details.

I'm really excited to read the replies on this topic.
 

chirpy

Member
You didn’t mention Apple sandbox account so I’m not sure whether you had it configured correctly. If you cannot get a list that’s probably one of the first things you need to check. Other than that, you might want to check more details in Apple’s IAP documentation.

I have gotten my IAP working and approved recently after 2.2.4 update so I’m sure it is at least partly working (I only added one nonconsumable product for ads removal). Had to handle network status and connection retries, ui to reflect parental control (not authorized for payment case) etc. Good luck.
 

spinout

Member
I've got IAP working for subscriptions and non consumables such as ad removals, but I'm having trouble getting consumables approved by the Apple team. They said they are unresponsive, although they work when I test them on sandbox.
 

chirpy

Member
I've got IAP working for subscriptions and non consumables such as ad removals, but I'm having trouble getting consumables approved by the Apple team. They said they are unresponsive, although they work when I test them on sandbox.
I had confronted similar issues when submitting first non-consumable (got sandbox working).
It seems to me that Apple is being pretty strict on parental control, so they randomly pick apps to test IAPs on devices where IAPs aren't allowed.
I had to change my IAP buttons to reflect the status (I ended up hiding all disallowed IAP buttons).

Disclaimer:
Since the app review board never answered me directly whether it was all about parental control until I got approved, it was just my assumption.
I had rewritten my network retry logic, and checked other possibilities mentioned in Apple developer forum before assuming this was the issue.

Ref:
https://developer.apple.com/app-store/review/guidelines/
  • We have lots of kids downloading lots of apps. Parental controls work great to protect kids, but you have to do your part too. So know that we’re keeping an eye out for the kids.
Screen time / content & privacy restrictions:
https://support.apple.com/bg-bg/HT201304
 

spinout

Member
I've got it working and approved by the Apple review team. I changed from validating the receipts locally to verifying then on Apple's servers. By their recommended procedure they suggest to verify on production server, if that fails fall back and try to verify it on the sandbox server. Worked!
 

yakmoon

Member
I've got it working and approved by the Apple review team. I changed from validating the receipts locally to verifying then on Apple's servers. By their recommended procedure they suggest to verify on production server, if that fails fall back and try to verify it on the sandbox server. Worked!
can you enlighten us on how did you do that?
 

spinout

Member
////make the pychase
if global.IAPEnabled {
ios_iap_PurchaseProduct(global.ProductID[2,0]);
iaploading = true;
iaptimer = 700;
} else {

}


////in-app response
var _json = async_load[? "response_json"];
if _json != "" {
var _map = json_decode(_json);
var _plist = _map[? "purchases"];
var _sz = ds_list_size(_plist);
for (var i=0;i<_sz;++i;) {
var _pmap = _plist[| i];
if _pmap[? "purchaseState"] != ios_purchase_failed {
var _receipt = ios_iap_GetReceipt();
if _receipt != "" {
switch _pmap[? "productId"] {
case "subway.sub":
currentIAP = 0;
break;
case "subway.adfree":
currentIAP = 1;
break;
case "subway.gems1":
currentIAP = 2;
break;
case "subway.gems2":
currentIAP = 3;
break;
case "subway.gems3":
currentIAP = 4;
break;
case "subway.gems4":
currentIAP = 5;
break;
case "subway.gems5":
currentIAP = 6;
break;
case "subway.gems6":
currentIAP = 7;
break;
}
receiptID= Script_IAPs_validation_server(1);
} else {
if global.receiptFails < 1 {
ios_iap_RefreshReceipt();
global.receiptFails++;
} else {
global.IAPEnabled = false;
}
}
} else {
iaploading = false;
iaptimer = 0;
}
var _ptoken = _pmap[? "purchaseToken"];
ios_iap_FinishTransaction(_ptoken);
ds_map_destroy(_pmap);
}
ds_map_destroy(_map);
}
break;

////Script_IAPs_validation_server()
var map = ds_map_create();
ds_map_add(map,"receipt-data",ios_iap_GetReceipt());//iap_receipt_full_read_base64())
ds_map_add(map,"password","########################");/// only used if you have subscriptions
var json = json_encode(map);
ds_map_destroy(map);

if argument0 {
return http_post_string("https://buy.itunes.apple.com/verifyReceipt",json);
} else {
return http_post_string("https://sandbox.itunes.apple.com/verifyReceipt",json);
}

////http response
if (not isMap(async_load)) {
exit;
} else {
if (async_load[?"id"] == receiptID) {

if(async_load[?"status"] != 0 or async_load[?"http_status"] != 200) {
iaploading = false;
iaptimer = 0;
exit;
}

var storeReceipt= async_load[?"result"];

var map = json_decode(storeReceipt);

if (map[?"status"] != 0) {
if map[? "status"] == "21007" {
receiptID= Script_IAPs_validation_server(0);
} else {
iaploading = false;
iaptimer = 0;
}
ds_map_destroy(map);
exit;
}
var _amount = 0;
var _sub = false;
switch currentIAP {
case -1: exit;
case 0:
_sub = true;
break;
case 1:
if !adfree {
sAdfreeBought(1);
}
break;
case 2:
_amount = gemsValue[0];
break;
case 3:
_amount = gemsValue[1];
break;
case 4:
_amount = gemsValue[2];
break;
case 5:
_amount = gemsValue[3];
break;
case 6:
_amount = gemsValue[4];
break;
case 7:
_amount = gemsValue[5];
break;
}
currentIAP = -1;
if _amount > 0 {
////give gems
}
else if _sub {
//do sub stuff
iaploading = false;
iaptimer = 0;
ds_map_destroy(map);
}
}
 

MrBazooka

Member
////make the pychase
if global.IAPEnabled {
ios_iap_PurchaseProduct(global.ProductID[2,0]);
iaploading = true;
iaptimer = 700;
} else {

}


////in-app response
var _json = async_load[? "response_json"];
if _json != "" {
var _map = json_decode(_json);
var _plist = _map[? "purchases"];
var _sz = ds_list_size(_plist);
for (var i=0;i<_sz;++i;) {
var _pmap = _plist[| i];
if _pmap[? "purchaseState"] != ios_purchase_failed {
var _receipt = ios_iap_GetReceipt();
if _receipt != "" {
switch _pmap[? "productId"] {
case "subway.sub":
currentIAP = 0;
break;
case "subway.adfree":
currentIAP = 1;
break;
case "subway.gems1":
currentIAP = 2;
break;
case "subway.gems2":
currentIAP = 3;
break;
case "subway.gems3":
currentIAP = 4;
break;
case "subway.gems4":
currentIAP = 5;
break;
case "subway.gems5":
currentIAP = 6;
break;
case "subway.gems6":
currentIAP = 7;
break;
}
receiptID= Script_IAPs_validation_server(1);
} else {
if global.receiptFails < 1 {
ios_iap_RefreshReceipt();
global.receiptFails++;
} else {
global.IAPEnabled = false;
}
}
} else {
iaploading = false;
iaptimer = 0;
}
var _ptoken = _pmap[? "purchaseToken"];
ios_iap_FinishTransaction(_ptoken);
ds_map_destroy(_pmap);
}
ds_map_destroy(_map);
}
break;

////Script_IAPs_validation_server()
var map = ds_map_create();
ds_map_add(map,"receipt-data",ios_iap_GetReceipt());//iap_receipt_full_read_base64())
ds_map_add(map,"password","########################");/// only used if you have subscriptions
var json = json_encode(map);
ds_map_destroy(map);

if argument0 {
return http_post_string("https://buy.itunes.apple.com/verifyReceipt",json);
} else {
return http_post_string("https://sandbox.itunes.apple.com/verifyReceipt",json);
}

////http response
if (not isMap(async_load)) {
exit;
} else {
if (async_load[?"id"] == receiptID) {

if(async_load[?"status"] != 0 or async_load[?"http_status"] != 200) {
iaploading = false;
iaptimer = 0;
exit;
}

var storeReceipt= async_load[?"result"];

var map = json_decode(storeReceipt);

if (map[?"status"] != 0) {
if map[? "status"] == "21007" {
receiptID= Script_IAPs_validation_server(0);
} else {
iaploading = false;
iaptimer = 0;
}
ds_map_destroy(map);
exit;
}
var _amount = 0;
var _sub = false;
switch currentIAP {
case -1: exit;
case 0:
_sub = true;
break;
case 1:
if !adfree {
sAdfreeBought(1);
}
break;
case 2:
_amount = gemsValue[0];
break;
case 3:
_amount = gemsValue[1];
break;
case 4:
_amount = gemsValue[2];
break;
case 5:
_amount = gemsValue[3];
break;
case 6:
_amount = gemsValue[4];
break;
case 7:
_amount = gemsValue[5];
break;
}
currentIAP = -1;
if _amount > 0 {
////give gems
}
else if _sub {
//do sub stuff
iaploading = false;
iaptimer = 0;
ds_map_destroy(map);
}
}
Hi there

I have a few questions about where you implemented everything. Is the purchase section on a button and the rest of the code in the async event on a persistent controller object?
 

MrBazooka

Member
In the Apple IAP Manual included with the extension it reads:
"IMPORTANT! In order for the function ios_iap_ValidateReceipt() to return a true response, users must download the Apple Inc. Root Certificate and include it with their project in the Included Files (using included files covers iOS/tvOS and macOS)"

Can someone explain how I include this certificate with my project? I tried Spinout's method, no errors but it doesn't work. Just an unresponsive button
 

MrBazooka

Member
Thanks guys. I hope this works. Apple contacted me about featuring my game, but one of the prerequisites is to implement an IAP for extra "lives". I've been struggling to get the one consumable IAP to work.
 

spinout

Member
Thanks guys. I hope this works. Apple contacted me about featuring my game, but one of the prerequisites is to implement an IAP for extra "lives". I've been struggling to get the one consumable IAP to work.
Wow! That must be frustrating! Did you get it to work? Sorry, I can go days without checking these forums sometimes.
 

MrBazooka

Member
Apple said that if I can't get my IAP to work that they'll just feature as is. I really want to try and implement one IAP that just gives extra lives. I'm posting my code below. Maybe someone can spot something. I followed the YYG tutorial on IAPs.

In main room>
IAP controller object (persistent)

Create event:
Code:
global.IAP_Enabled = ios_iap_IsAuthorisedForPayment();
global.ProductID[0, 0] = "ios_continues_15_more";

if global.IAP_Enabled
    {
    ios_iap_AddProduct(global.ProductID[0, 0]);
    ios_iap_QueryProducts();
    }
else
{
    obj_btn_iap.sprite_index = spr_btn_cantbuy;
}
Button object

Left released:
Code:
if global.IAP_Enabled
    {
    ios_iap_PurchaseProduct(global.ProductID[0, 0]);
    }
// According to documentation should trigger async event ios_payment_queue_update

Same IAP controller object as above

Async IAP event:
Code:
var _eventId = async_load[? "id"];

eventID = _eventId;

switch (_eventId)
    {
 
    case ios_payment_queue_update:
        // Decode the returned JSON
        var _json = async_load[? "response_json"];
        if _json != ""
            {
            var _map = json_decode(_json);
            var _plist = _map[? "purchases"];
            var _sz = ds_list_size(_plist);
            // loop through purchases
            for (var i = 0; i < _sz; ++i;)
                {
                var _pmap = _plist[| i];
                // Check purchases
                var _ptoken = _pmap[? "purchaseToken"];
                if _pmap[? "purchaseState"] != ios_purchase_failed
                    {
                    var _receipt = ios_iap_GetReceipt();
                    // CALL SERVER CHECK WITH RECEIPT HERE
                    // or validate, finalise and award the product
                    if ios_iap_ValidateReceipt(_receipt) == true
                        {
                        obj_iap.continues_left = 15;
                        sprite_index = spr_btn_blank;
                        scr_iap();
                        obj_iap.continues_left -= 1;
                     
                        ios_iap_FinishTransaction(_ptoken);
                        }
                    else
                        {
                        // Validation failed, so deal with it here
                        ios_iap_RefreshReceipt();
                        }
                    }
                else
                    {
                    // Purchase failed, so finalise it.
                    ios_iap_FinishTransaction(_ptoken);
                    }
                ds_map_destroy(_pmap);
                }
            }
        break;
    }
Please any help would be greatly appreciated
 
Last edited:

chirpy

Member
@MrBazooka
I only vaguely remember that ios_iap_ValidateReceipt does not actually take a _receipt argument by looking at the code you posted.
Perhaps you'd want to create a new thread and provide more details about your question and where you got stuck, what error messages?
 
Top