Implementing promotional offers into your app
This section provides details on how to handle promotional offers into your app
AvailabilityThe feature described in this section is supported starting from version 4.0.0 of Purchasely SDK.
If you use a prior version of the SDK your users won’t see a discount and will purchase at the regular price.
v6 running modeOn SDK v6,
observeris the default running mode. If you want Purchasely to trigger the promotional offer purchase automatically (as described infullmode below), you must setfullexplicitly.
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.
full mode - Trigger the purchase of an offer
full mode - Trigger the purchase of an offerIn full mode Purchasely handles automatically the promotional offer purchase directly from a Purchasely Paywall and you have nothing to do.
If you are using your own paywall or need the SDK to process the promotional offer transaction from your own purchase buttons, 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 (SDK 6.2.0 and later)
Purchasely.signPromotionalOffer(storeProductId: "storeProductId",
storeOfferId: "storeOfferId",
purchaseContextToken: nil) { signature, token in
// Success completion: put `token` in appAccountToken (StoreKit 2)
// or token.uuidString.lowercased() in applicationUsername (StoreKit 1)
} failure: { error in
// Failure completion
}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)
})// 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);
}// Purchase with the plan vendor id and promotional offer vendor id
// set in Purchasely Console
Purchasely.purchaseWithPlanVendorId(
'PURCHASELY_PLUS_YEARLY',
'PROMOTIONAL_OFFER_ID',
null, // optional content id
(plan) => console.log('Purchased plan: ' + plan),
(error) => console.error(error)
);observer mode - Retrieve the offer to purchase
observer mode - Retrieve the offer to purchaseWhen you are using Purchasely in observer mode, you can:
- retrieve the Plan and associated offer purchased by the user by using the action interceptor
- sign it (iOS only)
- and do the purchase with your own transaction processor
iOS - Sign the offer with a purchase context token (SDK 6.2.0 and later)
Availability
signPromotionalOffer(storeProductId:storeOfferId:purchaseContextToken:success:failure:)is available from version 6.2.0 of the Purchasely iOS SDK. It replaces the previous method, which is deprecated.On React Native, Flutter and Cordova, the method is
signPromotionalOfferWithToken, from bridge version 6.2.0. It does nothing on Android: the result isnullon React Native, an empty map on Flutter, and an empty success on Cordova.
This method gives the signature and a purchase context token. Purchasely uses the token to attribute the purchase to the paywall, placement, campaign and A/B test that started it.
Apple compares the signature with the value in the account field of the purchase. Put the token in that field. Do not change the token:
- StoreKit 2: add
.appAccountToken(token)to the purchase options. - StoreKit 1: set
applicationUsernametotoken.uuidString.lowercased(). Use lowercase letters. If the case is different, Apple rejects the offer.
Give nil as purchaseContextToken to get a new token. To sign again for the same purchase, for example after an error, give the token that you received before.
On React Native, Flutter and Cordova, give null (or no value) to get a new token. On iOS, a token that is not a UUID string makes the call fail, and the SDK does not sign. On Android, the bridges ignore the token and do nothing.
Purchasely.interceptAction(.purchase) { [weak self] info, params, completion in
guard let plan = params?.plan,
let appleProductId = plan.appleProductId,
let storeOfferId = params?.promoOffer?.storeOfferId else {
completion(.success)
return
}
Purchasely.signPromotionalOffer(storeProductId: appleProductId,
storeOfferId: storeOfferId,
purchaseContextToken: nil,
success: { signature, token in
Task {
// StoreKit 2
guard let product = try? await Product.products(for: [appleProductId]).first,
let decodedSignature = Data(base64Encoded: signature.signature) else {
completion(.failed)
return
}
let options: Set<Product.PurchaseOption> = [
.appAccountToken(token), // the token from the SDK, not the anonymous user id
.promotionalOffer(offerID: signature.identifier,
keyID: signature.keyIdentifier,
nonce: signature.nonce,
signature: decodedSignature,
timestamp: Int(signature.timestamp))
]
_ = try? await product.purchase(options: options)
// StoreKit 1: set payment.applicationUsername = token.uuidString.lowercased()
// Finally close the process with Purchasely
completion(.success)
}
}, failure: { error in
completion(.failed)
})
}Purchasely.interceptAction('purchase', async (info, payload) => {
if (payload?.kind !== 'purchase') {
return 'notHandled';
}
const storeProductId = payload.plan?.productId;
const storeOfferId = payload.offer?.storeOfferId;
if (storeProductId != null && storeOfferId != null) {
try {
// Pass `purchaseContextToken` to sign again for the same purchase.
// iOS only: the result is null on Android.
const signature = await Purchasely.signPromotionalOfferWithToken({
storeProductId,
storeOfferId,
});
if (signature != null) {
// Lowercase UUID. StoreKit 2: appAccountToken. StoreKit 1: applicationUsername.
const token = signature.purchaseContextToken;
// Launch your purchase with the signature and the token
}
} catch (e) {
console.error(e);
return 'failed';
}
}
// TODO launch your purchase flow
return 'success';
});await Purchasely.interceptAction(
PLYPresentationActionKind.purchase,
(info, payload) async {
if (payload is! PLYPurchasePayload) {
return PLYInterceptResult.notHandled;
}
final storeProductId = payload.plan.productId;
final storeOfferId = payload.offer?.storeOfferId;
if (storeProductId != null && storeOfferId != null) {
try {
// Pass `purchaseContextToken:` to sign again for the same purchase.
// iOS only: the result is an empty map on Android.
final signature = await Purchasely.signPromotionalOfferWithToken(
storeProductId, storeOfferId);
// Lowercase UUID. StoreKit 2: appAccountToken. StoreKit 1: applicationUsername.
final token = signature['purchaseContextToken'];
// Launch your purchase with the signature and the token
} catch (e) {
print(e);
return PLYInterceptResult.failed;
}
}
// TODO launch your purchase flow
return PLYInterceptResult.success;
},
);// Pass a token string instead of null to sign again for the same purchase.
// iOS only: on Android the success callback receives no signature.
Purchasely.signPromotionalOfferWithToken(
'store_product_id',
'store_offer_id',
null,
(signature) => {
// Lowercase UUID. StoreKit 2: appAccountToken. StoreKit 1: applicationUsername.
const token = signature.purchaseContextToken;
// Launch your purchase with the signature and the token
},
(error) => console.error(error)
);iOS - Sign the offer with the anonymous user id (deprecated)
Deprecated since SDK 6.2.0The methods
signPromotionalOffer(storeProductId:storeOfferId:success:failure:)andsignPromotionalOffer(plan:promoOffer:success:failure:)continue to work, but they are deprecated. Use the method with a purchase context token. These methods sign for the anonymous user id and do not attribute the purchase to the paywall. With these methods, do not put a purchase context token in the account field. On React Native, Flutter and Cordova,signPromotionalOfferis deprecated in the same way: usesignPromotionalOfferWithToken.
Here is a code sample to sign the offer on iOS with the deprecated method:
iOS applicationUserName or appAccountTokenOn iOS, you must use Purchasely anonymous user in lowercase as applicationUsername with StoreKit1 or appAccountToken with StoreKit2 if you use
Purchasely.signPromotionalOffer()method
Please look at sample code below for more details
// Intercept the tap on purchase to display the terms and condition
Purchasely.interceptAction(.purchase) { [weak self] info, params, completion in
// Grab the plan to purchase
guard let plan = params?.plan, let appleProductId = plan.appleProductId else {
completion(.success)
return
}
let offer = params?.promoOffer
// You can sign the offer with purchasely
Purchasely.signPromotionalOffer(storeProductId: appleProductId,
storeOfferId: offer?.storeOfferId) { signature in
// Success completion
} failure: { error in
// Failure completion
}
// Then Purchase with signature
// FULL EXAMPLE with code
// Using StoreKit1
self?.purchaseUsingStoreKit1(plan, offer)
// Using StoreKit2
self?.purchaseUsingStoreKit2(plan, offer)
// Finally close the process with Purchasely
completion(.success)
}
func purchaseUsingStoreKit1(_ plan: PLYPlan, _ offer: PLYPromoOffer) {
// 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, _ offer: PLYPromoOffer) {
if #available(iOS 15.0, *) {
Purchasely.signPromotionalOffer(storeProductId: plan.appleProductId,
storeOfferId: offer?.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.interceptAction<PLYPresentationAction.Purchase> { info, purchase ->
val sku = purchase.subscriptionOffer?.subscriptionId
val basePlanId = purchase.subscriptionOffer?.basePlanId
val offerId = purchase.subscriptionOffer?.offerId
val offerToken = purchase.subscriptionOffer?.offerToken
// TODO purchase with SKU and offer id
// Finally close the process with Purchasely
PLYInterceptResult.SUCCESS
}Purchasely.interceptAction('purchase', async (info, payload) => {
if (payload?.kind !== 'purchase') {
return 'notHandled';
}
// Retrieve the store product id and offer id
const storeProductId = payload.plan?.productId;
const storeOfferId = payload.offer?.storeOfferId;
// -- GOOGLE ONLY --
// Alternatively, just for Google you can retrieve everything if it is simpler for you,
// specially if you want the offer token
const productId = payload.subscriptionOffer?.subscriptionId;
const basePlanId = payload.subscriptionOffer?.basePlanId;
const offerId = payload.subscriptionOffer?.offerId;
const offerToken = payload.subscriptionOffer?.offerToken;
// -- END GOOGLE --
// -- APPLE ONLY --
if (storeOfferId != null) {
try {
// Deprecated since 6.2.0: use signPromotionalOfferWithToken
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
try {
// TODO launch purchase flow
// if successful, return 'success' to dismiss the paywall
return 'success';
} catch (e) {
// if not successful, return 'failed' to keep the paywall displayed
return 'failed';
}
});await Purchasely.interceptAction(
PLYPresentationActionKind.purchase,
(info, payload) async {
if (payload is! PLYPurchasePayload) {
return PLYInterceptResult.notHandled;
}
// Retrieve the store product id and offer id
String? storeProductId = payload.plan.productId;
String? storeOfferId = payload.offer?.storeOfferId;
// -- GOOGLE ONLY --
// Alternatively, just for Google you can retrieve everything if it is simpler for you,
// specially if you want the offer token
String? productId = payload.subscriptionOffer?.subscriptionId;
String? basePlanId = payload.subscriptionOffer?.basePlanId;
String? offerId = payload.subscriptionOffer?.offerId;
String? offerToken = payload.subscriptionOffer?.offerToken;
// -- END GOOGLE --
// -- APPLE ONLY --
if (storeProductId != null && storeOfferId != null) {
try {
// Deprecated since 6.2.0: use signPromotionalOfferWithToken
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
try {
// TODO launch purchase flow
// if successful, return success to dismiss the paywall
return PLYInterceptResult.success;
} catch (e) {
// if not successful, return failed to keep the paywall displayed
return PLYInterceptResult.failed;
}
},
);Updated 12 days ago

