In previous posts we have talked about how to setup products in iTunes Connect, how to write code to retrieve the list of products, how to implement the buying transaction and how to download content. Today we are going to talk about how to restore products.
Giving your user a way to restore products is very important and Apple requires that your app implements this. The most common way to implement the restore mechanism is to have a “Restore Purchases” button. It’s not a good idea to try to automatically restore products when the app launches. Restoring purchases prompts for the user’s App Store credentials and it can get very annoying to have to enter your details every time the app launches.
Depending on what kind of products you have configured and where they are stored, there are different things you need to do. In our example we use Apple-hosted content and this makes our life easy. All we need to do is call restoreCompletedTransaction on the default payment queue and use the transaction object to download the content.
Setup Restore Purchases button
Let’s start by adding a “Restore Purchases” button. It would be nice to have it in the table view footer, underneath all the products. In order to do this we have to create a separate xib file and add the button to it and then let the table view know that we want it to use our footer.
First create a new xib file. I called it RestoreFooterView.
Change the height of the view and make sure it doesn’t show a status bar or top bar. Then add a button to it, change its title to “Restore Purchases” and add some constraints so that it will always be centered in the view. Your view should look something like this:
You will also need to create the corresponding Swift file ( I haven’t found a way to create a Swift file and have “Also create nib file” checkbox so I guess that’s not an option anymore). Make sure you import UIKit and make your class inherit from UITableViewHeaderFooterView.
Now go to the MasterViewController file. In viewDidLoad: we want to register the xib file as a footer view for the table view. Add the following code:
let footerNib = UINib(nibName: "RestoreFooterView", bundle: nil) tableView.registerNib(footerNib, forHeaderFooterViewReuseIdentifier: "RestoreFooter") tableView.tableFooterView = tableView.dequeueReusableHeaderFooterViewWithIdentifier("RestoreFooter") as? RestoreFooterView
All you’re doing is register the xib file and then dequeue the view and set it as a footer view for the table view. If you run the project you will see the button appear under the product list. It doesn’t look very pretty and we could add borders to it so that it looks more consistent with the buy button, but we’re not going to focus on that right now.
The button is displaying but it’s not doing anything yet. So let’s add an action for it. Go to the xib file, bring up the Assistant Editor and add an action:
The code to trigger the restore looks like this:
@IBAction func restoreButtonPressed(sender: UIButton) { SKPaymentQueue.defaultQueue().restoreCompletedTransactions() }
Implementing the restore functionality
The code that we’ve added in the action method for the button sends a request to the Apple Store to restore all the completed transactions for this app. The App Store will generate a new transaction for each transaction that was previously completed. The transaction queue observer will be called with a status of Restored for each of the restore transactions. What we’d want to do at this point is download content again and make it available to the user.
Go to the PaymentTransactionObserver and notice that we have a placeholder method for the Restored status of the transaction. Let’s add some code in this method:
NSNotificationCenter.defaultCenter().postNotificationName(IAPTransactionRestore, object: transaction) if let downloads = transaction.downloads { SKPaymentQueue.defaultQueue().startDownloads(downloads) }
We’re not doing anything fancy, just posting the notification and starting the download. We don’t need to add any code to handle the content download. We have everything in place from the complete status for the transaction.
At this point restoring the purchases should work, but it doesn’t really look well and it’s not efficient. It doesn’t look well because we’re not disabling the “Restore Purchases” button or showing that something is happening so the user can press it lots and lots of times and that will create lots of requests to the App Store and our app will get inundated by restore transactions. We really want to avoid this, so let’s find a way to prevent it.
Improving the user experience
We’ll use the same strategy as for the buy button: just add an activity indicator and hide the button. We want to do this in the RestoreFooterView.xib file and add some outlets to the Swift file. And when the restore button is pressed just hide the button and start the activity indicator:
@IBAction func restoreButtonPressed(sender: UIButton) { sender.hidden = true activityIndicator.startAnimating() SKPaymentQueue.defaultQueue().restoreCompletedTransactions() }
This is all good, but how do we know when to stop the activity indicator and show the button again? We’re going to create some methods and allow the view controller to setup the state for the view.
func downloadFinished() { activityIndicator.stopAnimating() restoreButton.hidden = false }
We want this method to be called when the download of restored content is finished. For this we need to make some changes to the MasterViewController. First we need to have a variable for the footer view:
var footerView: RestoreFooterView? = nil
Next make sure you set this to the dequed view. In viewDidLoad update the code to look like this:
let footerNib = UINib(nibName: "RestoreFooterView", bundle: nil) tableView.registerNib(footerNib, forHeaderFooterViewReuseIdentifier: "RestoreFooter") footerView = tableView.dequeueReusableHeaderFooterViewWithIdentifier("RestoreFooter") as? RestoreFooterView tableView.tableFooterView = footerView
Now we want to know when the restore is completed. This can be a bit tricky. We don’t know how many transactions we’ve called start download on, so we don’t know if the restore is finished when we receive one download complete notification or several. One way to solve this is to register for the transaction restore notification and keep count of how many times we receive this notification. If we receive 2 transaction restore notifications then we can say the restore is complete when we got 2 download complete or download failed notifications. So let’s add some code to implement this. First subscribe to notifications:
func signUpForNotifications() { NSNotificationCenter.defaultCenter().addObserver(self, selector: "receivedRestoreTransactionNotification:", name: IAPTransactionRestore, object: nil) NSNotificationCenter.defaultCenter().addObserver(self, selector: "receivedDownloadFinishedNotification:", name: IAPDownloadFinished, object: nil) NSNotificationCenter.defaultCenter().addObserver(self, selector: "receivedDownloadFailedNotification:", name: IAPDownloadFailed, object: nil) }
Make sure you call this method in viewDidLoad. Add a deinit method to unsubscribe.
We will use 2 variables to keep count of the restore transaction and download notifications. We could use one and just increase or decrease it as needed, but I find it cleared with two. Feel free to choose your own implementation. When we receive restore transaction notification we increase the first variable. When we receive a download finished notification we increase the second variable and check to see if we have a match for the two values. If yes the we call the method downloadFinished on the footer view to update the UI. We also reset the variables. If the download failed we do the same thing but also show an error message. This should be enough to stop the user pressing that button over and over again.
What if we don’t want to restore all purchases? What if we bough all 3 photos products but we only want to restore one of them? There is not method on the payment queue that lets you specify which product you want to restore. However, there is a way. The way to do this is by filtering the restore transaction after you have called restoreCompletedTransactions.
Let’s assume that you have added a “Restore” button in the detail view of each product. Instead of pressing “Restore Purchases” your user will press the restore button for a certain product. At this point you have to find a way to save the user’s choice. One simple way to do that would be to use user defaults to save the product ID (remember that you have an instance of a SKProduct in your details view, so use it). So when you receive the status Restored for a transaction you won’t start the download of content unless it’s the right product. Add this check in the restoreTransaction method of your PaymentTransactionObserver:
if productId == transaction.payment.productIdentifier { //only then download content }
The productId is the one saved in user defaults.
Your app will get rejected if you don’t have a restore button for your purchases, so be sure to add it. As you can see it’s pretty easy to implement and it will keep your users happy.
Resources
This is the final post about In-App Purchase. I hope you enjoyed the posts and found them useful. I have only covered all the steps for non-consumable products. If you want to know more about In-App Purchase there are many wonderful resources. Here are the ones I like most:
- WWDC videos: Session 218 – Designing a Great In-App Purchase Experience, Session 303 – Optimizing In-App Purchases and Session 305 – Preventing Unauthorized Purchases with Receipts
- Apple’s In-App Purchase Programming Guide
You can find the full project on Github. Thank you for reading!