Reputation: 18379
I have an iOS app (Objective C) with a subscription (non renewing). How can I check that it is still active when the user restarts the app?
I have read a lot about this, but does answer not seem clear how to do this correctly.
What I have currently is when the app starts I register the TransactionObserver,
IAPManager* iapManager = [[IAPManager alloc] init];
[[SKPaymentQueue defaultQueue] addTransactionObserver: iapManager];
Then when the user makes a purchase I have,
- (void) paymentQueue: (SKPaymentQueue *)queue updatedTransactions: (NSArray *)transactions {
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchasing:
[self showTransactionAsInProgress:transaction deferred:NO];
break;
case SKPaymentTransactionStateDeferred:
[self showTransactionAsInProgress:transaction deferred:YES];
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:transaction];
[queue finishTransaction: transaction];
break;
case SKPaymentTransactionStatePurchased:
[self persistPurchase: transaction];
[queue finishTransaction: transaction];
break;
case SKPaymentTransactionStateRestored:
[self restoreTransaction:transaction];
[queue finishTransaction: transaction];stopBusy];
break;
default:
break;
}
}
}
So this works fine when the user first subscribes. But I am confused on how you should store/track this purchase. I store in a static variable that the user subscribed to enable app functionality, but when the user restarts the app, how should I check that they have an active subscription?
The code I was using was storing the subscription in either iCloud or NSUserDefaults. Storing in iCloud did not work at all, and when they restarted the app they lost their subscription. Storing in NSUserDefaults works, but the subscription will eventually expired, and could be refunded or canceled. How to check if it is active? I could store the subscription date and assume the duration and try to check myself, but this seems very wrong.
Also what if the user uninstalls the app and reinstalls, or gets a new phone/etc.
- (void) persistPurchase: (SKPaymentTransaction*) transaction {
#if USE_ICLOUD_STORAGE
NSUbiquitousKeyValueStore *storage = [NSUbiquitousKeyValueStore defaultStore];
#else
NSUserDefaults *storage = [NSUserDefaults standardUserDefaults];
#endif
if ([transaction.payment.productIdentifier isEqualToString: SUBSCRIPTION]) {
[storage setBool: true forKey: SUBSCRIPTION];
[IAPManager upgrade];
}
[storage synchronize];
[self unlockPurchase: transaction];
}
For this I think I need to call restoreCompletedTransactions. I think it would make sense to not store the subscription in NSUserDefaults, but instead call restoreCompletedTransactions every time the app starts.
But from what I read Apple seems to want you to have a "Restore Purchases" button that does this? Does it make sense to just call it every time the app starts? This will call the callback with every payment you ever processed (I think??) for each of these payments how to know if they are still active?
I can get the transaction date but this does not tell me if the payment is expired or not, unless I assume it was not canceled and assume the duration and check the date myself? or does Apple only give you active subscriptions/payments from restoreCompletedTransactions?
Also do I need to call finishTransaction for the restore again?
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
Upvotes: 4
Views: 5127
Reputation: 21005
How can I check that it is still active when the user restarts the app?
https://developer.apple.com/documentation/storekit/in-app_purchase/persisting_a_purchase
For non-renewing subscriptions, use iCloud or your own server to keep a persistent record.
I think it is better to use a server (so you can receive refund notifications), rather than iCloud (you will not receive a notification).
I'm guessing you mean that you wish to check that:
I am assuming you use receipts. This is important, as you will need it to compute the duration of the subscription in case the user chooses to renew a non-renewing subscription before it expires.
So this works fine when the user first subscribes. But I am confused on how you should store/track this purchase. I store in a static variable that the user subscribed to enable app functionality, but when the user restarts the app, how should I check that they have an active subscription?
In my opinion, the correct way is to check records stored on your server. This is because, refund notifications will be sent by Apple to the URL you configure on the server.
This link explains how your server will be notified of refunds. https://developer.apple.com/documentation/storekit/in-app_purchase/handling_refund_notifications
As your app will not directly be notified by apple of a refund, you must check with your server if a refund has been issued or not. How you communicate with your server is up to you (polling periodically, push notifications etc). The point is, you should store a record on your server, and communicate that a refund has been issued to your app, so you can decide whether to revoke the feature or not. Since you need to store a record on your server to check for refunds, you may as well ask your own server if a subscription has expired or not. To do this, you will simply have to store an identifier in your App (NSUserDefaults etc) so that you can ask your server if the period has expired.
This is better than simply storing the end date of the subscription in your app, because if you store the end date in an insecure manner, a user can simply edit the file and keep extending the end date. Apple states in the following link :
Storing a receipt requires more application logic but prevents the persistent record from being tampered with. As you need to implement the server to check for refunds, it is trivial to use the server to check if the subscription has expired.
Storing in NSUserDefaults works, but the subscription will eventually expired, and could be refunded or canceled. How to check if it is active? I could store the subscription date and assume the duration and try to check myself, but this seems very wrong.
.....
Yes, in my opinion storing the subscription date on your device is not the best way for several reasons:
The correct way is to query your server. That way he won't be able to edit the time on his device to trick your app into extending his subscription. Neither will he be able to "edit" the subscription date if you've stored it insecurely. The app should simply send an identifier to your server and receive a yes or no response as to whether the subscription is active or not. The following link describes how to do so:
Send a copy of the receipt to your server along with credentials or an identifier so you can keep track of which receipts belong to a particular user. For example, let users identify themselves to your server with a username and password. Don't use the identifierForVendor property of UIDevice. Different devices have different values for this property, so you can't use it to identify and restore purchases made by the same user on a different device.
..
For this I think I need to call restoreCompletedTransactions. I think it would make sense to not store the subscription in NSUserDefaults, but instead call restoreCompletedTransactions every time the app starts.
But from what I read Apple seems to want you to have a "Restore Purchases" button that does this? Does it make sense to just call it every time the app starts? This will call the callback with every payment you ever processed (I think??) for each of these payments how to know if they are still active?
No, it is not a good idea to call restore purchases every time your app starts. Apple explicitly says not to:
https://developer.apple.com/documentation/storekit/in-app_purchase/restoring_purchased_products
Important Don't automatically restore purchases, especially when your app is launched. Restoring purchases prompts for the user’s App Store credentials, which interrupts the flow of your app.
It also says:
However, an app might require an alternative approach under the given circumstances:... Your app uses non-renewing subscriptions — Your app is responsible for the restoration process.
In my opinion, it is unnecessary to restore purchases every time your app starts up - restoring purchases is intended for if you reinstall / install on a new device. It also won't tell you if the subscription has been refunded or active anyway, since Apple will notify only your server URL if a refund has been issued. What you should be doing each time your app starts up, is to query your server. Apple itself says to keep balances for user accounts updated on your server to identify refund abuse.
Reduce refund abuse and identify repeated refunded purchases by mapping REFUND notifications to the player accounts on your server. Monitor and analyze your data to identify suspicious refund activity. If you offer content across multiple platforms, keep the balances for user accounts updated on your server. Use App Store Server notifications to get near real-time status updates for the transactions that affect your customers.
You should implement the button to "restore purchases", to handle the case that a user uninstalls / reinstalls an app / installs on a different device. When the user restores a purchase, you should be able to compute the identifier you need to talk to your server to check if the subscription is still active or not (You can probably just use the original transaction id) Your receipt will contain both the original purchase date, as well as product id, and will be updated every time a non-renewing subscription is purchased. You can access this data by making a call to refresh the receipt (SKReceiptRefreshRequest) so any device has the receipt, and you can compute the subscription period.
I can get the transaction date but this does not tell me if the payment is expired or not, unless I assume it was not canceled and assume the duration and check the date myself? or does Apple only give you active subscriptions/payments from restoreCompletedTransactions?
When you implement your subscription behavior, the first time the subscription is initiated you should store the expiry date on your server. As mentioned earlier, Apple will send the refund notification to your server, so you should have your app check with your server if the subscription is refunded or expired. Also remember - the user can change his devices time / date to get around you storing the expiry date on the device. But if you're checking with your own server he cannot, since he cannot tamper with the time on your server.
Also do I need to call finishTransaction for the restore again?
I use swift, and in swift you simply call restoreCompletedTransactions(). It is easy to implement.
See: How to restore in-app purchases correctly?
At this point, you may be wondering, if the receipt contains a record of every single non-renewing subscription purchase, then why do I need a server for checking if a subscription is active ?
Remember:
Every time your user purchases a non-renewing subscription, you should ensure the logic on your server computes the correct end date. For example, if he chooses to purchase a non-renewing subscription before his current subscription ends, you should correctly compute the end date by letting the next subscription start only when the current one ends.
Upvotes: 7
Reputation: 474
The documentation explicitly states that to persist a purchase of a non-renewing subscription, "use iCloud or your own server to keep a persistent record". So basically Apple does not provide a built-in way to track in app purchases against users / installs, you have to roll your own.
As you figured out, storing on-device (e.g. using NUserDefaults) is not very useful, as it wouldn't survive a re-install by the same user, on the same or a different device.
If you already have an identity provider backend which can accomodate storing and retrieving the users in-app purchases data, you may want to use that. For each successful purchase, associate the SKU, expiry date and receipt against the user (Your server at this point should check that the receipt is valid to avoid fraud). When your app starts or resume, authenticate with your backend and retrieve the SKUs the user should have access to, and deliver the content in-app.
If you do not have an identity provider backend, you may want to use iCloud key-value store. You can then associate in-app purchases to apple ID, not per-install. Note that using iCloud has some side-effect, for example you may not be able to ever transfer your app to another organisation.
Upvotes: 0
Reputation: 1162
There a lightweight iOS library for In-App Purchases called RMStore. RMStore supports transaction persistence and provides the reference implementations. You can check it in below link for more details.
https://github.com/robotmedia/RMStore
Upvotes: 0