Displaying Paywalls & Products

Learn how to fetch and display products from paywalls in your app using Adapty SDK. With Adapty you can dynamically change the products visible to your users without new app releases

Adapty allows you to remotely configure the products that will be displayed in your app. This way, you don't have to hardcode your products, and you can dynamically change offers or run A/B tests without needing to release a new version of your app.

🚧

Make sure you add the products and paywalls to your Adapty Dashboard before fetching them.

In order to display the products, you need to query a paywall that contains them:

// Use Locale.preferredLanguages to find out which languages the user prefers using
let locale = Locale.current.identifier 
Adapty.getPaywall("YOUR_PAYWALL_ID", locale: locale) { result in
    switch result {
        case let .success(paywall):
            // the requested paywall
        case let .failure(error):
            // handle the error
    }
}
Adapty.getPaywall("YOUR_PAYWALL_ID", locale = "en") { result ->
    when (result) {
        is AdaptyResult.Success -> {
            val paywall = result.value
            // the requested paywall
        }
        is AdaptyResult.Error -> {
            val error = result.error
            // handle the error
        }
    }
}
Adapty.getPaywall("YOUR_PAYWALL_ID", "en", result -> {
    if (result instanceof AdaptyResult.Success) {
        AdaptyPaywall paywall = ((AdaptyResult.Success<AdaptyPaywall>) result).getValue();
        // the requested paywall
      
    } else if (result instanceof AdaptyResult.Error) {
        AdaptyError error = ((AdaptyResult.Error) result).getError();
        // handle the error
      
    }
});
try {
    const id = 'YOUR_PAYWALL_ID';
    const locale = 'en';

    const paywall = await adapty.getPaywall(id, locale);
  // the requested paywall
} catch (error) {
    // handle the error
}

Request parameters:

  • id (required): The identifier of the desired paywall. This is the value you specified when creating a paywall in your Adapty Dashboard.
  • locale (optional): The identifier of the paywall localization. This parameter is expected to be a language code composed of one or more subtags separated by the "-" character. The first subtag is for the language, the second one is for the region (The support for regions will be added later).
    Example: en means English, en-US represents US English.
    If this parameter is omitted, the paywall will be returned in the default locale.

Response:

  • Paywall: an AdaptyPaywall object. This model contains the list of the products ids, paywall's identifier, remote config, and several other properties.

Once you have the paywall, you can query the product array that corresponds to it:

Adapty.getPaywallProducts(paywall: paywall) { result in    
    switch result {
    case let .success(products):
        // the requested products array
    case let .failure(error):
        // handle the error
    }
}
Adapty.getPaywallProducts(paywall) { result ->
    when (result) {
        is AdaptyResult.Success -> {
            val products = result.value
            // the requested products
        }
        is AdaptyResult.Error -> {
            val error = result.error
            // handle the error
        }
    }
}
Adapty.getPaywallProducts(paywall, result -> {
    if (result instanceof AdaptyResult.Success) {
        List<AdaptyPaywallProduct> products = ((AdaptyResult.Success<List<AdaptyPaywallProduct>>) result).getValue();
        // the requested products
      
    } else if (result instanceof AdaptyResult.Error) {
        AdaptyError error = ((AdaptyResult.Error) result).getError();
        // handle the error
      
    }
});
try {
    // ...paywall
    const products = await adapty.getPaywallProducts(paywall);
  // the requested products list
} catch (error) {
    // handle the error
}

📘

Every time data is fetched from a remote server, it will be stored in the local cache. This way, you can display the products even when the user is offline.

Next, build your paywall view using the fetched products and show it to the user. When the user makes a purchase, simply call .makePurchase() with the product from your paywall.

let product = products.first

Adapty.makePurchase(product: product) { result in
    switch result {
    case let .success(profile):
        // successful purchase
    case let .failure(error):
        // handle the error
    }
}
Adapty.makePurchase(activity, product) { result ->
    when (result) {
        is AdaptyResult.Success -> {
            val profile = result.value              
            // successful purchase
        }
        is AdaptyResult.Error -> {
            val error = result.error
            // handle the error
        }
    }
}
Adapty.makePurchase(activity, product, result -> {
    if (result instanceof AdaptyResult.Success) {
        AdaptyProfile profile = ((AdaptyResult.Success<AdaptyProfile>) result).getValue();
        // successful purchase
    } else if (result instanceof AdaptyResult.Error) {
        AdaptyError error = ((AdaptyResult.Error) result).getError();
        // handle the error
    }
});
try {
    // ...product
    const result = await adapty.makePurchase(product);
  // successful purchase
} catch (error) {
    // handle the error
}

You can find more information on how to make purchases in the corresponding article.

🚧

Since paywalls are configured remotely, the available products, the number of products, and special offers (such as free trials) can change over time. Make sure your code handles these scenarios.
For example, if you initially retrieve 2 products, your app should display those 2 products. However, if you later retrieve 3 products, your app should display all 3 without requiring any code changes.

Don't hardcode product IDs — you won't need them.

Products fetch policy and intro offer eligibility (not applicable for Android)

A quite important aspect that we want to clarify is how to properly handle the availability of the intro offer. The point is that StoreKit version 1 does not provide a simple and reliable way to determine this value. The only option is to calculate this value based on a receipt that must be on your user's device. However, in rare situations (and in Sandbox mode - always) the check is not present on the device at the first startup.

In order not to mislead the user by displaying an incorrect intro offer value, we have introduced a special value .unknown for such situations. Next you can see all the possible values of introductoryOfferEligibility

ValueDescriptions
unknownWe are not sure about the eligibility at this moment
ineligibleUser is not eligible to get any into offer, you should't present it in your UI
eligbleUser is eligible for intro offer, it is safe to reflect this info in you UI

🚧

We recommend working with unknown in the same way as ineligible

If for some reason you observe .unknown values in the introductoryOfferEligibility fields when loading products for the first time, you should restart the .getProductsForPaywall method in special mode:

Adapty.getPaywallProducts(paywall: paywall, fetchPolicy: .waitForReceiptValidation) { result in
    if let products = try? result.get() {
        // update your UI
    }
}

In this case, the function will wait for the receipt to be refreshed on the device, and only after validation will return the correctly configured products. If at this point the user has not yet started the purchase process, update your interface according to the latest values.

🚧

We urge you to be very careful with this scenario, as Apple's reviewers can check it quite rigorously. However, based on our experience with them, we conclude that the behavior of the payment environment in which they perform their checks may be somewhat different from our usual sandbox and production.

Paywall analytics

Adapty helps you to measure the performance of your paywalls. We automatically collect all the metrics related to purchases except for paywall views. This is because only you know when the paywall was shown to a customer.
Whenever you show a paywall to your user, call .logShowPaywall(paywall) to log the event, and it will be accumulated in your paywall metrics.

Adapty.logShowPaywall(paywall)
Adapty.logShowPaywall(paywall)
Adapty.logShowPaywall(paywall);
await adapty.logShowPaywall(paywall);

Request parameters:

Fallback paywalls

Adapty allows you to provide fallback paywalls that will be used when a user opens the app and there's no connection with Adapty backend (e.g. no internet connection or in the rare case when backend is down) and there's no cache on the device.

Keep in mind that if your application is offline during the first run, the .getPaywall function will necessarily wait for the user's profile to be created and only after that it will be able to make a paywall request.

To set fallback paywalls, use .setFallbackPaywalls method. You should pass exactly the same payload you're getting from Adapty backend. You can copy it from Adapty Dashboard.
Here's an example of getting fallback paywall data from the locally stored JSON file named fallback_paywalls.

if let urlPath = Bundle.main.url(forResource: "fallback_paywalls", withExtension: "json"),
   let paywallsData = try? Data(contentsOf: urlPath) {
     Adapty.setFallbackPaywalls(paywallsData)
}
val paywalls: String = //get paywalls JSON from where you stored it

Adapty.setFallbackPaywalls(paywalls)
String paywalls = //get paywalls JSON from where you stored it

Adapty.setFallbackPaywalls(paywalls);
const fallbackPaywalls = require('./fallback_paywalls.json');
// React Native automatically parses JSON, but we do not need that
const fallbackString = JSON.stringify(fallbackPaywalls);

await adapty.setFallbackPaywalls(fallbackString);

Request parameters:

  • Paywalls (required): a JSON representation of your paywalls/products list in the exact same format as provided by Adapty backend.

📘

You can also hardcode fallback paywall data or receive it from your remote server.

Paywall remote config

There is a remote config available with Adapty which can be built right through the dashboard and then used inside your app. To get such config, just access remoteConfig property and extract needed values.

Adapty.getPaywall("YOUR_PAYWALL_ID") { result in
    let paywall = try? result.get()
    let headerText = paywall?.remoteConfig?["header_text"] as? String
}
Adapty.getPaywall("YOUR_PAYWALL_ID") { result ->
    when (result) {
        is AdaptyResult.Success -> {
            val paywall = result.value
            val headerText = paywall.remoteConfig?.get("header_text") as? String
        }
        is AdaptyResult.Error -> {
            val error = result.error
            // handle the error
        }
    }
}
Adapty.getPaywall("YOUR_PAYWALL_ID", result -> {
    if (result instanceof AdaptyResult.Success) {
        AdaptyPaywall paywall = ((AdaptyResult.Success<AdaptyPaywall>) result).getValue();
        
        ImmutableMap<String, Object> remoteConfig = paywall.getRemoteConfig();
        
        if (remoteConfig != null) {
            if (paywall.getRemoteConfig().get("header_text") instanceof String) {
                            String headerText = (String) paywall.getRemoteConfig().get("header_text");
                        }
        }
    } else if (result instanceof AdaptyResult.Error) {
        AdaptyError error = ((AdaptyResult.Error) result).getError();
        // handle the error
    }
});