Apple In-App Purchases

Overview

Apple mandates that payments for all digital goods within iOS apps be completed using their IAP platform. Digital goods include access to premium features as well as consumable tokens/credits.

In-app purchases provide additional channels to monetize your app. iOS contains these types of in-app purchases:

  • Consumables: can be purchased multiple times, e.g. 100 digital coins
  • Non-consumables: can only be purchased once, e.g. a particular character skin, or permanent advertising removal
  • Non-renewable subscriptions: lasts for a fixed amount of time and then expires, e.g. 1-year premium access
  • Renewable subscriptions: automatically renews, e.g. monthly premium access

Generally, if your app accepts payment for any digital goods or memberships, Apple’s rules require you to accept in-app purchases as the only payment method within your mobile app.

GoNative’s in-app purchase flow has these steps:

  1. Create IAP in iTunes Connect
  2. App gets list of purchasable items from your site and verifies with store
  3. Site shows UI for purchasable items
  4. User starts purchase.
  5. Purchase is verified and fulfilled

If using server-side verification, app posts receipt to your site. Web server verifies receipt with Apple and fulfills purchase.
If using client-side verification, purchase data is available via JavaScript.

Implementation Guide

Once the premium module has been added to your app, you may use the following GoNative JavaScript Bridge commands to access its functionality.

Create your app on iTunes connect, picking a globally unique bundle ID.

Open your new app on iTunes connect, go to Features -> In-App Purchases.

Create your In-App purchase. The Product ID is a string used to identify the IAP. For auto-renewing types, there is a concept of "subscription groups". Users can only be subscribed to one IAP in each subscription group. For example, you may offer a monthly or semiannual membership. It would only make sense for a user to be subscribed to one, not both. There is additional information used to describe the IAP, and notes for Apple's review. You must create a user-friendly description in at least one localized language.

At the IAP screen, click "View Shared Secret". Save this string.

Create a JSON file that lists the product IDs you would like to offer for sale. This allows you to in real time add or remove products from sale without publishing a new version of your app. The following example shows two products for sale, member_basic_w and member_basic_m.

{
    "products": [
        "member_basic_w",
        "member_basic_m"
        ]
}

Host the JSON file on your website.

Now you are ready to enable and configure the module. You will need:

  • productsUrl - The productsUrl should point to the JSON file on your website. When your GoNative app launches, it will make an HTTP GET to your productsUrl.
  • postUrl - The postUrl should be provided if you are using server-side verification (explained below). It can be omitted if you would like to handle the purchases on the device.

The app will verify the list of product IDs with iTunes to ensure they are all available for purchase. Then it will execute a javascript function on your website called gonative_info_ready, with a single object parameter:

{
    inAppPurchases: {
        platform: 'iTunes',
        canMakePurchases: true,
        products: [{
            productID: 'product_id',
            localizedDescription: 'Description from iTunes',
            localizedTitle: 'Title from iTunes',
            price: 9.99,
            priceLocale: 'en-US',
            priceFormatted: '$9.99'
        }]
    }
}

On iOS, platform will always be iTunes. canMakePurchases may be false if disabled via parental controls, or due to other reasons (see Testing process). Products is an array generated from productsUrl, filtered to show only purchasable items and with additional fields added.

You should create a page on your website that shows the available items for purchase. It should wait for the gonative_info_ready function to be called and then populate the available items for purchase and display. Show the price using the priceFormatted string, as users may have different language and currency settings.

️GoNative JavaScript Bridge

When a user decides to purchase an IAP, open the URL:

window.location.href = 'gonative://purchase/product_id';

The GoNative app will then start the in-app purchase flow.

Server-side verification

There are two methods available to fulfill your user’s purchases: server-side, and on-device. Server-side verification is generally recommended if purchases are to be associated with a user account, as it is more secure. When an in-app purchase is made, the purchase data will be sent to your web server, which will credit or fulfill the purchased item after it has verified the purchase with Apple. During this process, you should associate the purchase with the logged-in user in your system.

For example, your website may have user logins with a free membership tier, and a premium membership tier. In this case, you should only display the purchase page within the logged-in section of your website. When the purchase is made, the receipt data will be POSTed the configured postUrl with the same cookies as the logged-in user.

The JSON POST to postUrl will contain the contents:

{
    "receipt-data": "xxxxxxxxxxxxxxxxxxx"
}

Your web server needs to create a post to https://buy.itunes.apple.com/verifyReceipt with the contents:

{
    "receipt-data": "xxxxxxxxxxxxxxx",
    "password": "shared secret from iTunes connect",
    "exclude-old-transactions": true
}

If exclude-old-transactions is set to true, Apple will only return the latest transaction for auto-renewing subscriptions. Otherwise, you will get back the entire history of subscriptions.

Apple's server should return HTTP status 200 with a JSON object (see example below). If the JSON object is {"status":21007}, the receipt was generated from the sandbox/test environment. In that case, re-do the POST to the following url: https://sandbox.itunes.apple.com/verifyReceipt.

Assuming the response from Apple’s server has status 0, verify the receipt's bundle_id matches your app, and what products have been purchased. Additionally, save the receipt-data in your database so that you can verify successful auto-renews. The receipt-data serves as a “token” you can use to get updated subscription information. At this point, your server should provide whatever it is the user has purchased (premium content, virtual currency, etc.)

Your server should respond with a JSON object:

{  
    "success": true,
    "title": "Thank you for your purchase!",
    "message": "Your IAP has been credited to your account",
    "loadUrl": "https://example.com/purchase-success"
}

The GoNative app will notify iTunes that the purchase has been fulfilled and show the user your message. loadUrl is an optional field, which the app will open and show to the user if received.

If the status in the JSON is any value other than 0, or Apple’s endpoint does not return an HTTP status 200, or the request to Apple fails, do not fulfill the purchase. See https://developer.apple.com/documentation/appstorereceipts/status for other possible JSON status values. Your web server should respond with a JSON object with success set to false. We recommend logging the response from Apple for troubleshooting purchases, especially the status field.

On failure, your web server may supply a message, title, and loadUrl to provide feedback to your user. You may choose to surface the status value from Apple to your user in a message. The app will re-attempt the POST to your web server each time it launches until it gets a success. If a purchase is not ever fulfilled, Apple will eventually refund the user.

An example response from iTunes will look as follows:

{
    "status": 0,
    "environment": "Sandbox",
    "receipt": {
        "receipt_type": "ProductionSandbox",
        "adam_id": 0,
        "app_item_id": 0,
        "bundle_id": "io.gonative.ios.dev",
        "application_version": "1.0.0",
        "download_id": 0,
        "version_external_identifier": 0,
        "receipt_creation_date": "2016-12-01 22:26:23 Etc/GMT",
        "receipt_creation_date_ms": "1480631183000",
        "receipt_creation_date_pst": "2016-12-01 14:26:23 America/Los_Angeles",
        "request_date": "2016-12-02 02:31:44 Etc/GMT",
        "request_date_ms": "1480645904550",
        "request_date_pst": "2016-12-01 18:31:44 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": "member_basic_m",
            "transaction_id": "1000000255553673",
            "original_transaction_id": "1000000255553673",
            "purchase_date": "2016-12-01 22:26:22 Etc/GMT",
            "purchase_date_ms": "1480631182000",
            "purchase_date_pst": "2016-12-01 14:26:22 America/Los_Angeles",
            "original_purchase_date": "2016-12-01 22:26:23 Etc/GMT",
            "original_purchase_date_ms": "1480631183000",
            "original_purchase_date_pst": "2016-12-01 14:26:23 America/Los_Angeles",
            "expires_date": "2016-12-01 22:31:22 Etc/GMT",
            "expires_date_ms": "1480631482000",
            "expires_date_pst": "2016-12-01 14:31:22 America/Los_Angeles",
            "web_order_line_item_id": "1000000033828184",
            "is_trial_period": "false"
        }]
    },
    "latest_receipt_info": [{
            "quantity": "1",
            "product_id": "subscription1",
            "transaction_id": "1000000255546758",
            "original_transaction_id": "1000000255546758",
            "purchase_date": "2016-12-01 21:28:05 Etc/GMT",
            "purchase_date_ms": "1480627685000",
            "purchase_date_pst": "2016-12-01 13:28:05 America/Los_Angeles",
            "original_purchase_date": "2016-12-01 21:28:05 Etc/GMT",
            "original_purchase_date_ms": "1480627685000",
            "original_purchase_date_pst": "2016-12-01 13:28:05 America/Los_Angeles",
            "is_trial_period": "false"
        }
    }
}

On-device verification

An alternative to server-side verification is on-device verification. This can be used if your app does not have user login accounts to keep track of users using different devices. It is less secure than server-side verification, as someone with a jailbroken iPhone could theoretically modify your app and make it act as if a purchase has been made.

To use on-device verification only, do not provide a postUrl in your app’s config file. When your app launches and when purchases are made, the app will execute a javascript function you have defined called gonative_iap_purchases with the following data:

{
  "hasValidReceipt": true,
  "platform": "iTunes"
  "activeSubscriptions": ["member_basic_w"],
  "allPurchases": [
    {
      "purchaseDateString": "2019-08-11T15:53:13Z",
      "transactionIdentifier": "1000000556506948",
      "webOrderLineItemID": 1000000046196920,
      "originalPurchaseDateString": "2019-08-07T23:46:15Z",
      "quantity": 1,
      "productIdentifier": "member_basic_w",
      "originalTransactionIdentifier": "1000000555471857",
      "cancellationDateString": "",
      "subscriptionExpirationDateString": "2019-08-11T15:56:13Z"
    },
    {
      "purchaseDateString": "2019-08-11T16:03:44Z",
      "transactionIdentifier": "1000000556507336",
      "webOrderLineItemID": 1000000046196984,
      "originalPurchaseDateString": "2019-08-07T23:46:15Z",
      "quantity": 1,
      "productIdentifier": "member_basic_w",
      "originalTransactionIdentifier": "1000000555471857",
      "cancellationDateString": "",
      "subscriptionExpirationDateString": "2019-08-11T16:06:44Z"
    }
  ]
}

Check that hasValidReceipt=true. The allPurchases field is an array of objects containing information on what that user’s device has purchased. For convenience, we provide an activeSubscriptions array that lists the product IDs of what subscriptions are currently active.

Parse the data in your gonative_iap_purchases javascript function and provide any appropriate functionality. For example, you may choose to show ads in your app if the user has not purchased a premium add-removal nonconsumable purchase or recurring subscription.

Restoring Purchases

If you are offering subscriptions or non-consumable in-app purchases, you should add a “Restore Purchases” button somewhere in your app. This allows your users to continue to use their previous purchases if they change devices or have multiple devices.

️GoNative JavaScript Bridge

To restore purchases, open the url:

window.location.href = 'gonative://iap/restorePurchases';

Any previous purchases will be restored and will start the verification process outlined above, as if they were newly purchased. If there are no purchases to restore, no action is performed. Unfortunately, it is not possible to differentiate between a lack of purchases to restore, a delay, or a failure to restore. You may choose to display a message such as “Restore requested. If you have any previous purchases, they will be available shortly.”

Auto-renewable Subscriptions

Apple will automatically bill users who have purchased auto-renewable subscriptions. To check on the status of a user’s subscription, POST the receipt again to Apple’s endpoint and check the latest_receipt_info field. You may wish to set up a regular job to go through all active subscriptions.

Apple can also notify you of subscription status changes by posting to an endpoint you have set up to handle the change events. Go to App Store Connect -> Your App -> App Information and enter the URL. See the “Status Update Notifications” section in the In-App Purchase Programming guide: https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/Subscriptions.html

Additional References

https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html

https://help.apple.com/app-store-connect/#/dev0067a330b

Testing process

To test your in-app purchases, you will need to create iTunes sandbox users under the App Store Connect account that owns your app. The users must not already exist in the Apple system, i.e. you cannot use your regular account logins. You can enter fake data for most of the fields but may need to set passwords that are at least 10 characters with uppercase, lowercase, and numbers.

When the app launches or a purchase is initiated, you will be prompted to sign in with the test user’s account. The purchase flow can be tested without real payment. Auto-renewing subscriptions will renew at an accelerated rate (5 minutes per month) for 6 times, and will then cancel.

If you are having trouble testing iOS purchases, especially if canMakePurchases is false, please check the following items:

  • Ensure you are running the latest non-beat Xcode version
  • Do not use a sandbox login to sign in directly on your device. Only enter it when the in-app purchase is prompted for on your device
  • Make sure that you are using a sandbox login created under your account on iTunes Connect
  • Verify that the “In-app purchase” capability has been added to your app in Xcode. Open the GoNativeIOS project -> GonativeIO target -> Signing & Capabilities.
  • Verify that parental controls are not activated that may prevent purchases.
  • Reboot your device