I have a problem with validating an apple receipt on the server-side.
I tried to find a solution in the internet, but haven't succeeded.
So, description:
First of all, application is made for iOS7. Secondly, I have a few items ( type = Non-Renewing Subscription ). So user can buy one or multiple items and then he should manually renew them ( buy again ).
Applications sends a receipt to the server-side, I make a request to the Apple and get the result with a lot of in_app receipts. Something like:
"in_app":[
{
"quantity":"1", "product_id":"...", "transaction_id":"...",
"original_transaction_id":"...", "purchase_date":"...",
"purchase_date_ms":"...", "purchase_date_pst":"...",
"original_purchase_date":"...",
"original_purchase_date_ms":"...", "original_purchase_date_pst":"...",
"is_trial_period":"..."},
{
"quantity":"1", "product_id":"...",
"transaction_id":"...","original_transaction_id":"...",
"purchase_date":"...", "purchase_date_ms":"...",
"purchase_date_pst":"...", "original_purchase_date":"...",
"original_purchase_date_ms":"...", "original_purchase_date_pst":"...",
"is_trial_period":"..."}
]
So, each "receipt" in "in_app" has transaction_id. But how I can identify the transactionId of the current purchase? I would like to validate it as well and make sure that this is unique.
My concern is: if somebody will get one valid receipt, he will be able to hack our server-side API and make unlimited numbers of in-app purchases with the same valid receipt.
Should I somehow decrypt and check for transaction_id the "original" receipt, the one what I send to Apple for verification?
Any help/suggestions would be highly appreciated.
Thank you in advance.
Regards,
Maksim
#Doug Smith
https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html
If you go through the different fields on this page, you will find
Original Transaction Identifier::
For a transaction that restores a previous transaction, the transaction identifier of the original transaction. Otherwise, identical to the transaction identifier.
This value corresponds to the original transaction’s transactionIdentifier property.
All receipts in a chain of renewals for an auto-renewable subscription have the same value for this field.
So for your non-auto renewable subscriptions, you have to keep track of two things on your server side:
The original transaction identifier of the receipt that you are validating with itunes server, associate this with the user Id in your database.
Whether the request that you received from the client side is of a Purchase or of a Restore Purchase.
Once you have these two things with you, you can write your logic on these two parameters like below:
::If a request is of type "Purchase" and you already have the original transaction identifier of that receipt associated with some other user Id, you can block that purchase.
::If a request is of type "Restore Purchase" and request is coming from the same user id against which the original transaction identifier is associated in your DB than allow him otherwise block his restore.
Furthermore, you can derive your own logic based on these things, according to your needs.
Let me know if you have any further doubts.
For each new transaction apple send a new receipt which is unique, encode it so no one can forge data.
Get the transaction receipt from the completed transaction encode it and send it to your server, and on the server side decode it and match with the one apple send to server.
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
_transactionArray = transactions;
for (SKPaymentTransaction * transaction in transactions)
{
switch (transaction.transactionState)
{
case SKPaymentTransactionStatePurchased: {
NSData *receipt = transaction.transactionReceipt;
[self sendReceiptToServer];
} break;
case SKPaymentTransactionStateFailed: {
// finish this transaction
} break;
case SKPaymentTransactionStateRestored:
NSData *receipt = transaction.transactionReceipt;
[self sendReceiptToServer:receipt];
} break;
default:
break;
}
};
}
-(void)sendReceiptToServer:(NSData *)receipt {
// encode receipt
// send receipt to server
// add success and error callback
}
-(void) receiptSuccess {
// finish transaction here
}
-(void) receiptError {
// try again sending receipt to server
}
Related
I purchase a consumable product with StoreKit 2 using my device with Xcode:
let result = try await product.purchase()
switch result {
case .success(let verification):
switch verification {
case .verified(let transaction):
await transaction.finish() // comes here
...
}
it is successful (I get result with verified transaction in it, my server validates the receipt).
But I can't get any transactions after it (I need it to initiate iOS 15 refund in-app flow), nothing from these works: Transaction.currentEntitlements, Transaction.latest(for: productId), Transaction.currentEntitlement(for: productId) - always nil and []. Even Transaction.updates doesn't emit anything.
The only mention I got is https://developer.apple.com/forums/thread/689480
Any ideas?
I ran into a similar issue with storekit2 and it looks like it’s by design, at least according to this apple response on the dev forums https://developer.apple.com/forums/thread/687199
I haven’t looked if there is a way to do this in the original store kit api.
There is one thing I don't quite understand when it comes to In-App Subscription purchase.
I obtain the receipt on iOS client like this:
private func loadReceipt() -> Data? {
guard let url = Bundle.main.appStoreReceiptURL else {
return nil
}
do {
let receipt = try Data(contentsOf: url)
return receipt
} catch {
print("Error loading receipt data: \(error.localizedDescription)")
return nil
}
}
And send it for verification to my server (written n Python).
def verify_receipt(self, receipt):
r = requests.post(config.APPLE_STORE_URL, json=receipt)
request_date_ms = DateUtils.generate_ms_from_current_time()
for item in r.json()['latest_receipt_info']:
expires_date_ms = int(item['expires_date_ms'])
if expires_date_ms > request_date_ms:
return True
return False
I'm not sure if this is the correct way of verifying if a subscription is still valid.
I get the expires_date_ms from latest_receipt_info, and if it's greater than the current time in milliseconds, then the subscription counts as still valid.
However what I noticed is that the separate latest_receipt, which is supposed to be equal to the one I have just sent in, is actually changing every time I call the API. But why? I haven't subscribed to anything new, why is the latest receipt changing?
According to the docs:
latest_receipt
Only returned for receipts containing auto-renewable subscriptions.
For iOS 6 style transaction receipts, this is the base-64 encoded
receipt for the most recent renewal. For iOS 7 style app receipts,
this is the latest base-64 encoded app receipt.
If this is against the sandbox then the subscription is auto-renewing on a predefined period. See:
https://help.apple.com/app-store-connect/#/dev7e89e149d
If I wipe the data in my app, then re-purchase some managed IAP that I know the account already owns, iOS gives me the native "are you sure you wish to repurchase this item you will not be charged" dialog. That's as expected.
When the purchase returns to my app, I get the receipt with [[NSBundle mainBundle] appStoreReceiptURL]. I then attempt to verify the SKPaymentTransaction's transactionIdentifier with the receipt via my server.
However the receipt's transaction ID for this IAP, because I've already purchased it long ago, is not the same as the SKPaymentTransaction.
How should I be verifying that this is a valid repurchase? Can I get a signed receipt for this repurchase somehow?
All the purchases will be in a single receipt. You do have to check all iAPs there and look for the needed ones.
If you want the id of the original iAP transaction, there is Original Transaction Identifier
For a transaction that restores a previous transaction, the transaction identifier of the original transaction. Otherwise, identical to the transaction identifier.
More information about receipt fields is here: https://developer.apple.com/library/prerelease/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html
Anyway, does it really matter whether this is an original purchase or a repurchase? My checks of this are usually the same.
As far as I understand it you should check the originalTransaction to check that this is valid for transactions that are SKPaymentTransactionStateRestored.
[[NSBundle mainBundle] appStoreReceiptURL] is for the receipt of the main application, not the IAP.
You can use RMStore library, and check all purchases:
RMStoreAppReceiptVerificator *verificator = [[RMStoreAppReceiptVerificator alloc] init];
if ([verificator verifyAppReceipt])
{
NSArray *inAppPurchases = [RMAppReceipt bundleReceipt].inAppPurchases;
for (RMAppReceiptIAP *inAppPurchase in inAppPurchases)
{
NSLog(#"productIdentifier %# originalPurchaseDate %#", inAppPurchase.productIdentifier, inAppPurchase.originalPurchaseDate);
}
}
If your receipt is nil you can refresh receipt:
[[RMStore defaultStore] refreshReceiptOnSuccess:^{
//get purchases
} failure:^(NSError *error) {
}];
I have an iOS app which has in-app purchase and restore purchases functionality.
When I was testing the app in sandbox mode I use to get the alert all times asking for the password of iTunes account (UserName was already populated).
Now my app is live and I installed it and did the in-app purchase and then restored the purchases, So I am not in sandbox mode still I keep getting the alerts asking for password of iTunes account.
Following is a code which gives the ProductIds that I have purchased earlier,Then I pass those productIds to delegate which changes the status of those from 'Buy' to 'Purchased'
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
{
//NSLog(#"Restored Transactions are once again in Queue for purchasing %#",[queue transactions]);
NSMutableArray *purchasedItemIDs = [[NSMutableArray alloc] init];
//NSLog(#"received restored transactions: %i", queue.transactions.count);
for (SKPaymentTransaction *transaction in queue.transactions)
{
NSString *productID = transaction.payment.productIdentifier;
[purchasedItemIDs addObject:productID];
// NSLog (#"product id is %#" , productID);
}
if ( mDelegate != nil && [mDelegate respondsToSelector:#selector(purchasedProductList:)] ){
[mDelegate purchasedProductList:purchasedItemIDs];
}
[purchasedItemIDs release];
}
Any idea how to avoid this?
Thanks
You need to ensure that finishTransaction: is called. It must always be called and calling it on a transaction multiple times is fine. If it isn't called, the transaction remains in the queue and the app will try to process it again later. It doesn't matter how the transaction was added to the queue, once it has been acted upon it should be 'finished'.
Note that when restoring new transactions are created, which are effectively wrappers around the original transactions, and they need to be 'finished'. From the restore docs:
The payment queue will deliver a new transaction for each previously completed transaction that can be restored. Each transaction includes a copy of the original transaction.
So I have an app that has a consumable purchase and from what I have read a little more work is involved in terms of persisting some info on the server.
What I am not clear on is where does one update the server on a sucessful purchase? Is it
(void)completeTransaction:(SKPaymentTransaction *)transaction;
??
If it is indeed the above mentioned function, is it just a matter of making a web call and updating the server server? I am just trying to understand best practices and would appreciate any help.
much regards
I don't know if this counts as a valid answer, but you should maybe check out this article.
The blog post is from 2010, so one could say it's rather outdated! But i think it's worth a look and the provided example (completeTransaction method implementation) should be valid also.
Getting in app purchases to work is a lot more painful than it should be actually.
I did and do it in
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
...
case SKPaymentTransactionStatePurchased:
case SKPaymentTransactionStateRestored:
[self sendReceiptToServerAndValidateFurther:transaction.receiptData
it is then only a webcall to the server-side script which can validate the receipt with itunes and update its DB/send the data
btw some server php
function itunes_verify2($receiptData,$sandboxed) {
//itunes store url -- sandbox or not :)
$itunes_storeURL = "https://buy.itunes.apple.com/verifyReceipt";
if($sandboxed == true) {
$itunes_storeURL = "https://sandbox.itunes.apple.com/verifyReceipt";
}
log_action('itunes_verify2 using ' . $itunes_storeURL);
$post = array("receipt-data" => $receiptData );
$postData = json_encode($post);
$responseData = post_doRequest( $itunes_storeURL, $postData );
$response = json_decode($responseData);
if($responseArray->status == 0) {
return "YES";
} else {
log_action('failed to get status ok from ' . $reponseData);
return "NO";
}
}
I'll give a vote here for this SO question. Seems to have several examples.
https://stackoverflow.com/questions/4178214/ios-in-app-purchase-sample-code
From what I can tell, consumables do require your own hosting, as mentioned here:
iOS In App Purchases for Subscriptions and Consumables products
I have no personal experience with consumables, only with upgrade-type IAPs, which weren't too painful (relatively speaking).