How to check if a subscription has been cancelled? - ios

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.

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

Auto-renewable subscriptions expiration date off Apple specification

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!

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

Mac App Store consumable receipts have empty in_app hash on server side validation

I verify the receipt of in-app-purchases (so called consumables) for the Mac App Store on the server side. The response from Apple's servers usually looks like this:
{
"status"=>0,
"environment"=>"Production",
"receipt" =>
{
"receipt_type" => "Production",
"adam_id"=>410628904,
"bundle_id" => "com.company.product",
"application_version"=>"1.0.0",
"download_id"=>002141541230420,
"request_date"=>"2013-10-22 07:53:11 Etc/GMT",
"request_date_ms"=>"1382428391914",
"request_date_pst"=>"2013-10-22 00:53:11 America/Los_Angeles",
"original_purchase_date"=>"2011-08-22 06:05:47 Etc/GMT",
"original_purchase_date_ms"=>"1313993147000",
"original_purchase_date_pst"=>"2011-08-21 23:05:47 America/Los_Angeles",
"original_application_version"=>"1.0.0",
"in_app"=> [
{
"quantity"=>"1",
"product_id"=>"com.company.product.mac_consumable",
"transaction_id"=>"9123912391231",
"original_transaction_id"=>"51881235936908",
"purchase_date"=>"2013-10-22 07:52:06 Etc/GMT",
"purchase_date_ms"=>"1382428326000",
"purchase_date_pst"=>"2013-10-22 00:52:06 America/Los_Angeles",
"original_purchase_date"=>"2013-10-22 07:52:06 Etc/GMT",
"original_purchase_date_ms"=>"1382428326000",
"original_purchase_date_pst"=>"2013-10-22 00:52:06 America/Los_Angeles",
"bundle_id"=>"com.company.product"
}
]
}
}
But sometimes we get back information without the in_app hash set:
{
"status"=>0,
"environment"=>"Production",
"receipt" =>
{
"receipt_type" => "Production",
"adam_id"=>312621904,
"bundle_id" => "com.company.product",
"application_version"=>"1.0.0",
"download_id"=>002141541230420,
"request_date"=>"2013-10-22 07:53:11 Etc/GMT",
"request_date_ms"=>"1382428391914",
"request_date_pst"=>"2013-10-22 00:53:11 America/Los_Angeles",
"original_purchase_date"=>"2011-08-22 06:05:47 Etc/GMT",
"original_purchase_date_ms"=>"1313993147000",
"original_purchase_date_pst"=>"2011-08-21 23:05:47 America/Los_Angeles",
"original_application_version"=>"1.0.0",
"in_app"=> []
}
}
Does this mean the receipts are invalid? Should the in_app field always be populated? Or should those receipts be considered valid as well and why is the in_app information empty then?
Does this mean the receipts are invalid?
No, the status value is 0, which according to the docs means that the receipt is valid. It just doesn't contain any in-app purchase “sub-receipts”.
Should the in_app field always be populated?
No, it's possible for a receipt to not contain any in-app purchases.
So apparently your problem is that for some reason the receipt your app is sending to your backend doesn't contain information for an in-app purchase, even though you expect it to.
When an in-app purchase transaction enters the "purchased" state, the receipt should be up to date on the client device — you should ensure that you don't try to send the receipt onto your server before this happens.
One other thing to consider trying is SKReceiptRefreshRequest (only available since 10.9, though) — in some edge conditions the receipt might not be up to date, and you'd need to wait for it to update before sending the receipt redemption request to your backend.

Resources