About In-App Purchase: 5. Delivering Products
The summary of the official documentation of Apple Developer, About In-App Purchase.
Purchase process 3): Delivering Products
Your app
– waits for the App Store to process the payment request,
– stores information about the purchase for future launches,
– downloads the purchased content,
– marks the transaction as finished
Waiting for the App Store to Process Transactions
The transaction queue
– lets your app communicate with the App Store through the StoreKit framework.
– to which you add work that the App Store needs, e.g. payment request.
When the transaction’s state changes (e.g. when a payment request succeeds),
– StoreKit calls the app’s transaction queue observer.
In very small apps,
=> you could handle all the StoreKit logic in the app delegate, including observing the transaction queue.
In most apps,
=> you create a separate class that handles this observer logic along with the rest of your app’s store logic.
SKPaymentTransactionObserver (protocol)
– The observer must conform to
——
Using an observer: your app doesn’t constantly poll the status of its active transactions.
Your app also uses the transaction queue
– for payment requests,
– to download Apple-hosted content
– to find out that subscriptions have been renewed.
——
when your app is launched
– Register a transaction queue observer [4-1].
– Make sure that the observer is ready to handle a transaction at any time, not just after you add a transaction to the queue.
e.g. a user bought something in your app right before going into a tunnel.
=> Your app isn’t able to deliver the purchased content because there’s no network connection.
The next time your app is launched:
StoreKit
– calls your transaction queue observer again
– delivers the purchased content
e.g. if your app fails to mark a transaction as finished
=> StoreKit calls the observer every time your app is launched until the transaction is properly finished.
Listing 4-1 Registering the transaction queue observer
1 2 3 4 5 6 7 |
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { /* ... */ [[SKPaymentQueue defaultQueue] addTransactionObserver:observer]; } |
paymentQueue:updatedTransactions: (method)
– a method called by StoreKit.
– Implement it on your transaction queue observer.
– StoreKit calls when the status of a transaction changes
e.g. when a payment request has been processed.
The transaction status
– tells you what action your app needs to perform [4-1] [4-2]
– Transactions in the queue can change it in any order.
Your app needs to be ready to work on any active transaction at any time.
Table 4-1 Transaction statuses and corresponding actions
Listing 4-2 Responding to transaction statuses
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { for (SKPaymentTransaction *transaction in transactions) { switch (transaction.transactionState) { // Call the appropriate custom method for the transaction state. case SKPaymentTransactionStatePurchasing: [self showTransactionAsInProgress:transaction deferred:NO]; break; case SKPaymentTransactionStateDeferred: [self showTransactionAsInProgress:transaction deferred:YES]; break; case SKPaymentTransactionStateFailed: [self failedTransaction:transaction]; break; case SKPaymentTransactionStatePurchased: [self completeTransaction:transaction]; break; case SKPaymentTransactionStateRestored: [self restoreTransaction:transaction]; break; default: // For debugging NSLog(@"Unexpected transaction state %@", @(transaction.transactionState)); break; } } } |
SKPaymentTransactionObserver (protocol)
– from which the transaction queue observer can implement optional methods
– keeps your user interface up to date while waiting as follows.
paymentQueue:removedTransactions: (method)
– is called when transactions are removed from the queue
– remove the corresponding items from your app’s UI.
paymentQueueRestoreCompletedTransactionsFinished: (method)
paymentQueue:restoreCompletedTransactionsFailedWithError: (method)
– is called when StoreKit finishes restoring transactions, depending on whether there was an error.
In your implementation of these methods,
=> update your app’s UI to reflect the success or error.
Persisting the Purchase
After making the product available,
your app
– needs to make a persistent record of the purchase.
– uses that persistent record on launch to continue to make the product available.
– uses that record to restore purchases
– whose persistence strategy depends the type of products you sell and the versions of iOS.
—-
– For non-consumable products and auto-renewable subscriptions
[iOS 7 and later] => use the app receipt as your persistent record.
[earlier than iOS 7] => use the User Defaults system or iCloud to keep a persistent record.
– For non-renewing subscriptions,
=> use iCloud or your own server to keep a persistent record.
– For consumable products
=> your app updates its internal state to reflect the purchase, but there’s no need to keep a persistent record because consumable products aren’t restored or synced across devices.
Ensure that
– the updated state is part of an object that supports state preservation (in iOS)
– you manually preserve the state across app launches (in iOS or macOS).
—-
When using the User Defaults system or iCloud,
=> your app can store a value, such as a number or a Boolean, or a copy of the transaction receipt.
In macOS,
=> the user can edit the User Defaults system using the defaults command.
Storing a receipt requires more application logic,
but prevents the persistent record from being tampered with.
When persisting via iCloud,
=> note that your app’s persistent record is synced across devices,
but your app is responsible for downloading any associated content on other devices.
Persisting Using the App Receipt
The app receipt contains
– a record of the user’s purchases,
– cryptographically signed by Apple.
Information about consumable products
– is added when they’re paid for and remains in the receipt until you finish the transaction.
– is removed the next time the receipt is updated—for example, the next time the user makes a purchase after you finish the transaction
Information about all other kinds of purchases
– is added to the receipt when they’re paid for
– remains in the receipt indefinitely.
Persisting a Value in User Defaults or iCloud
Set the value for a key
– to store information in User Defaults or iCloud
1 2 3 4 5 6 7 8 |
#if USE_ICLOUD_STORAGE NSUbiquitousKeyValueStore *storage = [NSUbiquitousKeyValueStore defaultStore]; #else NSUserDefaults *storage = [NSUserDefaults standardUserDefaults]; #endif [storage setBool:YES forKey:@"enable_rocket_car"]; [storage setObject:@15 forKey:@"highest_unlocked_level"]; [storage synchronize]; |
Persisting a Receipt in User Defaults or iCloud
Set the value for a key to the data of that receipt
– to store a transaction’s receipt in User Defaults or iCloud
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#if USE_ICLOUD_STORAGE NSUbiquitousKeyValueStore *storage = [NSUbiquitousKeyValueStore defaultStore]; #else NSUserDefaults *storage = [NSUserDefaults standardUserDefaults]; #endif NSData *newReceipt = transaction.transactionReceipt; NSArray *savedReceipts = [storage arrayForKey:@"receipts"]; if (!savedReceipts) { // Storing the first receipt [storage setObject:@[newReceipt] forKey:@"receipts"]; } else { // Adding another receipt NSArray *updatedReceipts = [savedReceipts arrayByAddingObject:newReceipt]; [storage setObject:updatedReceipts forKey:@"receipts"]; } [storage synchronize]; |
Persisting Using Your Own Server
Send a copy of the receipt to your server along with some kind of credentials or identifier
=> so you can keep track of which receipts belong to a particular user.
e.g. let users identify themselves to your server with an email or user name, plus a password.
Don’t use the identifierForVendor property of UIDevice
– you can’t use it to identify and restore purchases made by the same user on a different device,
– different devices have different values for this property.
Unlocking App Functionality
If the product enables app functionality,
set a Boolean value to
– enable the code path
– update your user interface as needed.
Consult the persistent record that your app made when the transaction occurred.
– to determine what functionality to unlock,
Your app needs to update this Boolean value
– whenever a purchase is completed
– at app launch.
e.g. using the app receipt:
1 2 3 4 5 |
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL]; // Custom method to work with receipts BOOL rocketCarEnabled = [self receipt:receiptData includesProductID:@"com.example.rocketCar"]; |
e.g. using the User Defaults system:
1 2 |
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; BOOL rocketCarEnabled = [defaults boolForKey:@"enable_rocket_car"]; |
Then use that information
– to enable the appropriate code paths in your app.
1 2 3 4 5 |
if (rocketCarEnabled) { // Use the rocket car. } else { // Use the regular car. } |
Delivering Associated Content
If the product has associated content, your app needs to deliver that content to the user.
e.g. purchasing a level in a game requires delivering the files that define that level
e.g. purchasing additional instruments in a music app requires delivering the sound files needed to let the user play those instruments.
– You can embed that content in your app’s bundle
– you can download it as needed
=> Each approach has its advantages and disadvantages.
If you include too little content in your app bundle,
– the user must wait for even small purchases to be downloaded.
If you include too much in your app bundle,
– the initial download of the app takes too long
– the space is wasted for users who don’t purchase the corresponding products.
– users won’t be able to download it over cellular networks.
Embed smaller files (up to a few megabytes) in your app, especially if you expect most users to buy that product.
[+] Content in your app bundle can be made available immediately when the user purchases it.
[-] To add or update content in your app bundle, you have to submit an updated version of your app.
Download larger files when needed.
– Separating content from your app bundle keeps your app’s initial download small.
e.g. a game can include the first level in its app bundle and let users download the rest of the levels when they’re purchased.
– Assuming your app fetches its list of product identifiers from your server,
– not hard-coded in the app bundle,
– you don’t need to resubmit your app to add or update content that is downloaded by your app.
In iOS 6 and later,
– most apps should use Apple-hosted content for downloaded files.
– You create an Apple-hosted content bundle using the In-App Purchase Content target in Xcode and submit it to iTunes Connect.
– When you host content on Apple’s servers you don’t need to provide any servers
– your app’s content is stored by Apple using the same infrastructure that supports other large-scale operations, such as the App Store.
– Apple-hosted content automatically downloads in the background even if your app isn’t running.
You might choose to host your own content if you already have server infrastructure,
– if you need to support older versions of iOS,
– if you share your server infrastructure across multiple platforms.
Note:
You can’t patch your app binary or download executable code.
– Your app must contain all executable code needed to support all of its functionality when you submit it.
– If a new product requires code changes, submit an updated version of your app.
Loading Local Content
Load local content using the NSBundle class,
– just as you load other resources from your app bundle.
1 2 3 |
NSURL *url = [[NSBundle mainBundle] URLForResource:@"rocketCar" withExtension:@"plist"]; [self loadVehicleAtURL:url]; |
Downloading Hosted Content from Apple’s Server
When the user purchases a product that has associated Apple-hosted content,
– the transaction passed to your transaction queue observer also includes an instance of SKDownload that lets you download the associated content.
—-
To download the content,
– add the download objects from the transaction’s downloads property to the transaction queue
– by calling the startDownloads: method of SKPaymentQueue.
If the value of the downloads property is nil,
– there’s no Apple-hosted content for that transaction.
Unlike downloading apps,
– downloading content doesn’t automatically require a Wi-Fi connection for content larger than a certain size.
– Avoid using cellular networks to download large files without an explicit action from the user.
—-
Implement the paymentQueue:updatedDownloads: method on the transaction queue observer
– to respond to changes in a download’s state—for example, by updating progress in your UI.
– If a download fails, use the information in its error property to present the error to the user.
—-
Ensure that your app handles errors gracefully.
e.g. if the device runs out of disk space during a download,
– give the user the option
– to discard the partial download or
– to resume the download later when space becomes available.
—-
Update your user interface while the content is downloading
– using the values of the progress and timeRemaining properties.
– You can use the pauseDownloads:, resumeDownloads:, and cancelDownloads: methods of SKPaymentQueue from your UI to let the user control in-progress downloads.
– Use the downloadState property to determine whether the download has completed. – Don’t use the progress or timeRemaining property of the download object to check its status—these properties are for updating your UI.