Purchasely
4.4
4.4
  • Welcome page
  • General
    • Presentation
    • Release notes
  • Quick start
    • Console Configuration
    • SDK Implementation
    • Testing
    • Sample
  • Basic configuration
    • Console Configuration
      • Creating a new application
      • Creating your Products
        • App Store
        • Play Store
        • Huawei App Gallery
        • Amazon App Store
        • Products & Plans
      • Design your Paywalls
      • Design your Paywalls-Latest
        • Carousel
        • Carousel Flow
        • Features List
        • Features List & Plan Picker with 2 & 3 columns
        • Feature list overlay
        • Plan picker horizontal
        • Plan picker with 2 and 3 Column
    • SDK installation
      • iOS SDK
      • Android SDK
      • React Native SDK
      • Cordova SDK
      • Flutter SDK
      • Unity SDK
    • SDK configurations
      • Paywall observer mode
      • Full mode
      • StoreKit 2
      • Appendices
        • Start the SDK
        • Set User Id
        • Notify when the app is ready
        • Present paywalls
        • Unlock content / service
        • Close SDK (Android only)
    • Stripe
    • Purchasely with RevenueCat
  • S2S notifications
    • Server-to-server notifications ?
    • Apple App Store
    • Google Play Store
    • Huawei App Gallery
  • Analytics
    • Dashboards
      • Introduction
      • Live
      • Subscriptions
      • Cohorts
      • Trials
      • Events
    • Events
      • Webhook events
        • Subscription events
        • Subscription events attributes
      • SDK events
        • UI events
        • UI attributes
  • Integrations
    • Webhook
      • Receiving and understanding messages
      • Managing entitlements
      • Detailed sequence diagrams
    • Airship
    • Amplitude
    • AppsFlyer
    • Adjust
    • Piano analytics(ex AT Internet)
    • Batch
    • Branch
    • Braze
    • Clevertap
    • Customer.io
    • Firebase
    • Iterable
    • Mixpanel
    • MoEngage
    • OneSignal
    • Segment
    • Brevo(ex Sendinblue)
  • Advanced Features
    • Asynchronous paywalls
    • NEW: Promotional offers
    • Anonymous user
    • Associating content
    • Audiences
    • Customising UI
      • Errors & alerts
      • Controllers (iOS) / Fragments (Android)
    • Deeplinks automations
    • Disable placements
    • Displaying users subscriptions
    • Localization
    • Lottie animations
    • Non-subscription products
    • Paywall action interceptor
    • Promoting your products
      • Self-promotion
      • Promoting In-App Purchases
    • Purchase manually
    • Subscription status
    • Use your own paywall
  • Others
    • Frequently Asked Questions
    • Migration guides
      • Migrate to Purchasely
      • Webhook
        • Migrate to Webhook v3.0
      • SDK
        • Migrate to SDK v3.0
          • v2.2.0
          • v2.1.3
        • Migrate to SDK v3.1
        • Migrate to SDK v3.2
        • Migrate to SDK v4.0.0
  • TESTING
    • Testing Cycle Durations
Powered by GitBook

© Purchasely 2020-2023

On this page
  • Apple
  • Console configuration
  • AppStore Connect configuration
  • Google
  • Purchasely Console
  • Trigger the purchase of an offer
  • Retrieve the offer to purchase in observer mode

Was this helpful?

Edit on GitHub
  1. Advanced Features

NEW: Promotional offers

Use Apple promotional offers and Google developer determined offers to create win-back campaigns

PreviousAsynchronous paywallsNextAnonymous user

Last updated 1 year ago

Was this helpful?

The feature described in this section is supported on the following versions and above:

  • iOS: 4.0.1

  • Android: 4.0.0

  • ReactNative: 4.0.1

  • Flutter: 4.0.0

If you use a prior version of the SDK your users won’t see a discount and will purchase at the regular price.

In this article we are going to describe the process to create promotional offers on , and Promotional offers can be used to offer a specific discount to current or past subscribers. It is a great way to retain or win-back a customer. You will be able to set up as many as you want by creating specific paywalls with those offers.

You are responsible for the eligibility of those promotional offers, you must create a specific paywall and display it only for the users you want to target (see Purchasely console below for more details)

Apple

Console configuration

Purchasely must have an Apple certificate to sign promotional offers, the configuration is exactly the same than for , so if you already did it you can skip that part and move to AppStore Connect configuration

Allowing Purchasely to sign promotional offers requires a . Once completed, you can update your application settings in Purchasely console.

Enable App Store Connect API access

  • Sign in to

  • Go to "Users and Access"

  • Select "Keys" under the "In-App Purchase" section

  • Click on the "+" button to generate a new API key

  • Choose a name for the key and click "Generate"

  • Download the API key file (.p8), and note the Key ID and Issuer ID. Keep the file secure, as you won't be able to download it again

Setup on Purchasely Console

  • Go to "App Settings"

  • Select Apple App Store" under the "Store configuration" section

  • Fill in the Private Key Id from the key you generated

  • Upload your Private Key File (.p8)

  • Fill your Issuer Id

  • Click on Save in the top right corner

AppStore Connect configuration

From that page select Promotional Offers tab and then click on the + button to create a new one

Setup the discount you wish to offer, it can be a: - free (example: 3 months free then $9,99/month) - pay up front (example: $14,99 for 3 months then $9,99/month) - pay as your go (example: $4,99/month for 3 months then $9,99/month)

Google

Promotional offers for Google are Developer Determined Offer which can be set on your base plans for a subscription. It requires the usage of Google Play Billing v5 which is included in Purchasely SDK 4.0.0

Developer determined offer are available for all your users all the time. As the name suggest, it is up to you to decide when to make this offer available. Unfortunately Purchasely SDK cannot know the offer type, so by default this offer will be presented to all your users by our SDK. To avoid this, you can add the tag ignore-offer (see below for more details)

Then chose the base plan to apply this offer to

To avoid offering these offers to all your users, we strongly suggest to add the tag ignore-offer to all your developer determined offers so that Purchasely SDK won't display it to your users unless explicitly defined in a paywall as a promotional offer

Purchasely Console

When your promotional offer has been created on AppStore Connect and/or Google Play Console, the final step is to declare it in Purchasely Console to use it with your paywall First edit the plan where you want to declare you new promotional offer. The plan MUST be the App Store or Play Store product that you used to declare your offer

Set a name, an identifier for Purchasely and the identifiers you have set in AppStore Connect and Google Play Console. Finally click Save to apply your changes

PROMOTIONAL_OFFER_CONVERTED

PROMOTIONAL_OFFER_NOT_CONVERTED

The information about the promotional offer will also be in the payload of events like the one above and ACTIVATE

offer_type: "PROMOTIONAL_OFFER"

Trigger the purchase of an offer

Purchasely handles automatically the purchase from a Purchasely paywall but if you are displaying your own paywall or purchase button, you may want to trigger the purchase with Purchasely. You can do it really easily by providing the PLYPlan and PLYOffer you want to use for the purchase

// First get the plan you want to purchase
Purchasely.plan(with: "planId") { plan in
// Success completion
} failure: { error in 
// Failure completion
}

// Retrieve offer id
let promoOffer = plan.promoOffers.first(where: { $0.vendorId == promoOfferVendorId })

// Then purchase
Purchasely.purchaseWithPromotionalOffer(plan: plan,
                                        contentId: nil,
                                        storeOfferId: promoOffer.storeOfferId) {                    
// Success completion       
} failure: { error in
// Failure completion                    
}

...

// We also offer the possibility to sign your promotional offers 
// if you want to purchase with your own system
Purchasely.signPromotionalOffer(storeProductId: "storeProductId",
                                storeOfferId: "storeOfferId") { signature in            
// Success completion
} failure: { error in
// Failure completion
}
[Purchasely setPaywallActionsInterceptor:^(enum PLYPresentationAction action, PLYPresentationActionParameters *parameters, PLYPresentationInfo *presentationInfos, void (^ proceed)(BOOL)) {
        switch (action) {
            // Intercept the tap on purchase to display the terms and condition
            case PLYPresentationActionPurchase:{
                // Grab the apple product id to purchase
                NSString *appleProductId = parameters.plan.appleProductId;
                NSString *appleOfferId = parameters.offer.storeOfferId;
                
                // Sign the offer with signPromotionalOffer method from Purchasely
            
                // TODO purchase with product id and signature
                break;
            }
            default:
                proceed(true);
                break;
        }
    }];
val plan = Purchasely.plan("plan_id on purchasely console")
val offer = plan?.promoOffers?.firstOrNull { it.vendorId == "offer_id on purchasely console" }

Purchasely.purchase(activity, plan, offer, onSuccess = { plan ->
  Log.d("Purchasely", "Purchase success with ${plan?.name}")
}, onError = {
  Log.e("Purchasely", "Purchase error", it)
})
Purchasely.plan(
        "plan_id on purchasely console",
        plyPlan -> {
            List<PLYPromoOffer> offers = plyPlan.getPromoOffers();
            PLYPromoOffer offer = null;
            
            // retrieve PLYPromoOffer from plan if found
            if(offers != null && offers.size() > 0) {
                for(int i = 0; i < offers.size(); i++) {
                    if(plyPlan.getPromoOffers().get(i).getVendorId().equals("offer_id on purchasely console")) {
                        offer = plyPlan.getPromoOffers().get(i);
                        break;
                    }
                }
            }
            
            // Purchase plan with offer
            Purchasely.purchase(
                activity, 
                plyPlan, 
                offer, 
                null, // set a content id if needed
                (Function1<PLYPlan, Unit>) plyPlan1 -> null, 
                (Function1<PLYError, Unit>) plyError -> null
            );
            
            return null;
        },
        throwable -> null
);
// Purchase with the plan vendor id and promotional offer vendor id 
// set in Purchasely Console
try {
  const plan = await Purchasely.purchaseWithPlanVendorId(
    'PURCHASELY_PLUS_YEARLY',
    'PROMOTIONAL_OFFER_ID',
    null, // optional content id
  );
  console.log('Purchased plan: ' + plan);
} catch (e) {
  console.error(e);
}
// Purchase with the plan vendor id and promotional offer vendor id 
// set in Purchasely Console
try {
    Map<dynamic, dynamic> plan = await Purchasely.purchaseWithPlanVendorId(
        vendorId: 'PURCHASELY_PLUS_MONTHLY', offerId: 'PROMOTIONAL_OFFER_ID');
    print('Purchased plan is $plan');
} catch (e) {
    print(e);
}

Retrieve the offer to purchase in observer mode

On iOS, Purchasely anonymous user in lowercase is required as applicationUsername with StoreKit1 or appAccountToken with StoreKit2 Please look at sample code below for more details

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 {
				proceed(false)
				return
			}
			
			let offer = parameters?.promoOffer
	
			// sign the offer
        		Purchasely.signPromotionalOffer(storeProductId: appleProductId,
        						storeOfferId: offer?.storeOfferId) { signature in
            		// Success completion
       		 	} failure: { error in
            		// Failure completion
        		}
				
			// Purchase with signature
			
			// Using StoreKit1
			purchaseUsingStoreKit1(plan) 
			// Using StoreKit2
			purchaseUsingStoreKit2(plan)
			
			// Finally close the process with Purchasely
			proceed(false)
		default:
			proceed(true)
	}
}

func purchaseUsingStoreKit1(_ plan: PLYPlan) {

            // First step: Get SKProduct using your own service
            
            // Example
            let request = SKProductsRequest(productIdentifiers: Set<String>([plan.appleProductId ?? ""]))
            request.delegate = <Your delegate> // Get Product in the `productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse)` method
            request.start()
            
            // Second Request payment
            guard SKPaymentQueue.canMakePayments() else {
                return nil
            }
            
            let payment = SKMutablePayment(product: product)
            
            payment.applicationUsername = Purchasely.anonymousUserId.lowercased() // lowercase anonymous user id is mandatory
            
            if let signature = promotionalOfferSignature, #available(iOS 12.2, macOS 10.14.4, tvOS 12.2, *) {
                let paymentDiscount = SKPaymentDiscount(identifier: signature.identifier,
                                                        keyIdentifier: signature.keyIdentifier,
                                                        nonce: signature.nonce,
                                                        signature: signature.signature,
                                                        timestamp: NSNumber(value: signature.timestamp))
                payment.paymentDiscount = paymentDiscount
            }
            
            SKPaymentQueue.default().add(payment)
}

func purchaseUsingStoreKit2(_ plan: PLYPlan) {
    if #available(iOS 15.0, *) {
                Purchasely.signPromotionalOffer(storeProductId: plan.appleProductId,
                                                storeOfferId: plan.promoOffers.first?.storeOfferId,
                                                success: { promoOfferSignature in
    
                    Task {
    
                        do {
                            let products = try await Product.products(for: ["storeProductId"])
                            var options: Set<Product.PurchaseOption> = [.simulatesAskToBuyInSandbox(<Bool: true for testing>)]
    
                            let userId = Purchasely.anonymousUserId.lowercased()
                            options.insert(.appAccountToken(userId))
    
                            if let decodedSignature = Data(base64Encoded: promoOfferSignature.signature) {
                                let offerOption:Product.PurchaseOption = .promotionalOffer(offerID: promoOfferSignature.identifier,
                                                                                           keyID: promoOfferSignature.keyIdentifier,
                                                                                           nonce: promoOfferSignature.nonce,
                                                                                           signature: decodedSignature,
                                                                                           timestamp: Int(promoOfferSignature.timestamp))
                                options.insert(offerOption)
                            }
    
                            if let product = products.first {
                                let purchaseResult = try await product.purchase(options: options)
                            }                        
                        }
                    }
                }, failure: { error in
    
                })
            } else {
                // Fallback on earlier versions
            }
    }
}
[Purchasely setPaywallActionsInterceptor:^(enum PLYPresentationAction action, PLYPresentationActionParameters *parameters, PLYPresentationInfo *presentationInfos, void (^ proceed)(BOOL)) {
        switch (action) {
            // Intercept the tap on purchase to display the terms and condition
            case PLYPresentationActionPurchase:{
                // Grab the apple product id to purchase
                NSString *appleProductId = parameters.plan.appleProductId;
                NSString *appleOfferId = parameters.offer.storeOfferId;
                
                // Sign the offer with signPromotionalOffer method from Purchasely
            
                // TODO purchase with product id and signature
                break;
            }
            default:
                proceed(true);
                break;
        }
    }];
Purchasely.setPaywallActionsInterceptor { info, action, parameters, processAction ->
    when(action) {
        PLYPresentationAction.PURCHASE -> {
            val plan = parameters?.plan
            val sku = plan?.store_product_id
            val offer = parameters?.offer
            val offerId = offer?.storeOfferId

            // TODO purchase with SKU and offer id
            
            // Finally close the process with Purchasely
            processAction(false)
        }
        else -> processAction(true)
    }
}
Purchasely.setPaywallActionsInterceptor((info, action, parameters, listener) -> {
    switch (action) {
        case PURCHASE:
            if(parameters == null || parameters.plan == null) return
            
            String sku = parameters.plan.getStore_product_id();
            
            PLYPLan plan = parameters.plan
            String sku = plan.getStore_product_id();
            PLYOffer offer = parameters.offer
            String offerId = offer.getStore_offer_id();

            // TODO purchase with SKU and offer id
            
            // Finally close the process with Purchasely
            listener.processAction(false);
            break;
        default:
            listener.processAction(true);
    }
});
Purchasely.setPaywallActionInterceptorCallback((result) => {
    switch (result.action) {
      case PLYPaywallAction.PURCHASE:
        // Retrieve the store product id and offer id
        const storeProductId = result.parameters.plan?.productId;
        const storeOfferId = result.parameters.offer?.storeOfferId;

        // -- GOOGLE ONLY --
        // Alternatively, just for Google with v5 and v6 you can retrieve everything if it simpler for you,
        // specially if you want the offer token
        const productId = result.parameters.subscriptionOffer?.subscriptionId;
        const basePlanId = result.parameters.subscriptionOffer?.basePlanId;
        const offerId = result.parameters.subscriptionOffer?.offerId;
        const offerToken = result.parameters.subscriptionOffer?.offerToken;
        // -- END GOOGLE --

        // -- APPLE ONLY --
        if(storeOfferId != null) {
          try {
            async() => {
              const signature = await Purchasely.signPromotionalOffer(storeProductId, storeOfferId);
              const anonymousUserId = await Purchasely.getAnonymousUserId();
              const appTokenUserId = anonymousUserId.toLowerCase();

              // You need the signature and appTokenUserId to validate the offer
            }
          } catch (e) {
            console.log("Error while signing promotional offer");
            console.error(e);
          }
        }
        // -- END APPLE --


        // Now that you have the ids you need, you can launch your purchase flow

        // Hide Purchasely paywall if you want
        Purchasely.hidePresentation();

        // TODO launch purchase flow

        // When purchase is done, call this method to stop loader on Purchasely paywall
        Purchasely.onProcessAction(false);

        // if successful, close the paywall
        Purchasely.closePresentation();

        // if not successful, display the paywall again
        Purchasely.showPresentation()
        break;
      default:
        Purchasely.onProcessAction(true);
    }
  });
Purchasely.setPaywallActionInterceptorCallback(
          (PaywallActionInterceptorResult result) {
  if (result.action == PLYPaywallAction.purchase) {
    // Retrieve the store product id and offer id
      String? storeProductId = result.parameters.plan?.productId;
      String? storeOfferId = result.parameters.offer?.storeOfferId;

      // -- GOOGLE ONLY --
      // Alternatively, just for Google with v5 and v6 you can retrieve everything if it simpler for you,
      // specially if you want the offer token
      String? productId = result.parameters.subscriptionOffer?.subscriptionId;
      String? basePlanId = result.parameters.subscriptionOffer?.basePlanId;
      String? offerId = result.parameters.subscriptionOffer?.offerId;
      String? offerToken = result.parameters.subscriptionOffer?.offerToken;
      // -- END GOOGLE --

      // -- APPLE ONLY --
      if(storeProductId != null && storeOfferId != null) {
        try {
          Map signature = await Purchasely.signPromotionalOffer(storeProductId, storeOfferId);
          String? anonymousUserId = await Purchasely.anonymousUserId;
          String? appTokenUserId = anonymousUserId.toLowerCase();

            // You need the signature and appTokenUserId to validate the offer
            // Signature contains those fields
            /*
              signature['identifier'] as String
              signature['signature'] as String
              signature['keyIdentifier'] as String
              signature['timestamp'] as int
            */
        } catch (e) {
          print("Error while signing promotional offer");
          print(e);
        }
      }
      // -- END APPLE --


      // Now that you have the ids you need, you can launch your purchase flow

      // Hide Purchasely paywall if you want
      Purchasely.hidePresentation();

      // TODO launch purchase flow

      // When purchase is done, call this method to stop loader on Purchasely paywall
      Purchasely.onProcessAction(false);

      // if successful, close the paywall
      Purchasely.closePresentation();

      // if not successful, display the paywall again
      Purchasely.showPresentation();
  } else {
    Purchasely.onProcessAction(true);
  }
});

Connect to

Once this configuration is set, Purchasely SDK 4.0.0 and up will be configured to use StoreKit 2 as default. If you wish to remain with Store Kit 1, which also works with promotional offers, you need to force it in the method of our SDK.

A promotional offer is only available for current and previous subscribers of the selected subscription. You can create it from in the same page where you manage your subscription price and introductory offers

Once created, copy the id you have set for this offer to paste in

To create an offer, go to your and select Add offer

Your offer must contain the following information: - Offer id: you can chose anything, it will be the one you will fill in - Eligibility criteria: Developer determined - Tags: ignore-offer (see notice below) - Phases: you can add up to 2 phases, one free trial and one price discount

Then you can create a paywall for your offer, we have created a new action button for that: Winback/retention offer You need to select your plan and offer to be applied as the action for this button You can use the field "Offer" of the different labels to set offers like OFFER_PRICE and OFFER_DURATION (same principle than trial offer)

You are responsible for displaying this paywall to users you want to target, so you should create a specific for them. You can also target them using

We will send specific events to your or external integration: PROMOTIONAL_OFFER_STARTED

When you are using Purchasely in mode, you can retrieve the offer from our , sign it (iOS only) and do the purchase with your system

AppStore Connect
Google Play Console
Purchasely Console
StoreKit 2
few steps
App Store Connect
Purchasely Console
AppStore Connect
application subscription
tags
placement
Audience
webhook
paywallObserver
paywall action interceptor
Purchasely Console
Purchasely console
start()