Signing iOS Subscription Offers

Example of using RevenueCat to show a Subscription Offer to a user who has recently cancelled.

Signing iOS Subscription Offers with RevenueCat
Ryan Kotzebue

Ryan Kotzebue

PublishedLast updated

Last month, we wrote about Apple’s new iOS Subscription Offers, and today, we’re excited to announce official support for Subscription Offers! Below we’ll walk through a simple example of using RevenueCat to show a retention Subscription Offer to a user who has recently cancelled.

To learn more about what Subscription Offers are, and if they make sense for your app, check out our original post here.

1. Configure the offer in App Store Connect

Our journey into Subscription Offer land starts by stepping into App Store Connect to set up our offer. If you already have your offers set up in App Store Connect, head to Step 2.

Subscription Offers are included as a pricing option to an existing subscription product. When you click the “+” option next to Subscription Prices, you’ll see a new option to Create Promotional Offer.

To create the offer there are two fields that you need to specify: Reference Name, which is just used for your reference, and the Promotional Offer Product Code, which is what you will actually use to activate a specific offer in your app. The product code doesn’t need to be shared with users (though you could depending on your use case).

On the next screen you’ll select the type of offer you wish to provide. Just like introductory offers, there are three types of subscription offers:

  1. Pay-up-front — The customer pays once for a period of time, e.g. $0.99 for 3 months. Allowed durations are 1, 2, 3, 6 and 12 months.
  2. Pay-as-you-go — The customer pays a reduced rate, each period, for a number of periods, e.g. $0.99 per month for 3 months. Allowed durations are 1-12 months. Can only be specified in months.
  3. Free — This is analogous to a free trial, the user receives 1 of a specified period free. Just to keep things interesting the allowed durations are 3 days, 1 week, 2 weeks, 1 month, 2 months, 3 months, 6 months, and 1 year.

For this example, we’re going to choose a Pay up front offer, and try to win back lapsed premium users with 3-months access for only $0.99 (what a deal!). Hopefully, after the 3-month offer users will stay subscribed.

Don’t forget to click Save in the upper right after you configure the offer!

Subscription Keys

After you’ve created an offer, you need to make sure you have a subscription key to securely authenticate and validate a subscription request with Apple.

Subscription keys are generated and the Users and Access section of App Store Connect, and you can use the same subscription key for all of your offers.

Click Generate Subscription Key, you’ll be prompted to enter a name for the key.

Once your key is generated, it will appear in Active Keys and you get one shot to download it.

Click Download API Key and store the file in a safe place, we’ll need it later!

Now that everything is set up in App Store Connect, it’s time to start building our offer.

2. Add the Subscription Key to RevenueCat

The last thing we need to do before we jump into some code is upload the Subscription Key that we generated in the previous step to RevenueCat. RevenueCat will handle all the server-side authentication and validation of the Subscription Offers.

RevenueCat: Hard at work authenticating your Subscription Offers

Navigate to your app Settings in the RevenueCat dashboard.

If you don’t have an account yet, you can sign up for free and follow the instructions to add a new app. We’ll wait…

In your app Settings you’ll see an area to upload your Subscription Key .p8 file that you downloaded from App Store Connect. That’s all there is to it!

Upload your Subscription Key file to RevenueCat and you’re good to go

3. Show Subscription Offer to eligible users

Only users who had an active subscription in your app at some point (including current subscribers) are eligible for Subscription Offers. Just like trial eligibility, Apple doesn’t directly tell you if a user is eligible, so you need to check yourself. However, Apple will enforce this from a payment perspective, they will just be shown the regular product, regardless of the offer you try to present. This could create an awkward customer experience if you tell a user to redeem and offer and then presented the normal price.

In this example, I only want to show my Subscription Offer to users who had their paid subscription expire over 7-days ago — hopefully they’ll be willing to subscribe again if I give them a deal.

Feel free to get creative with how you choose to offer these promotions!

I won’t be recreating an entire app from scratch, let’s start with the SwiftExample that’s included in the Purchases iOS repo. This app lets you subscribe to see happy cat emojis, non-premium members can only see sad cat emojis. You can get the starter code for this app here.

We’ll use our SwiftExample app as the base for adding Subscription Offers.

The CatsViewController.swift is the main view controller for the cat content in our app. This is where we’ll determine if we should present the user with a Subscription Offer.

Lets write a function to check if we should present this offer to the user:

1func shouldShowSubscriptionOffer(_ purchaserInfo: PurchaserInfo) -> Bool {
2
3    // Check that the user has a 'pro_cat' entitlement that expired >= 7 days ago
4    // and check that we haven't previously shown this offer to the user
5    if (purchaserInfo.expirationDate(forEntitlement: "pro_cat") ?? Date()).addingTimeInterval(7*86400) <= Date() &&
6            !UserDefaults.standard.bool(forKey: "has_seen_promo_cat_discount")
7    {
8        return true
9    }
10
11    return false
12}

Our requirements are fairly simple, we want to show the offer to any user that is still using the app at least 7-days after their ‘premium’ subscription has expired. We’re also checking a flag in UserDefaults to see if they’ve already been presented this offer. Not everyone will want to redeem the offer, so we’ll consider them ineligible if we’ve ever shown it before.

Next, let’s add this check to the end of our existing function that configures the view.

1func configureCatContentFor(purchaserInfo: PurchaserInfo?) {    
2    if let purchaserInfo = purchaserInfo {
3        if purchaserInfo.activeEntitlements.contains("pro_cat") {
4
5            // ...
6            // ...
7            // ...
8        } else {
9
10            // ...
11            // ...
12            // Check if we should show a Subscription Offer
13            // and display the promo upsell after 2 seconds
14            if #available(iOS 12.2, *) {
15                if shouldShowSubscriptionOffer(purchaserInfo) {
16                    DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
17                        self.showPromoUpsell()
18                    }
19                }
20            }
21
22        }
23    }
24}

If it’s determined we should show a user the Subscription Offer, we’ll add a 2-second delay before presenting them with the offer to smooth out the UX a bit. Also, since we want to target iOS versions below 12.2, we wrap everything in an if #available(iOS 12.2, *). The details of the showOfferUpsell() function are covered in the next step!

4. Sign and Redeem Subscription Offer

At this point, we’ve set up a Subscription Offer and determined that we want to present the current user with the option to buy the offer.

We’ll add a function, showOfferUpsell(), that asks the user if they’d like to redeem the offer — and if so, we continue with the purchase. The first step is to retrieve the offers and validate them with the Subscription Key we downloaded earlier. If the offer is valid, we present the user with the option to purchase it. Finally, if the user agrees, we make a purchase of the product with the Subscription Offer applied.

There’s a lot going on here, so we’ll first break down this function piece-by-piece, then show the entire function code after.

1@available(iOS 12.2, *)
2func showPromoUpsell() {
3
4        Purchases.shared.entitlements { (entitlements, error) in
5
6            // 1: - Get the discount for the product we want to offer
7            //
8            guard let product = entitlements?["pro_cat"]?.offerings["monthly_cats"]?.activeProduct else {
9                print("Error finding monthly_cat product")
10                return
11            }
12            guard let productDiscount = product.discounts.first(where: { $0.identifier == "promo_cat" }) else {
13                print("Error finding promo_cat discount")
14                return
15            }
16
17            // ... continued

1. First we need to get the SKProduct (product) and the SKProductDiscount (productDiscount) for the Subscription Offer we want to show. We use a couple of guard statements here to make sure we’ve entered the right key names after fetching the entitlements.

1  // 2: - Fetch the payment discount
2  //
3  Purchases.shared.paymentDiscount(for: productDiscount, product: product, completion: { (paymentDiscount, err) in
4
5      guard let discount = paymentDiscount else {
6          print("Payment discount doesn't exist. Check error.")
7          return
8      }
9
10      // ... continued

2. With the SKProduct and SKProductDiscount in hand, the next step is to fetch the SKPaymentDiscount with the Purchases SDK’s new .paymentDiscount method. This makes a call to the RevenueCat backend and signs the offer using the Subscription Key uploaded in step 2 to verify the request.

1  // 3: - Show the user the offer to purchase and save a flag
2  //      in local storage so we don't show again
3  self.presentPromoAlert(productDiscount, shouldPurchase: {
4
5      Purchases.shared.makePurchase(product, discount: discount, { (transaction, info, error, cancelled) in
6          self.configureCatContentFor(purchaserInfo: info)
7      })
8  })
9
10  // ... continued

3. Once the SKPaymentDiscount is retrieved , we have everything we need to let the user redeem the offer. We use a helper method to present a simple UIAlertController to show the user that there is an offer available, and ask if they’d like to redeem it.

When the user agrees to the offer, we use the .makePurchase method to pass in the SKProduct and SKPaymentDiscount from earlier. This will trigger Apple to display the default purchase prompt, asking the user to confirm their purchase. Once the purchase is complete, we can reconfigure the view to show the premium content (happy cat emojis)!

Wow that was a lot! Let’s check out the entire function below to help understand how everything works together:

1@available(iOS 12.2, *)
2func showPromoUpsell() {
3
4      Purchases.shared.entitlements { (entitlements, error) in
5
6          // 1: - Get the discount for the product we want to offer
7          //
8          guard let product = entitlements?["pro_cat"]?.offerings["monthly_cats"]?.activeProduct else {
9              print("Error finding monthly_cat product")
10              return
11          }
12          guard let productDiscount = product.discounts.first(where: { $0.identifier == "promo_cat" }) else {
13              print("Error finding promo_cat discount")
14              return
15          }
16
17          // 2: - Fetch the payment discount
18          //
19          Purchases.shared.paymentDiscount(for: productDiscount, product: product, completion: { (paymentDiscount, err) in
20
21              guard let discount = paymentDiscount else {
22                  print("Payment discount doesn't exist. Check error.")
23                  return
24              }
25
26
27              // 3: - Show the user the offer to purchase and save a flag
28              //      in local storage so we don't show again
29              self.presentPromoAlert(productDiscount, shouldPurchase: {
30
31                  Purchases.shared.makePurchase(product, discount: discount, { (transaction, info, error, cancelled) in
32                      self.configureCatContentFor(purchaserInfo: info)
33                  })
34              })
35          })
36      }
37
38  }
39
40  // Helper function to present promo alert view
41  func presentPromoAlert(_ discount: SKProductDiscount, shouldPurchase: @escaping (() -> Void)) {
42      let price = discount.price
43
44      let promoAlert = UIAlertController(
45          title: "Thanks for being a loyal user!",
46          message: "We'd like to offer you an exclusive deal of only \(price) for 3-months of pro access.",
47          preferredStyle: .alert)
48
49      promoAlert.addAction(UIAlertAction(
50          title: "No thanks",
51          style: .cancel,
52          handler: nil))
53
54      promoAlert.addAction(UIAlertAction(
55          title: "Sure!",
56          style: .default,
57          handler: { _ in
58              shouldPurchase()
59      }))
60      UserDefaults.standard.set(true, forKey: "has_seen_promo_cat_discount")
61      self.present(promoAlert, animated: true, completion: nil)
62  }

When we put it all together, we should see a new purchase prompt from Apple which outlines the promo conditions.

Whew! Looks like our Subscription Offer is working and we’re ready to win back churned users!

Let us know what you build!

Now that you have the tools to build and track Subscription Offers in your app, we wanna know what you use them for! This was just a simple example, and know you’ll be able to do a lot more than what we walked through here.

In the future, we’ll be adding features for deploying win-backs, referrals and other incentives, but want to hear from you. If you have ideas or requests for this feature, comment about it on our Community or email. As always, we are available to help you in any way we can, so don’t be shy about reaching out.

You can download the complete example app here.

For more updates, tip, and tricks follow us on Twitter!

In-App Subscriptions Made Easy

See why thousands of the world's tops apps use RevenueCat to power in-app purchases, analyze subscription data, and grow revenue on iOS, Android, and the web.

Related posts

How we solved RevenueCat’s biggest challenges on data ingestion into Snowflake
How we solved RevenueCat’s biggest challenges on data ingestion into Snowflake
Engineering

How we solved RevenueCat’s biggest challenges on data ingestion into Snowflake

Challenges, solutions, and insights from optimizing our data ingestion pipeline.

Jesús Sánchez

Jesús Sánchez

April 15, 2024

How RevenueCat handles errors in Google Play’s Billing Library
How RevenueCat handles errors in Google Play’s Billing Library  
Engineering

How RevenueCat handles errors in Google Play’s Billing Library  

Lessons on Billing Library error handling from RevenueCat's engineering team

Cesar de la Vega

Cesar de la Vega

April 5, 2024

Use cases for RevenueCat Billing
Engineering

Use cases for RevenueCat Billing

3 ways you can use the new RevenueCat Billing beta today.

Charlie Chapman

Charlie Chapman

March 21, 2024

Want to see how RevenueCat can help?

RevenueCat enables us to have one single source of truth for subscriptions and revenue data.

Olivier Lemarié, PhotoroomOlivier Lemarié, Photoroom
Read Case Study