Issue #703
What is receipt
Read When to refresh a receipt vs restore purchases in iOS?
From iOS 7, every app downloaded from the store has a receipt (for downloading/buying the app) at appStoreReceiptURL
. When users purchases something via In App Purchase, the content at appStoreReceiptURL
is updated with purchases information. Most of the cases, you just need to refresh the receipt (at appStoreReceiptURL) so that you know which transactions users have made.
Note
- Receipt is generated and bundled with your app when user download the app, whether it is free or paid
- When user makes IAP, receipt is updated with IAP information
- When user downloads an app (download free, or purchase paid app), they get future updates (whether free or paid) forever.
- Call
SKReceiptRefreshRequest
orSKPaymentQueue.restoreCompletedTransactions
asks for Appstore credential - When we build the app from Xcode or download from Testflight, receipt is not bundled within the app since the app is not downloaded from AppStore. We can use
SKReceiptRefreshRequest
to download receipt from sandbox Appstore restoreCompletedTransactions
updates app receipt- Receipt is stored locally on device, so when user uninstalls and reinstalls your app, there’s no in app purchases information, this is when you should refresh receipt or
restoreCompletedTransactions
Users restore transactions to maintain access to content they’ve already purchased. For example, when they upgrade to a new phone, they don’t lose all of the items they purchased on the old phone. Include some mechanism in your app to let the user restore their purchases, such as a Restore Purchases button. Restoring purchases prompts for the user’s App Store credentials, which interrupts the flow of your app: because of this, don’t automatically restore purchases, especially not every time your app is launched.
In most cases, all your app needs to do is refresh its receipt and deliver the products in its receipt. The refreshed receipt contains a record of the user’s purchases in this app, on this device or any other device. However, some apps need to take an alternate approach for one of the following reasons:
- If you use Apple-hosted content, restoring completed transactions gives your app the transaction objects it uses to download the content. If you need to support versions of iOS earlier than iOS 7, where the app receipt isn’t available, restore completed transactions instead.
- Refreshing the receipt asks the App Store for the latest copy of the receipt. Refreshing a receipt does not create any new transactions.
- Restoring completed transactions creates a new transaction for every completed transaction the user made, essentially replaying history for your transaction queue observer.
More about receipt, from WWDC 2017, What’s new in StoreKit session https://developer.apple.com/videos/play/wwdc2017/303/
You can also watch WWDC 2017, session Advanced StoreKit for more detail https://developer.apple.com/videos/play/wwdc2017/305/
Restoring Purchased Products
Read Restoring Purchased Products
Users sometimes need to restore purchased content, such as when they upgrade to a new phone.
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
In most cases, you only need to refresh the app receipt and deliver the products listed on the receipt. The refreshed receipt contains a record of the user’s purchases in this app, from any device the user’s App Store account is logged into
Refreshing a receipt doesn’t create new transactions; it requests the latest copy of the receipt from the App Store
Restoring completed transactions creates a new transaction for every transaction previously completed, essentially replaying history for your transaction queue observer. Your app maintains its own state to keep track of why it’s restoring completed transactions and how to handle them.
What are the different IAP types
From AppStore
Consumable (pay everytime)
A consumable In-App Purchase must be purchased every time the user downloads it. One-time services, such as fish food in a fishing app, are usually implemented as consumables.
Non-Consumable (one time payment)
Non-consumable In-App Purchases only need to be purchased once by users. Services that do not expire or decrease with use are usually implemented as non-consumables, such as new race tracks for a game app.
Auto-Renewable Subscriptions (will deduct money from your credit card on a cycle complete)
Auto-renewable Subscriptions allow the user to purchase updating and dynamic content for a set duration of time. Subscriptions renew automatically unless the user opts out, such as magazine subscriptions.
Free Subscription (no payment and is still visible even you did not submitted your account detail to itunes connect)
Free subscriptions are a way for developers to put free subscription content in Newsstand. Once a user signs up for a free subscription, it will be available on all devices associated with the user’s Apple ID. Note that free subscriptions do not expire and can only be offered in Newsstand-enabled apps.
Non-Renewing (need to renew manually)
Subscription Non-Renewing Subscriptions allow the sale of services with a limited duration. Non-Renewing Subscriptions must be used for In-App Purchases that offer time-based access to static content. Examples include a one week subscription to voice guidance feature within a navigation app or an annual subscription to online catalog of archived video or audio.
When is app receipt missing
Use this API to request a new receipt if the receipt is invalid or missing
Receipt is stored locally on device. It can be missing in case user sync or restore device.
Watch WWDC 2014 - 305 Preventing Unauthorized Purchases with Receipts
How to check receipt existence
Bundle.main.appStoreReceiptURL
checkResourceIsReachable
How to read receipt
Read In-App Purchases: Receipt Validation Tutorial
The receipt consists of a single file in the app bundle. The file is in a format called PKCS #7. The payload consists of a set of receipt attributes in a cross-platform format called ASN.1
case 12: // Receipt Creation Date
var dateStartPtr = ptr
receiptCreationDate = readASN1Date(ptr: &dateStartPtr, maxLength: length)
case 17: // IAP Receipt
print("IAP Receipt.")
case 19: // Original App Version
var stringStartPtr = ptr
originalAppVersion = readASN1String(ptr: &stringStartPtr, maxLength: length)
Use TPInAppReceipt which includes certificates.
try InAppReceipt.localReceipt()
Check Receipt Fields
Original Application Version
The version of the app that was originally purchased.
ASN.1 Field Type 19
ASN.1 Field Value UTF8STRING
Note
This corresponds to the value of CFBundleVersion (in iOS) or CFBundleShortVersionString (in macOS) in the Info.plist file when the purchase was originally made
CFBundleVersion
is build number, and CFBundleShortVersionString
is app version
In-App Purchase Receipt
The receipt for an in-app purchase.
ASN.1 Field Type 17
Read Validating Receipts with the App Store
Sample verifyReceipt
json
{
"receipt": {
"receipt_type": "ProductionSandbox",
"adam_id": 0,
"app_item_id": 0,
"bundle_id": "com.example.app.ios",
"application_version": "3",
"download_id": 0,
"version_external_identifier": 0,
"receipt_creation_date": "2018-11-13 16:46:31 Etc/GMT",
"receipt_creation_date_ms": "1542127591000",
"receipt_creation_date_pst": "2018-11-13 08:46:31 America/Los_Angeles",
"request_date": "2018-11-13 17:10:31 Etc/GMT",
"request_date_ms": "1542129031280",
"request_date_pst": "2018-11-13 09:10:31 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": "test2",
"transaction_id": "1000000472106082",
"original_transaction_id": "1000000472106082",
"purchase_date": "2018-11-13 16:46:31 Etc/GMT",
"purchase_date_ms": "1542127591000",
"purchase_date_pst": "2018-11-13 08:46:31 America/Los_Angeles",
"original_purchase_date": "2018-11-13 16:46:31 Etc/GMT",
"original_purchase_date_ms": "1542127591000",
"original_purchase_date_pst": "2018-11-13 08:46:31 America/Los_Angeles",
"is_trial_period": "false"
}]
},
"status": 0,
"environment": "Sandbox"
}
Verify your receipt first with the production URL; then verify with the sandbox URL if you receive a 21007 status code. This approach ensures you do not have to switch between URLs while your application is tested, reviewed by App Review, or live in the App Store.
Show me the code
Let’s use enum to represent possible states for each resource. Here’s simple case where we only have 1 non consumable IAP product.
enum IAPError: Error {
case failedToRefreshReceipt
case failedToRequestProduct
case failedToPurchase
case receiptNotFound
}
enum IAPResourceState<T> {
case notAsked
case loading
case success(T)
case failure(IAPError)
}
final class PricingPlan: ObservableObject {
static let pro = (Bundle.main.bundleIdentifier ?? "") + ".pro"
@Published
var isPro: Bool = false
@Published
var product: IAPResourceState<SKProduct> = .notAsked
@Published
var purchase: IAPResourceState<SKPayment> = .notAsked
@Published
var receipt: IAPResourceState<InAppReceipt> = .notAsked
}
Let’s have a central place for managing all IAP operations, called IAPManager
, it can update our ObservableObject
PricingPlan
hence triggers update to SwiftUI.
import StoreKit
import TPInAppReceipt
import Version
final class IAPManager: NSObject {
private let pricingPlan: PricingPlan
private let paymentQueue: SKPaymentQueue
init(pricingPlan: PricingPlan) {
self.pricingPlan = pricingPlan
self.paymentQueue = SKPaymentQueue.default()
super.init()
self.paymentQueue.add(self)
}
func requestProducts() {
let identifiers = PricingPlan.pro
let request = SKProductsRequest(productIdentifiers: Set(arrayLiteral: identifiers))
request.delegate = self
pricingPlan.product = .loading
request.start()
}
func purchase(product: SKProduct) {
guard SKPaymentQueue.canMakePayments() else {
showAlert(text: "You are not allowed to make payment. Please check device settings.")
return
}
pricingPlan.purchase = .loading
let payment = SKPayment(product: product)
paymentQueue.add(payment)
}
func refreshReceipt() {
let request = SKReceiptRefreshRequest()
request.delegate = self
request.start()
}
func restorePurchase() {
paymentQueue.restoreCompletedTransactions()
}
}
Refresh receipt
You can use restoreCompletedTransactions
if you simply finishTransaction
and grant user pro feature, like in this simple tutorial In-App Purchase Tutorial: Getting Started, search for SKPaymentTransactionObserver
. restoreCompletedTransactions
also updates receipt.
Otherwise refreshing receipt is a better idea. It serves both case when receipt is not there locally and when you want to restore transactions. With receipt refreshing, no restored transactions are created and SKPaymentTransactionObserver
is not called, so we need to check receipt proactively.
Either restoreCompletedTransactions
or SKReceiptRefreshRequest
asks for AppStore credential so you should present a button there and ask user.
Check local receipt
Try to locate local receipt and examine it.
- If it is not there (missing, corrupted), refresh receipt
- If it’s there, check if it was from a version when the app was still as paid. Notice the difference in meaning of
originalAppVersion
in macOS and iOS - If it is not paid, check if this receipt contains In App Purchase information for our product
In practice, we need to perform some basic checks on receipt, like bundle id, app version, device id. Read In-App Purchases: Receipt Validation Tutorial, search for Validating the Receipt
. TPInAppReceipt
also has some handy verify
functions
Besides verifying receipt locally, it is advisable to call verifyreceipt either on device, or better on serve to let Apple verify receipt and returns you a human readable json for receipt information.
func checkReceipt() {
DispatchQueue.main.async {
do {
let receipt = try InAppReceipt.localReceipt()
self.pricingPlan.receipt = .success(receipt)
if self.isPaid(receipt: receipt) {
self.pricingPlan.isPro = true
} else if receipt.containsPurchase(ofProductIdentifier: PricingPlan.pro) {
self.pricingPlan.isPro = true
}
} catch {
self.pricingPlan.receipt = .failure(.receiptNotFound)
}
}
}
private func isPaid(receipt: InAppReceipt) -> Bool {
#if os(macOS)
// originalAppVersion is CFBundleShortVersionString
if let version = Version(receipt.originalAppVersion) {
return version < versionToIAP
}
#else
// originalAppVersion is CFBundleVersion
if let buildNumber = Int(receipt.originalAppVersion) {
return buildNumber < buildNumberToIAP
}
#endif
return false
}
Finally, observe SKProductsRequestDelegate
which also conforms to SKRequestDelegate
for both product and receipt refresh request
extension IAPManager: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
DispatchQueue.main.async {
guard let product = response.products.first else {
self.pricingPlan.product = .failure(IAPError.failedToRequestProduct)
return
}
self.pricingPlan.product = .success(product)
}
}
func request(_ request: SKRequest, didFailWithError error: Error) {
DispatchQueue.main.async {
switch request {
case is SKProductsRequest:
self.pricingPlan.product = .failure(IAPError.failedToRequestProduct)
case is SKReceiptRefreshRequest:
self.pricingPlan.receipt = .failure(IAPError.failedToRefreshReceipt)
default:
break
}
}
}
func requestDidFinish(_ request: SKRequest) {
switch request {
case is SKReceiptRefreshRequest:
checkReceipt()
default:
break
}
}
}
Updated at 2020-12-04 06:58:17