Auto-renewable subscriptions expiration date off Apple specification - ios

I am trying to validate auto-renewable subscriptions on iOS. More specifically I want to:
1) Verify that the signature is correct [ √ ]
2) Verify that the subscription has not expired [ X ]
When I hand the receipt of to the server I get validation status 0, which is exactly what I expect, nothing wrong with that.
However, when I try to read the expiration date of the receipt in the sandbox environment, the expiration dates drift off of the specified expiration dates for subscriptions from the Apple documentation, meaning, instead of having the subscription expire after 5 minutes in the case of our 1 month subscription, we get expiration times of several hours!
{
"quantity": "1",
"product_id": "{PRODUCT_IDENTIFIER}",
"transaction_id": "1000000182307463",
"original_transaction_id": "1000000182307463",
"purchase_date": "2015-11-27 23:47:06 Etc/GMT",
"purchase_date_ms": "1448668026000",
"purchase_date_pst": "2015-11-27 15:47:06 America/Los_Angeles",
"original_purchase_date": "2015-11-27 23:47:07 Etc/GMT",
"original_purchase_date_ms": "1448668027000",
"original_purchase_date_pst": "2015-11-27 15:47:07 America/Los_Angeles",
"expires_date": "2015-11-28 02:35:06 Etc/GMT", // ---> expires_date - purchase_date => ~ 3 hrs
"expires_date_ms": "1448678106000",
"expires_date_pst": "2015-11-27 18:35:06 America/Los_Angeles",
"web_order_line_item_id": "1000000031037801",
"is_trial_period": "true"
}
If you look at the purchase_date and expiration_date fields you will quickly notice that they are almost 3hrs apart! (It is not even an even number, which also adds to the confusion...).
I haven't seen anybody else having this issue, which is why I am basically pulling my hair out over this.

The problem was that we didn't create sandbox accounts and used our iTunes Connect ones. After we created and used those it suddenly worked perfectly!

Related

Should I check cancellation_date_ms in addition to expires_date_ms for a auto-renewable subscriptions?

I am implementing an auto-renewable subscription in my app.
Currently, to check if the user have an active subscription, I examine all the records in latest_receipt_info array from the response JSON returned from the Apple's /verifyReceipt service, find the one with the max expires_date_ms and check if this date is in the future. Before checking the dates I also check if the status field is 0 (receipt is valid).
I was thinking that it is enough, but I recently found out that there is another field - cancellation_date_ms. As I understand from the docs, this filed is present if user has canceled his subscription through the Apple support.
From (Apple docs)
You can use this value to:
Determine whether to stop providing the content associated with the
purchase.
Check for any latest renewal transactions, which may indicate the user
re-started or upgraded their subscription, for an auto-renewable
subscription purchase.
So I am wondering, if a user cancels his subscription through the Apple support, will this affect the expires_date_ms for the current subscription period? So the next time I check expires_date_ms, I know the subscription is not active.
Or does expires_date_ms stays the same as it was before the user canceled the subscription, and so I need to check the cancellation_date_ms as well?
You should check for cancellation_date_ms as well. Expires_date field doesn't change when customer cancels subscription through Apple Support.
Here is production JSON example with refund:
"latest_receipt_info": [
{
"quantity": "1",
"product_id": "XXX",
"transaction_id": "150000567873035",
"original_transaction_id": "150000567873035",
"purchase_date": "2019-11-10 17:37:05 Etc/GMT",
"purchase_date_ms": "1573407425000",
"purchase_date_pst": "2019-11-10 09:37:05 America/Los_Angeles",
"original_purchase_date": "2019-11-10 17:37:06 Etc/GMT",
"original_purchase_date_ms": "1573407426000",
"original_purchase_date_pst": "2019-11-10 09:37:06 America/Los_Angeles",
"expires_date": "2019-12-10 17:37:05 Etc/GMT",
"expires_date_ms": "1575999425000",
"expires_date_pst": "2019-12-10 09:37:05 America/Los_Angeles",
"cancellation_date": "2019-12-05 19:14:48 Etc/GMT",
"cancellation_date_ms": "1575573288000",
"cancellation_date_pst": "2019-12-05 11:14:48 America/Los_Angeles",
"web_order_line_item_id": "150000194283402",
"is_trial_period": "false",
"is_in_intro_offer_period": "false",
"cancellation_reason": "0",
"subscription_group_identifier": "20502261"
}
],
"pending_renewal_info": [
{
"auto_renew_product_id": "XXX",
"original_transaction_id": "150000567873035",
"product_id": "XXX",
"auto_renew_status": "0"
}
]}

Need to understand iOS InApp purchase receipt

I'm digging into iOS in-app purchase validation (server side) and I got pretty much confused about the receipt fields returned by apple's validation server. The documentation available here is not really clear (at least for me)
So here is a real (obfuscated) in-app purchase receipt returned by Apple's validation server
{
"receipt": {
"receipt_type": "Production",
"adam_id": XXXXXXX,
"app_item_id": XXXXXXXXX,
"bundle_id": "com.XXXXX.XXXXX",
"application_version": "XXXXXXXXX",
"download_id": XXXXXXXXXXXX,
"version_external_identifier": XXXXXXXXXXX,
"receipt_creation_date": "2019-10-15 14:01:41 Etc/GMT",
"receipt_creation_date_ms": "1571148101000",
"receipt_creation_date_pst": "2019-10-15 07:01:41 America/Los_Angeles",
"request_date": "2019-10-15 14:04:20 Etc/GMT",
"request_date_ms": "1571148260390",
"request_date_pst": "2019-10-15 07:04:20 America/Los_Angeles",
"original_purchase_date": "2018-11-27 18:28:48 Etc/GMT",
"original_purchase_date_ms": "1543343328000",
"original_purchase_date_pst": "2018-11-27 10:28:48 America/Los_Angeles",
"original_application_version": "XXXXXXXXX",
"in_app": [
{
"quantity": "1",
"product_id": "com.XXXXXXXXXX.XXXXX.XXXXXX",
"transaction_id": "XXXXXXXXXXX",
"original_transaction_id": "XXXXXXXXXX",
"purchase_date": "2019-10-15 14:01:41 Etc/GMT",
"purchase_date_ms": "1571148101000",
"purchase_date_pst": "2019-10-15 07:01:41 America/Los_Angeles",
"original_purchase_date": "2019-10-15 14:01:41 Etc/GMT",
"original_purchase_date_ms": "1571148101000",
"original_purchase_date_pst": "2019-10-15 07:01:41 America/Los_Angeles",
"is_trial_period": "false"
}
]
},
"status": 0,
"environment": "Production"
}
So my questions are:
The fields starting with "original_purchase_date" represent a datetime but why they have a different value in the 2 parts of the receipt ?
In the in_app part of the receipt, can the values of the fields starting with "purchase_date" and the values of the fields starting with "original_purchase_date" be different ? And if yes, in which case ?
Is the "application_version" field contains the value of the current build version published of the app since the "original_application_version" field, according to the documentation, represent the build version of the app the user used to make the purchase ?
Thanks a lot for your help and answers.
R.E.B Hernandez!
original_purchase_date inside in_app is the time when in-app purchase has been made, the one outside this array is the date when the app was installed for the first time.
If you use non-consumable and non-renewing subscriptions, then purchase_date will be the same as original_purchase_date. However, if you use auto-renewable subscriptions, then you should check latest_receipt_info array instead of in_app. And in case of subscriptions, original_purchase_date will represent only the date of the first transaction.
Yes, application_version is the current version of the app on the device.
original_application_version is the version of the app that the user originally purchased.
Here is documentation link

How to check if a subscription has been cancelled?

I have requested a refund on my own app’s monthly subscription. Apple gave me the refund. But my subscription is still active and I can use the premium features.
My app is checking the validity of the receipt every time I use it, this proves that the receipt's expiry date has remained valid.
This is how I check for it. Although it is in Python on my server, I hope the code is clear enough for anyone to understand:
def verify_receipt(receipt):
r = requests.post(config.APPLE_STORE_URL, json=Subscription.produce_receipt_with_credentials(receipt))
now = datetime.utcnow()
if 'latest_receipt_info' in r.json():
for item in r.json()['latest_receipt_info']:
dt, tz = item['expires_date'].rsplit(maxsplit=1)
expires_date = datetime.strptime(dt, '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.timezone(tz))
expires_date = expires_date.astimezone(pytz.utc)
expires_date = expires_date.replace(tzinfo=None)
if expires_date > now:
return True, expires_date, r.json()['latest_receipt'], item
else:
current_app.logger.info('latest_receipt_info not found: %s', r.json())
return False, None, None, None
Essentially I'm checking within the collection of ‘latest_receipt_info’ for each receipt's ‘expires_date’. If at least one of them is set in the future, then the premium check is valid.
But in my case even though that Apple has refunded the subscription, they left it active until the next renewal.
So what is the point of checking the receipt regularly then? If we
can't catch early cancellation?
Wouldn’t be more efficient and faster for the existing users to just save the expiry date in UserDefaults and check locally when the expiry date has expired and then check for the validity of the next receipt?
SWIFT:
UserDefaults.standard.set(expiryDate, forKey: Constants.expiryDate)
UPDATE:
So based on the answer I have received, I suppose the cancellation_reason and cancellation_date will be next to these fields in the latest receipt?
"latest_receipt_info": [
{
"quantity": "1",
"product_id": "com.x.sub.weekly",
"transaction_id": "100000053x",
"original_transaction_id": "100000053x",
"purchase_date": "2019-06-03 19:52:05 Etc/GMT",
"purchase_date_ms": "1559591525000",
"purchase_date_pst": "2019-06-03 12:52:05 America/Los_Angeles",
"original_purchase_date": "2019-06-03 19:52:06 Etc/GMT",
"original_purchase_date_ms": "1559591526000",
"original_purchase_date_pst": "2019-06-03 12:52:06 America/Los_Angeles",
"expires_date": "2019-06-03 19:55:05 Etc/GMT",
"expires_date_ms": "1559591705000",
"expires_date_pst": "2019-06-03 12:55:05 America/Los_Angeles",
"web_order_line_item_id": "10000000x",
"is_trial_period": "false",
"is_in_intro_offer_period": "false"
"cancellation_reason": "0",
"cancellation_date" "2019-06-03 21:55:05 Etc/GMT"
},
I wished there was a way to emulate this. How can I code against this based on the docs and go to production without being really able to test it?
For refunds you need to check the cancellation_reason field which will indicate that customer support refunded the user. There will also be a cancellation_date that will indicate when the cancellation occurred.
If that field is present then your premium check should be invalid:
Treat a canceled receipt the same as if no purchase had ever been
made.

iOS always returning the same transaction id when validating IAP receipt

I'm testing In App Purchasing in iOS using 1 SandBox user. When I validate the receipt using Apple server, I get the following response:
{"status":0, "environment":"Sandbox", "receipt":
{"receipt_type":"ProductionSandbox", "adam_id":0, "app_item_id":0,
"bundle_id":"xxxxxxxxx", "application_version":"0",
"download_id":0, "version_external_identifier":0,
"request_date":"2015-05-19 22:04:45 Etc/GMT",
"request_date_ms":"1432073085427",
"request_date_pst":"2015-05-19 15:04:45 America/Los_Angeles",
"original_purchase_date":"2013-08-01 07:00:00 Etc/GMT",
"original_purchase_date_ms":"1375340400000",
"original_purchase_date_pst":"2013-08-01 00:00:00 America/Los_Angeles",
"original_application_version":"1.0", "in_app":[ {"quantity":"1",
"product_id":"xxxxxx", "transaction_id":"xxxxxxxxx",
"original_transaction_id":"xxxxxxxx", "purchase_date":"2015-05-19 18:32:54 Etc/GMT", "purchase_date_ms":"1432060374000",
"purchase_date_pst":"2015-05-19 11:32:54 America/Los_Angeles",
"original_purchase_date":"2015-05-19 18:32:54 Etc/GMT",
"original_purchase_date_ms":"1432060374000",
"original_purchase_date_pst":"2015-05-19 11:32:54 America/Los_Angeles", "is_trial_period":"false"}]}}
I tried to make multiple purchases of the same product with the same sandbox user and each time, I get the same response when I validate the receipt. The ONLY fields which are different are request_date_ms and request_date_pst.
Isn't the Transaction ID supposed to be unique each time??? Is this a bug?
Also the receipt seems to be exactly the same receipt each time. Not sure though as It's a VERY long string. So not sure if it's the same one each time but it sounds so.
The product is consumable so It should have a different transaction id each time.
Thanks

How can I see if a non consumable in-app purchase is valid using Apples Receipt Validation Endpoint

I recently set up a service to validate and record purchases from my iOS in accordance with
Apples documentation. After a few purchases had gone through I reviewed my table and found that I had 45 purchases that came back with a status code of 0 which according to Apple's docs means that they are valid. The issue is that when I logged into my iTunes Account it had only recorded 22 valid purchases. Upon closer examination of the JSON responses from Apple with the status of 0 I found two variations:
{
"status": 0,
"environment": "Production",
"receipt": {
"receipt_type": "Production",
"adam_id": 888310447,
"app_item_id": 888310447,
"bundle_id": "com.studioName.gameName",
"application_version": "1.4",
"download_id": 62010318102259,
"version_external_identifier": 810789159,
"request_date": "2014-12-16 15:34:17 Etc/GMT",
"request_date_ms": "1418744057267",
"request_date_pst": "2014-12-16 07:34:17 America/Los_Angeles",
"original_purchase_date": "2014-07-08 18:04:28 Etc/GMT",
"original_purchase_date_ms": "1404842668000",
"original_purchase_date_pst": "2014-07-08 11:04:28 America/Los_Angeles",
"original_application_version": "1.1",
"in_app": []
}
}
{
"status": 0,
"environment": "Production",
"receipt": {
"receipt_type": "Production",
"adam_id": 888310447,
"app_item_id": 888310447,
"bundle_id": "com.studioName.gameName",
"application_version": "1.4",
"download_id": 39011903209949,
"version_external_identifier": 810789159,
"request_date": "2014-12-16 15:11:10 Etc/GMT",
"request_date_ms": "1418742670718",
"request_date_pst": "2014-12-16 07:11:10 America/Los_Angeles",
"original_purchase_date": "2014-10-15 05:52:28 Etc/GMT",
"original_purchase_date_ms": "1413352348000",
"original_purchase_date_pst": "2014-10-14 22:52:28 America/Los_Angeles",
"original_application_version": "1.2.4",
"in_app": [
{
"quantity": "1",
"product_id": "com.studioName.gameName.productName",
"transaction_id": "190000148450370",
"original_transaction_id": "190000148450370",
"purchase_date": "2014-11-29 08:22:49 Etc/GMT",
"purchase_date_ms": "1417249369000",
"purchase_date_pst": "2014-11-29 00:22:49 America/Los_Angeles",
"original_purchase_date": "2014-10-17 08:30:26 Etc/GMT",
"original_purchase_date_ms": "1413534626000",
"original_purchase_date_pst": "2014-10-17 01:30:26 America/Los_Angeles",
"is_trial_period": "false"
}
]
}
}
Note that the first objects in-app array is empty while the second seems to actually have a receipt for the IAP. My assumption was that I should look to the receipts that have the in-app array populated. My issue is now that I have parsed out the receipts without an in-app array I am still left with 25 valid purchases (3 more than was recorded by Apple).
My next thought was that maybe some of these purchases were restores. I found this note in the Receipt Validation Programming guide. It states under the original transaction identifier:
"For a transaction that restores a previous transaction, the transaction identifier of the original transaction. Otherwise, identical to the transaction identifier."
After I examined all of my valid receipts I found no instances where the original_transaction_id and the transaction_id were different. For that matter I made a duplicate purchase which gave me the prompt that the item had already been purchased and asked if I would like to restore it for free. When I did so and checked the receipt I found that even in this instance the two transaction ids were the same which should not be the case.
When I put in an inquiry I received a canned response telling me that the IAP section of itunes was working properly. If anyone could help to shed some light on how I could better filter these to match Apples final count of IAPs it would be much appreciated!

Resources