paywallObserver - using the Paywall Action Interceptor

This sections provides a detailed overview of how to use the Paywall Action Interceptor to process transactions in paywallObserver Mode

What is the Paywall Action Interceptor?

The Paywall Action Interceptor allows to intercept and override every interaction the users have with a Purchasely Screen.

This can be used to:

  • Intercept purchase and restore actions to perform them using your own code or another third-party SDK
  • Intercept the login button tapped to display your login form
  • Force the explicit acceptance of terms and conditions before a purchase
  • Intercept the call to a webview to inject credentials and be directly logged in
  • Block purchases in Kids category apps to add a parental permission gate
  • Block direct access to external content (webview or link to Safari) in Kids category apps to add a parental permission gate

With the action interceptor, you get everything you need to:

  • Get the action (purchase, login, ...) and context (Plan purchased for instance)
  • Display views, errors, messages, … above the Purchasely Screens
  • Choose if Purchasely should continue the action or not

Example of use of the Paywall Action Interceptor: when a user clicks on the Purchase button, the SDK hands over to the app that displays a modal to make the user accept the T&C. The same principle is used to make the app process the transaction with an already-in-place transaction infrastructure

Example of use of the Paywall Action Interceptor: when a user clicks on the Purchase button, the SDK hands over to the app that displays a modal to make the user accept the T&C. The same principle is used to make the app process the transaction with an already-in-place transaction infrastructure

What Paywall Actions can be intercepted?

You can intercept the following buttons being tapped:

  • Close
  • Login
  • Navigate (web or deeplink)
  • Purchase
  • Win-back / retention offer
  • Restore
  • Open another paywall
  • Promo code

Implementing the Paywall Action Interceptor

The interceptor passes 4 parameters:

  • action: the PLYPresentationAction enum that gives the type of action
  • parameters: a dictionary that contains the objects needed to perform the action (like a PLYPlan for a purchase)
  • info: the PLYPresentationInfo object containing the controller of the paywall to dismiss it or display content / error messages above it, and the presentation id and content id associated to this paywall
  • proceed: a completion handler parameter with a boolean telling Purchasely if it should continue the action itself. In other words, returning true on a purchase action will lead the Purchasely SDK to trigger the native in-app purchase flow

⚠️

When should you call proceed(true) after handling the action?

On a login action, call proceed(true) to refresh the paywall if the user has logged in

On a purchase action, if you've successfully handled the transaction, you should not call proceed(true) to avoid a second trigger of the native in-app purchase flow by the SDK

If you don't handle every action, you HAVE TO call proceed(true) otherwise the bouton will keep spinning and nothing will happen.

Processing transactions with your in-house system

Here is a code sample using the Paywall Action Interceptor to process transactions with your own in-house purchase system, for the actions purchase and restore:

Purchasely.setPaywallActionsInterceptor { [weak self] (action, parameters, presentationInfos, proceed) in
    switch action {
    // Intercept the tap on purchase to display the terms and condition
    case .purchase:        
        // Grab the plan to purchase
        guard let plan = parameters?.plan, let appleProductId = plan.appleProductId else {
            return
        }

        let success = MyPurchaseSystem.purchase(appleProductId)
        if success {
            Purchasely.synchronize() // synchronize new purchase with Purchasely
        }
        proceed(false) // notify Purchasely paywall to stop processing action
    case .restore:
        MyPurchaseSystem.restorePurchases()
        Purchasely.synchronize() // synchronize all purchases with Purchasely
        proceed(false) // notify Purchasely paywall to stop processing action
    default:
        proceed(true) // notify Purchasely paywall to continue other actions
    }
}
Purchasely.setPaywallActionsInterceptor { info, action, parameters, processAction ->
    when(action) {
        PLYPresentationAction.PURCHASE -> {
            val subscriptionId = parameters.subscriptionOffer?.subscriptionId
            val basePlanId = parameters.subscriptionOffer?.basePlanId
            val offerId = parameters.subscriptionOffer?.offerId
            val offerToken = parameters.subscriptionOffer?.offerToken
            
            // you just need to pass the offerToken to BillingClient
            val success = MyPurchaseSystem.purchase(offerToken)
          
            if(success) {
              Purchasely.synchronize() // synchronize new purchase
            }
            
            processAction(false) // notify Purchasely paywall to stop processing action
        }
        PLYPresentationAction.RESTORE -> {
            MyPurchaseSystem.restoreAllPurchases()
            Purchasely.synchronize() // synchronize all purchases with Purchasely
            processAction(false) // notify Purchasely paywall to stop processing action
        }
        else -> processAction(true) // notify Purchasely paywall to continue other actions
    }
}
Purchasely.setPaywallActionInterceptorCallback((result) => {
    if (result.action === PLYPaywallAction.PURCHASE) {
      try {
        //the store product id (sku) the user clicked on in the paywall
        String storeProductId = result.parameters.plan.productId
        
        if (Platform.OS === 'android') {
          // Only for Android you can retrieve other information about the purchase
          const basePlanId = result.parameters.subscriptionOffer?.basePlanId;
          const offerId = result.parameters.subscriptionOffer?.offerId;
          const offerToken = result.parameters.subscriptionOffer?.offerToken;
        }
        
        try {
          const success = await MyPurchaseSystem.purchase(storeProductId)
          if (success) {
            Purchasely.synchronize(); // synchronize all purchases with Purchasely
            Purchasely.onProcessAction(false); // notify Purchasely paywall to stop processing action
            Purchasely.closePresentation(); // close the current screen displayed by Purchasely
          }
        } catch (e) {
           Purchasely.onProcessAction(false); // notify Purchasely paywall to stop processing action
        }
      } catch (e) {
        console.log(e);
        Purchasely.onProcessAction(false); // notify Purchasely paywall to stop processing action
      }
    } else if (result.action === PLYPaywallAction.RESTORE) {
      try {
        const restore = await MyPurchaseSystem.restorePurchases();
        
        Purchasely.onProcessAction(false); // notify Purchasely paywall to stop processing action
        Purchasely.synchronize(); // synchronize all purchases with Purchasely
        Purchasely.closePresentation(); // close the current screen displayed by Purchasely
      } catch (e) {
        Purchasely.onProcessAction(false); // notify Purchasely paywall to stop processing action
      }
    } else {
      Purchasely.onProcessAction(true); // notify Purchasely paywall to continue other actions
    }
  });
Purchasely.setPaywallActionInterceptorCallback(
          (PaywallActionInterceptorResult result) {
    if (result.action == PLYPaywallAction.purchase) {
      try {
        //the store product id (sku) the user clicked on in the paywall
        var productId = result.parameters.plan.productId
     
        if (Platform.isAndroid) {
           // Only for Android you can get other interesting parameters
          String subscriptionId = result.parameters.subscriptionOffer?.subscriptionId
          String basePlanId = result.parameters.subscriptionOffer?.basePlanId;
          String offerId = result.parameters.subscriptionOffer?.offerId;
          String offerToken = result.parameters.subscriptionOffer?.offerToken;
        }
        
        bool success = await MyPurchaseSystem.purchase(productId);
        if (success) {
          // synchronize all purchases with Purchasely
          Purchasely.synchronize();
          // notify Purchasely paywall to stop processing action
          Purchasely.onProcessAction(false);
        }
      } catch (e) {
        Purchasely.onProcessAction(false);
        print(e);
      }
    } if (result.action == PLYPaywallAction.restore) {
      Purchasely.onProcessAction(false);
      
      try {
        await MyPurchaseSystem.restoreAllPurchases();
       
        // synchronize all purchases with Purchasely
        Purchasely.synchronize();
        // notify Purchasely paywall to stop processing action
        Purchasely.onProcessAction(false);
      } on PlatformException catch (e) {
        Purchasely.onProcessAction(false);
        // Error restoring purchases
      }
    } else {
      // notify Purchasely paywall to continue other actions
      Purchasely.onProcessAction(true);
    }
 });
Purchasely.setPaywallActionInterceptorCallback((result) => {
    if (result.action === Purchasely.PaywallAction.purchase) {
        // the store product id (sku) the user clicked on in the paywall
        const storeProductId = result.parameters.plan.productId;

        MyPurchaseSystem.purchase(storeProductId, ({ success, error }) => {
            if (success) {
                // synchronize all purchases with Purchasely
                Purchasely.synchronize();
            }
            // notify Purchasely paywall to stop processing action
            Purchasely.onProcessAction(false);
        }, ({ error, userCancelled }) => {
            // Error making purchase
            Purchasely.onProcessAction(false);
        });
    } else if (result.action === Purchasely.PaywallAction.restore) {
        MyPurchaseSystem.restoreTransactions(
            info => {
                // synchronize all purchases with Purchasely
                Purchasely.synchronize();
                // notify Purchasely paywall to stop processing action
                Purchasely.onProcessAction(false);
            },
            error => {
                // Error restoring purchases
                // notify Purchasely paywall to stop processing action
                Purchasely.onProcessAction(false);
            }
        );
    } else {
        // notify Purchasely paywall to continue other actions
        Purchasely.onProcessAction(true);
    }
});
purchasely.SetPaywallActionInterceptor(OnPaywallActionIntercepted);

private void OnPaywallActionIntercepted(PaywallAction action)
	{
		Log($"Purchasely Paywall Action Intercepted. Action: {action.action}.");

		switch (action.action)
		{
			case "purchase":
				var storeProductId = action.parameters.plan?.storeProductId;
				var basePlanId = action.parameters.plan?.basePlanId;
				var offerId = action.parameters.offer?.storeOfferId;

				MyPurchaseSystem.purchase(storeProductId, basePlanId, offerId);
				// if purchase successful, synchronize all purchases with Purchasely
				purchasely.Synchronize();
				
				// notify Purchasely paywall to stop processing action
				purchasely.ProcessPaywallAction(false);
        
        // dismiss the paywall if you want
        purchasely.ClosePresentation();

				break;
			case "restore":
				MyPurchaseSystem.restoreTransactions();
        // synchronize all purchases with Purchasely
				purchasely.Synchronize();
				// notify Purchasely paywall to stop processing action
				purchasely.ProcessPaywallAction(false);
        // dismiss the paywall if you want
        purchasely.ClosePresentation();
				break;
			default:
					purchasely.ProcessPaywallAction(true);
					break;
		}
}

🚧

Don't forget to call synchronize() after the transaction has been processed

Calling this method allows the Purchasely SDK to observe the transaction, i.e fetch the receipt and pass it to the Purchasely Platform to extract the data out of it without interfering with it

Processing transaction with RevenueCat

Here is a code sample using the Paywall Action Interceptor to process transactions with RevenueCat, for the actions purchase and restore:

Purchasely.setPaywallActionsInterceptor { [weak self] (action, parameters, presentationInfos, proceed) in
    switch action {
    // Intercept the tap on purchase to display the terms and condition
    case .purchase:
        // Grab the plan to purchase
        guard let plan = parameters?.plan, let appleProductId = plan.appleProductId else {
            return
        }

        Purchases.shared.getOfferings { (offerings, error) in
            if let packages = offerings?.current?.availablePackages {
                if let package = packages.first(where: { $0.storeProduct.productIdentifier == appleProductId }) {
                    Purchases.shared.purchase(package: package) { (transaction, customerInfo, error, userCancelled) in
                        /** IMPORTANT for Purchasely **/
                        // synchronize new purchase with Purchasely
                        Purchasely.synchronize()
                        // notify Purchasely paywall to stop processing action
                        proceed(false)

                        if customerInfo.entitlements["your_entitlement_id"]?.isActive == true {
                            // Unlock that great "pro" content              
                        }
                    }
                }
            }
        }
    case .restore:
        Purchases.shared.restorePurchases { customerInfo, error in
            /** IMPORTANT for Purchasely **/
            // synchronize new purchase with Purchasely
            Purchasely.synchronize()
            // notify Purchasely paywall to stop processing action
            proceed(false)
        }
    default:
        proceed(true) // notify Purchasely paywall to continue other actions
    }
}
Purchasely.setPaywallActionsInterceptor { info, action, parameters, processAction ->
    when(action) {
        PLYPresentationAction.PURCHASE -> {
            val subscriptionId = parameters.subscriptionOffer?.subscriptionId
            val basePlanId = parameters.subscriptionOffer?.basePlanId
            val offerId = parameters.subscriptionOffer?.offerId
            val offerToken = parameters.subscriptionOffer?.offerToken
            
            //get RevenueCat package
            Purchases.sharedInstance.getOfferingsWith({ error ->
            // An error occurred
            }) { offerings ->
                offerings.current
                    ?.availablePackages
                    ?.takeUnless { it.isNullOrEmpty() }
                    ?.let { list ->
                     val rcPackage = list.firstOrNull { it.product.sku == subscriptionId }
                     
                     Purchases.sharedInstance.purchasePackage(
                        this,
                        rcPackage,
                        onError = { error, userCancelled -> 
                            /* No purchase */
                            //stop process on Purchasely side
			    processAction(false)
                        },
                        onSuccess = { product, customerInfo ->
                            //stop process on Purchasely side
			    processAction(false)
                            if (customerInfo.entitlements["my_entitlement_identifier"]?.isActive == true) {
                                // Unlock that content and synchronize with Purchasely
                                Purchasely.synchronize()
                                processAction(false)
                            }
                    })
                }
            }
        }
        PLYPresentationAction.RESTORE -> {
           // restore purchases with RevenueCat
            Purchases.sharedInstance.restorePurchases(::showError) { customerInfo ->
                //... check customerInfo to see if entitlement is now active
                
                //one this is done, stop Purchasely process and synchronize
	        			processAction(false)
                Purchasely.synchronize() // synchronize all purchases with Purchasely
            }
        }
        else -> processAction(true) // notify Purchasely paywall to continue other actions
    }
}
Purchasely.setPaywallActionInterceptorCallback((result) => {
    if (result.action === PLYPaywallAction.PURCHASE) {
      try {
        //the store product id (sku) the user clicked on in the paywall
        String storeProductId = result.parameters.plan.productId
        
        if (Platform.OS === 'android') {
          // Only for Android you can retrieve other information about the purchase
          const basePlanId = result.parameters.subscriptionOffer?.basePlanId;
          const offerId = result.parameters.subscriptionOffer?.offerId;
          const offerToken = result.parameters.subscriptionOffer?.offerToken;
        }
        
        try {
          const offerings = await Purchases.getOfferings();
          if (offerings.current !== null && offerings.current.availablePackages.length !== 0) {
            //get your package
            const package = offerings.current.monthly;
            
            //and purchase with RevenueCat
            try {
              const {customerInfo, productIdentifier} = await Purchases.purchasePackage(package);
              if (typeof customerInfo.entitlements.active.my_entitlement_identifier !== "undefined") {
                Purchasely.synchronize(); // synchronize all purchases with Purchasely
              }
              Purchasely.onProcessAction(false); // notify Purchasely paywall to stop processing action
              Purchasely.closePresentation(); // close the current screen displayed by Purchasely
            } catch (e) {
              Purchasely.onProcessAction(false); // notify Purchasely paywall to stop processing action
              if (!e.userCancelled) {
                showError(e);
              }
            }
          }
        } catch (e) {
           Purchasely.onProcessAction(false); // notify Purchasely paywall to stop processing action
        }
      } catch (e) {
        console.log(e);
        Purchasely.onProcessAction(false); // notify Purchasely paywall to stop processing action
      }
    } else if (result.action === PLYPaywallAction.RESTORE) {
      try {
        const restore = await Purchases.restorePurchases();
        // ... check restored purchaserInfo to see if entitlement is now active
        
        Purchasely.onProcessAction(false); // notify Purchasely paywall to stop processing action
        Purchasely.synchronize(); // synchronize all purchases with Purchasely
        Purchasely.closePresentation(); // close the current screen displayed by Purchasely
      } catch (e) {
        Purchasely.onProcessAction(false); // notify Purchasely paywall to stop processing action
      }
    } else {
      Purchasely.onProcessAction(true); // notify Purchasely paywall to continue other actions
    }
  });
Purchasely.setPaywallActionInterceptorCallback(
          (PaywallActionInterceptorResult result) {
    if (result.action == PLYPaywallAction.purchase) {
      try {
        //the store product id (sku) the user clicked on in the paywall
        var productId = result.parameters.plan.productId
        
        if(Platform.isAndroid) {
          // Only for Android you can get other interesting parameters
          String basePlanId = result.parameters.subscriptionOffer?.basePlanId;
          String offerId = result.parameters.subscriptionOffer?.offerId;
          String offerToken = result.parameters.subscriptionOffer?.offerToken;
        }
        
        Offerings offerings = await Purchases.getOfferings();
        if (offerings.current != null && offerings.current.monthly != null) {
          //get your product from revenuecat
          Product product = offerings.current.monthly.product;
          
          //start purchase
          PurchaserInfo purchaserInfo = await Purchases.purchasePackage(product);
          if (purchaserInfo.entitlements.all["my_entitlement_identifier"].isActive) {
            // synchronize all purchases with Purchasely
            Purchasely.synchronize();
          }
          // notify Purchasely paywall to stop processing action
          Purchasely.onProcessAction(false);
        }
      } catch (e) {
        Purchasely.onProcessAction(false);
        print(e);
      }
    } if (result.action == PLYPaywallAction.restore) {
      Purchasely.onProcessAction(false);
      
      try {
        PurchaserInfo restoredInfo = await Purchases.restoreTransactions();
        // ... check restored purchaserInfo to see if entitlement is now active
        
        // synchronize all purchases with Purchasely
        Purchasely.synchronize();
        // notify Purchasely paywall to stop processing action
        Purchasely.onProcessAction(false);
      } on PlatformException catch (e) {
        Purchasely.onProcessAction(false);
        // Error restoring purchases
      }
    } else {
      // notify Purchasely paywall to continue other actions
      Purchasely.onProcessAction(true);
    }
 });
Purchasely.setPaywallActionInterceptorCallback((result) => {
    if (result.action === Purchasely.PaywallAction.purchase) {
      //the store product id (sku) the user clicked on in the paywall
      const storeProductId = result.parameters.plan.productId
      
      Purchases.getOfferings(
          offerings => {
            if (offerings.current && offerings.current.monthly) {  
              //get your package from RevenueCat
              const product = offerings.current.monthly;
             
              Purchases.purchasePackage(product, ({ productIdentifier, purchaserInfo }) => {
                  if (typeof purchaserInfo.entitlements.active.my_entitlement_identifier !== "undefined") {
                    // synchronize all purchases with Purchasely
                    Purchasely.synchronize();
                  }
                  // notify Purchasely paywall to stop processing action
          				Purchasely.onProcessAction(false);
                },
                ({error, userCancelled}) => {
                  // Error making purchase
                  Purchasely.onProcessAction(false)
                }
              );
            }
          },
          error => {
     				Purchasely.onProcessAction(false)
          }
      );
    } if (result.action === Purchasely.PaywallAction.restore) {
      Purchases.restoreTransactions(
        info => {
          // synchronize all purchases with Purchasely
          Purchasely.synchronize();
          // notify Purchasely paywall to stop processing action
          Purchasely.onProcessAction(false);
        },
        error => {
          // Error restoring purchases
          // notify Purchasely paywall to stop processing action
          Purchasely.onProcessAction(false);
        }
      );
    } else {
      // notify Purchasely paywall to continue other actions
      Purchasely.onProcessAction(true);
    }
  });
purchasely.SetPaywallActionInterceptor(OnPaywallActionIntercepted);

private void OnPaywallActionIntercepted(PaywallAction action)
	{
		Log($"Purchasely Paywall Action Intercepted. Action: {action.action}.");

		switch (action.action)
		{
			case "purchase":
				var storeProductId = action.parameters.plan?.storeProductId;
				var basePlanId = action.parameters.plan?.basePlanId; //only for Android with Google
				var offerId = action.parameters.offer?.storeOfferId;
        
        var purchases = GetComponent<Purchases>();
        purchases.GetOfferings((offerings, error) =>
        {
          // Get the offering and product that matches the storeProductId and offerID
          // Here just a sample from RevenueCat documentation with the Monthly product
          if (offerings.Current != null && offerings.Current.Monthly != null){
            var product = offerings.Current.Monthly.Product;
            
            purchases.PurchasePackage(package, (product, customerInfo, userCancelled, error) =>
            {
              if (customerInfo.Entitlements.Active.ContainsKey("my_entitlement_identifier")) {
                // synchronize purchases with Purchasely
								purchasely.Synchronize();
              }
              
              // notify Purchasely paywall to stop processing action and hide loader
							purchasely.ProcessPaywallAction(false);
              
              // dismiss the paywall if you want
              purchasely.ClosePresentation();
            });
          }
        });
				break;
			case "restore":
				var purchases = GetComponent<Purchases>();
        purchases.RestorePurchases((info, error) =>
        {
            //... check purchaserInfo to see if entitlement is now active
          
          	// synchronize all purchases with Purchasely
            purchasely.Synchronize();
            // notify Purchasely paywall to stop processing action
            purchasely.ProcessPaywallAction(false);
            // dismiss the paywall if you want
            purchasely.ClosePresentation();
        }
				break;
			default:
					purchasely.ProcessPaywallAction(true);
					break;
		}
}

🚧

Don't forget to call synchronize() after the transaction has been processed

Calling this method allows the Purchasely SDK to observe the transaction, i.e fetch the receipt and pass it to the Purchasely Platform to extract the data out of it without interfering with it


More details on the Paywall Action Interceptor and how to intercept of types of actions


What’s Next

It's time to test that everything is functioning correctly