How to convert from paid to free with IAP

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 or SKPaymentQueue.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/

receipt

You can also watch WWDC 2017, session Advanced StoreKit for more detail https://developer.apple.com/videos/play/wwdc2017/305/

receipt tips

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

Read SKReceiptRefreshRequest

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

1
2
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

1
2
3
4
5
6
7
8
9
10
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.

1
try InAppReceipt.localReceipt()

Check Receipt Fields

1
2
3
4
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

1
2
3
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

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
30
31
32
33
34
35
36
{
"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.

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
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.

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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.

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
30
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

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
30
31
32
33
34
35
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

Comments