Use Apple promotional offers and Google developer determined offers to create win-back campaigns
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 AppStore Connect, Google Play Console and Purchasely Console
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 StoreKit 2, 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 few steps. Once completed, you can update your application settings in Purchasely console.
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
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 start() method of our SDK.
AppStore Connect configuration
A promotional offer is only available for current and previous subscribers of the selected subscription. You can create it from AppStore Connect in the same page where you manage your subscription price and introductory offers
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)
Once created, copy the id you have set for this offer to paste in Purchasely Console
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)
Your offer must contain the following information:
- Offer id: you can chose anything, it will be the one you will fill in Purchasely console
- 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
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
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 tags 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 placement for them. You can also target them using Audience
We will send specific events to your webhook or external integration:
PROMOTIONAL_OFFER_STARTED
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 purchasePurchasely.plan(with:"planId") { plan in// Success completion} failure: { error in// Failure completion}// Retrieve offer idlet promoOffer = plan.promoOffers.first(where: { $0.vendorId == promoOfferVendorId })// Then purchasePurchasely.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 systemPurchasely.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 foundif(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 offerPurchasely.purchase( activity, plyPlan, offer,null,// set a content id if needed (Function1<PLYPlan, Unit>) plyPlan1 ->null, (Function1<PLYError, Unit>) plyError ->null );returnnull; }, throwable ->null);
// Purchase with the plan vendor id and promotional offer vendor id // set in Purchasely Consoletry {constplan=awaitPurchasely.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 Consoletry {Map<dynamic, dynamic> plan =awaitPurchasely.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
When you are using Purchasely in paywallObserver mode, you can retrieve the offer from our paywall action interceptor, sign it (iOS only) and do the purchase with your system
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)inswitch action {// Intercept the tap on purchase to display the terms and conditioncase .purchase:// Grab the plan to purchaseguardlet 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 StoreKit1purchaseUsingStoreKit1(plan)// Using StoreKit2purchaseUsingStoreKit2(plan)// Finally close the process with Purchaselyproceed(false)default:proceed(true) }}funcpurchaseUsingStoreKit1(_plan: PLYPlan) {// First step: Get SKProduct using your own service// Examplelet 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 paymentguard SKPaymentQueue.canMakePayments()else {returnnil }let payment =SKMutablePayment(product: product) payment.applicationUsername = Purchasely.anonymousUserId.lowercased()// lowercase anonymous user id is mandatoryiflet signature = promotionalOfferSignature, #available(iOS12.2, macOS10.14.4, tvOS12.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)}funcpurchaseUsingStoreKit2(_plan: PLYPlan) {if#available(iOS15.0, *) { Purchasely.signPromotionalOffer(storeProductId: plan.appleProductId, storeOfferId: plan.promoOffers.first?.storeOfferId, success: { promoOfferSignature in Task {do {let products =tryawait Product.products(for: ["storeProductId"])var options: Set<Product.PurchaseOption>= [.simulatesAskToBuyInSandbox(<Bool:truefor testing>)]let userId = Purchasely.anonymousUserId.lowercased() options.insert(.appAccountToken(userId))iflet 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) }iflet product = products.first {let purchaseResult =tryawait 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?.planval sku = plan?.store_product_idval offer = parameters?.offerval offerId = offer?.storeOfferId// TODO purchase with SKU and offer id// Finally close the process with PurchaselyprocessAction(false) }else->processAction(true) }}
Purchasely.setPaywallActionsInterceptor((info, action, parameters, listener) -> {switch (action) {case PURCHASE:if(parameters ==null||parameters.plan==null) returnString sku =parameters.plan.getStore_product_id();PLYPLan plan =parameters.planString sku =plan.getStore_product_id();PLYOffer offer =parameters.offerString offerId =offer.getStore_offer_id();// TODO purchase with SKU and offer id// Finally close the process with Purchaselylistener.processAction(false);break;default:listener.processAction(true); }});
Purchasely.setPaywallActionInterceptorCallback((result) => {switch (result.action) {casePLYPaywallAction.PURCHASE:// Retrieve the store product id and offer idconststoreProductId=result.parameters.plan?.productId;conststoreOfferId=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 tokenconstproductId=result.parameters.subscriptionOffer?.subscriptionId;constbasePlanId=result.parameters.subscriptionOffer?.basePlanId;constofferId=result.parameters.subscriptionOffer?.offerId;constofferToken=result.parameters.subscriptionOffer?.offerToken;// -- END GOOGLE --// -- APPLE ONLY --if(storeOfferId !=null) {try {async() => {constsignature=awaitPurchasely.signPromotionalOffer(storeProductId, storeOfferId);constanonymousUserId=awaitPurchasely.getAnonymousUserId();constappTokenUserId=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 wantPurchasely.hidePresentation();// TODO launch purchase flow// When purchase is done, call this method to stop loader on Purchasely paywallPurchasely.onProcessAction(false);// if successful, close the paywallPurchasely.closePresentation();// if not successful, display the paywall againPurchasely.showPresentation()break;default:Purchasely.onProcessAction(true); } });
Purchasely.setPaywallActionInterceptorCallback( (PaywallActionInterceptorResult result) {if (result.action ==PLYPaywallAction.purchase) {// Retrieve the store product id and offer idString? 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 tokenString? 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 =awaitPurchasely.signPromotionalOffer(storeProductId, storeOfferId);String? anonymousUserId =awaitPurchasely.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 wantPurchasely.hidePresentation();// TODO launch purchase flow// When purchase is done, call this method to stop loader on Purchasely paywallPurchasely.onProcessAction(false);// if successful, close the paywallPurchasely.closePresentation();// if not successful, display the paywall againPurchasely.showPresentation(); } else {Purchasely.onProcessAction(true); }});