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 several 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: last for a fixed amount of time and then expire, e.g. 1-year premium access
  • Renewable subscriptions: automatically renew, e.g. monthly premium access

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

👍

Developer Demo

Display our demo page in your app to test during development https://median.dev/iap/

Median’s Apple App Store In-App purchase flow has these steps:

  1. Create In-App Purchase items in App Store Connect
  2. Host a JSON file on your website with a list of purchasable items
  3. Display a web UI listing purchasable items
  4. Initiate a purchase when requested by a user
  5. Verify and fulfil purchases through the Apple App Store
  6. Restore previous purchases through the Apple App Store
  7. Manage subscription renewals and cancellations with the Apple App Store

If using server-side verification, your app posts a receipt to your site. Your web server verifies the receipt with Apple and fulfills the purchase.
If using on-device verification, the purchase is automatically fulfilled and purchase item data is available via JavaScript.

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

Configure IAP in App Store Connect

Create your app on App Store Connect, picking a globally unique bundle ID.

Open your new app on App Store Connect, go to App Store -> 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 semi-annual 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.

Host a JSON file with available products

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 for sale without publishing a new version of your app. Apple requires product IDs be alphanumeric and dots, dashes and underscores are allowed. We recommend prefixing with your Bundle ID to aid in reporting. The following example shows two products for sale, com.appname.subscription_monthly and com.appname.subscription_yearly.

{
    "products": [
        "com.appname.subscription_monthly",
        "com.appname.subscription_annual"
        ]
}

You must then host the JSON file on your website to be accessible by your app. We are using https://median.dev/iap/productsApple.json

App Configuration

Now you are ready to enable and configure the plugin within the Native Plugins tab of the App Studio. You will need to enter:

  • productsUrl - The productsUrl should point to the JSON file on your website. When your Median app launches, it will make an HTTP GET to your productsUrl.
  • postUrl - The postUrl should be provided if you are using server-side verification. It can be omitted if you would like to use on-device verification and fulfil the purchases within your app.

IAP Products and Status

Upon launch your app will verify the list of product IDs with the App Store to ensure they are all available for purchase. Then it will execute a JavaScript function on your website defined as median_info_ready with a single object parameter. Alternatively you can use the Median JavaScript Bridge to obtain this data at runtime by calling the method median.iap.info().

↔️Median JavaScript Bridge

You define this function on your page, but do not actually call it.
If present on a page, it will be called by the app when the page is loaded.

function median_info_ready(productsData) {
   console.log(productsData);
}

Or you may return the available products at runtime via a promise (in async function)

var productsData = await median.iap.info();
console.log(productsData);

In both cases productsData will be of the form:

{
   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 - refer to testing process. products is an array generated from productsUrl, filtered to show only purchasable items and with additional fields added.

Display web UI for purchases

You should create a page on your website that shows the available items for purchase. It should wait for the median_info_ready function to be called or the response from median.iap.info(), and then populate the available items for purchase and display. Display the price using the priceFormatted string, as users may have different language and currency settings.

Initiate purchase

↔️Median JavaScript Bridge

When a user decides to purchase an IAP, run the JavaScript function:

median.iap.purchase({'productID': 'product_id'});

Your app will then start the in-app purchase flow.

Purchase verification and fulfillment

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 sent via a POST request to your 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 then 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 receive 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 which 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"
}

Your app will next notify the App Store 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 in the response.

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 never fulfilled, Apple will eventually refund the user.

An example response from the App Store is as follows:

{
    "status": 0,
    "environment": "Sandbox",
    "receipt": {
        "receipt_type": "ProductionSandbox",
        "adam_id": 0,
        "app_item_id": 0,
        "bundle_id": "io.median.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 as median_iap_purchases with the following data:

↔️Median JavaScript Bridge

You define this function on your page, but do not actually call it.
If present on a page it will be called by the app when the page is loaded,

function median_iap_purchases(purchasesData) {
   console.log(purchasesData);
}

Or you may return the purchases at runtime via a promise (in async function)

var purchasesData = await median.iap.purchases();
console.log(purchasesData);

In both cases purchasesData will be of the form:

{  
  "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"  
    }  
  ]  
}

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 the subscriptions are currently active.

Parse the data in your median_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 non-consumable purchase or a recurring subscription.

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

↔️Median JavaScript Bridge

To restore purchases, run the JavaScript function:

median.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 previous purchases they will be available shortly.”

Ongoing subscription management

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/documentation/appstoreservernotifications/enabling_app_store_server_notifications

Testing process

To test your in-app purchases, you will need to create App Store 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 test 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 experiencing issues testing iOS purchases, especially if canMakePurchases is false, please review the following:

  • Ensure you are building with the latest release (non-beta) Xcode version
  • Verify that the “In-app purchase” capability has been added to your app in Xcode under Signing & Capabilities.
  • Do not use a sandbox login to sign in directly to iCloud on your device. Only use the sandbox login when the in-app purchase is prompted on your device
  • Make sure that you are using a sandbox login created under your account on App Store Connect
  • Verify that parental controls are not activated that may prevent purchases.
  • Reboot your device

🚧

In-App Purchase Testing

Each of the above are potential causes of issues when testing In-App Purchases. Check each point carefully and make use of our demo page at https://median.dev/iap

Apple Documentation References

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

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