Strong Customer Authentication (SCA) with RevenueCat

SCA requirements with in-app purchases

Strong Customer Authentication (SCA) with RevenueCat
Ryan Kotzebue
Ryan KotzebueJanuary 4, 2021

Earlier this week, Apple sent out an email informing developers about new requirements for Strong Customer Authentication (SCA) for in-app purchases.

These requirements are designed to protect against fraud in online transactions. Certain in-app purchases in the European Economic Area will need to be authenticated by the bank or payment service provider before they can be completed:

  • For auto-renewable subscriptions, SCA is required only for the first transaction.
  • Purchases under €30 may not require SCA.
  • Purchases made with Apple Pay, which already meets SCA requirements, will not require additional authentication.
  • Purchases made with mobile phone billing, other payment services, or an Apple ID balance (from a gift card or Apple Wallet) will not require additional authentication.

You can read all of the details in the Apple press release here:

Supporting SCA in your app

Apple wasn’t clear in their announcement about exactly how these SCA transactions will be handled.

Handling this interrupted transaction is similar to Ask to Buy purchases that need approval from a family approver or when users need to agree to updated App Store terms and conditions before completing a purchase…. which can update your payment queue with a state of “failed” or “deferred” as the user exits the app.

Google has not released any information yet on how developers should handle SCA in the Play Store. We hope that it will be automatically handled in the native in-app purchase flow — that would be the most seamless solution for both developers and users. However, if this is not the case, then they will probably behave the same way as Google pending transactions (these are also already supported in RevenueCat).

The customer experience with SCA

Unfortunately, we can’t know exactly what the customer experience with SCA will look like until it’s live, as there’s no way to test the behavior now in sandbox. However, Apple did provide some information on the general flow:

  1. Customers will be taken outside of the purchase flow in your app — to the bank or payment service provider’s website or app — for authentication.
  2. After confirming the purchase outside of your app, customers will be redirected back to your app, where they’ll see a message from Apple letting them know that their purchase was completed.

The difficulty for developers is that, since the customer is taken outside of your app to complete the transaction, you’re left with a few unknowns. Will the user ever come back? What if they return without completing the transaction? What if the transaction completes tomorrow or next week? 

If you’re like most developers, then you probably have some similar code as the image below to make purchases. With SCA, we’ll be receiving an error message from our purchase attempt and showing an alert to customers as they’re being redirected away from the app to complete the transaction.

If the transaction gets completed successfully outside of the app, it’s too late to trigger our purchaseCompletedHandler() since we already showed an error to the customer.

The transaction is still being recorded in the background by RevenueCat, but it can lead to a clunky app experience where customers may have to manually close out of your paywall and possibly refresh the page to pick up the latest subscription status from RevenueCat.

So while the app “works”, to make sure customers have the smoothest experience with SCA you should implement the didReceiveUpdatedPurchaserInfo delegate and respond to new transactions from the background.

(Optional) Improve your customer experience with the PurchaserInfo listener

With a few lines of code (and RevenueCat), we can create a smoother experience for customers that go through the SCA flow. This also applies to any type of deferred or pending purchase, like Ask To Buy for kids. We’re using Swift for this example, but the same concepts apply to iOS and Android.

Let’s walk through the following:

  1. Broadcasting a notification to your app when a customer’s subscription status changes.
  2. Handling the notification from step 1 to update our app when the subscription status changes in the background.

The first thing we’ll do is set up a notification (kPurchaserInfoUpdated) when the didReceiveUpdatedPurchaserInfo delegate is fired. On iOS, this is in our AppDelegate file.

1import Purchases
4class AppDelegate: UIResponder, UIApplicationDelegate {
6    var window: UIWindow?
8    func application(_ application: UIApplication,
9                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
11        Purchases.configure(withAPIKey: "<your_api_key>")
12        Purchases.shared.delegate = self // set up the delegate
13        return true
14    }
17extension AppDelegate : PurchasesDelegate {
18    func purchases(_ purchases: Purchases, didReceiveUpdated purchaserInfo: Purchases.PurchaserInfo) {
20        // Broadcast a notification when the PurchaserInfo changes
21 NSNotification.Name("kPurchaserInfoUpdated"), object: nil)
22    }

Here we’re setting the delegate after configuring Purchases, and posting a notification in the didReceiveUpdatedPurchaserInfo function.

Next, we’ll respond to this listener in two spots: #1 on our paywall, so we can dismiss the paywall if the purchase completes. And #2, on our main view controller, in case the customer dismisses the paywall on their own before the listener fires, the main view will refresh to show the “premium” state.

On both the paywall and main view controller the setup is very similar. We’ll add a notification observer that triggers some function to update the view based on the latest PurchaserInfo status.

1import UIKit
2import Purchases
4class CatsViewController: UIViewController {
7    override func viewDidLoad() {
8        super.viewDidLoad()
10        // 1. Add an observer for our kPurchaserInfoUpdated notification that triggers a function to update the view
11        NotificationCenter.default.addObserver(self,
12                                               selector: #selector(updateViewForPurchaserInfo),
13                                               name: NSNotification.Name("kPurchaserInfoUpdated"), object: nil)
15        updateViewForPurchaserInfo()
17    }
19    // 2. Create a function that will update the state of your view based on the PurchaserInfo status
20    @objc func updateViewForPurchaserInfo() {
21        Purchases.shared.purchaserInfo { (purchaserInfo, error) in
22            self.configureCatContentFor(purchaserInfo: purchaserInfo)
23        }
24    }
26    func configureCatContentFor(purchaserInfo: Purchases.PurchaserInfo?) {
28        // set the content based on the user subscription status
29    }
31    // 3. Remove the observer when the view is deallocated
32    deinit {
33        NotificationCenter.default.removeObserver(self)
34    }

To breakdown the above code:

  • We add an observer to the notification we set up earlier in the didReceiveUpdatedPurchaserInfo delegate.
  • The observer triggers some function that updates the state of our view based on the PurchaserInfo status.
  • We remove the observer when the view is deallocated.

If we wanted to take it a step further, we could handle some specific errors around pending payments differently, or maybe even show a dismissible banner indicating a pending payment was initialized. But the extra development and testing may not be worth it, as the small changes we’ve already made are a huge improvement.

Depending on your app, all or only some of this may be required, or you may need similar code in multiple view controllers.

What’s the impact of SCA?

With so many unknowns about when and how SCA will trigger an authentication flow, it’s difficult to predict the impact of these new requirements — but it seems likely they will decrease conversion rates somewhat. How much, and whether the impact will be significant, probably won’t be clear for many months. However, if you build a great product, and people really want to buy it, they’ll figure it out! Just make sure you follow the above steps to remove as much friction as possible.

Subscribe to our monthly newsletter

Read more

Envoy buffering investigation

400 Errors When Migrating from NGINX to Envoy

A look into the work our core infrastructure team does

Mario de Frutos

Mario de Frutos

August 17, 2022

does Apple keep its commission

Does Apple Keep its Commission After You Refund a Purchase?

We did a little sleuthing and here's our answer

David Barnard

David Barnard

August 16, 2022

Renewal rates of monthly vs annual subscriptions

How Subscription Duration Impacts Renewal Rates

We analyzed 10,000+ apps to find out

Traci Hirokawa

Traci Hirokawa

August 16, 2022

Ready to get started?

We saved 6,000+ engineering hours per year by switching to RevenueCat

Kemal Ugur, Pixery LabsKemal Ugur, Pixery Labs
Read case study