Let's say a user has purchased a trial subscription in your app, then canceled the subscription and removed your app. After some time – it could be weeks or months – the user has reinstalled your app. In this case, you shouldn't offer them a trial. Instead, you should display a full subscription price.
Introductory offer works within the same subscription group. That means, that user can purchase regular weekly subscription without trial, then cancel subscription, and after some time purchase monthly subscription with trial.
Apple has a nice scheme, informing when introductory offer can be applied:
So, user is eligible for introductory offer when:
AND
To determine eligibility in iOS you should perform these 3 steps:
Let's take a closer look.
To validate receipt you should send HTTPS request to Apple with receiptData
and sharedSecret
parameters. In the example below, you should replace sharedSecret
with your own value. If you don't know where to get it, check this link.
func isEligibleForIntroductory(callback: @escaping (Bool) -> Void) { guard let receiptUrl = Bundle.main.appStoreReceiptURL else { callback(true) return } #if DEBUG let urlString = "https://sandbox.itunes.apple.com/verifyReceipt" #else let urlString = "https://buy.itunes.apple.com/verifyReceipt" #endif let receiptData = try? Data(contentsOf: receiptUrl).base64EncodedString() let sharedSecret = "YOUR_SHARED_SECRET" let requestData = ["receipt-data" : receiptData ?? "", "password" : sharedSecret, "exclude-old-transactions" : false] as [String : Any] var request = URLRequest(url: URL(string: urlString)!) request.httpMethod = "POST" request.setValue("Application/json", forHTTPHeaderField: "Content-Type") let httpBody = try? JSONSerialization.data(withJSONObject: requestData, options: []) request.httpBody = httpBody URLSession.shared.dataTask(with: request) { (data, response, error) in // continue here }.resume() }
In the example above, I used #if DEBUG
macros to determine whether subscription is sandbox
or production
. If you use other macroses, replace these lines of code.
After you get response from Apple, convert it to Dictionary
and get transactions array:
// paste this code after "continue here" comment guard let data = data, let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? \[String : AnyHashable\], let receipts\_array = json\["latest\_receipt\_info"\] as? \[\[String : AnyHashable\]\] else { callback(true) return } // continue here
Loop through the array and check is_trial_period
and is_in_intro_offer_period
values. If one of them is true
, that means that user has previously used introductory offer.
is_trial_period
and is_in_intro_offer_period
come as string in JSON, so to be more safe, we try to convert these values to Bool and to String.
// paste this code after "continue here" comment var latestExpiresDate = Date(timeIntervalSince1970: 0) let formatter = DateFormatter() for receipt in receipts_array { let used_trial : Bool = receipt["is_trial_period"] as? Bool ?? false || (receipt["is_trial_period"] as? NSString)?.boolValue ?? false let used_intro : Bool = receipt["is_in_intro_offer_period"] as? Bool ?? false || (receipt["is_in_intro_offer_period"] as? NSString)?.boolValue ?? false if used_trial || used_intro { callback(false) return } // continue here
To get current subscription status you should find the latest expires_date
in receipts array and compare with a current date. If subscription is not lapsed, then introductory offer is not available:
// paste this code after "continue here" comment formatter.dateFormat = "yyyy-MM-dd HH:mm:ss VV" if let expiresDateString = receipt["expires_date"] as? String, let date = formatter.date(from: expiresDateString) { if date > latestExpiresDate { latestExpiresDate = date } } } if latestExpiresDate > Date() { callback(false) } else { callback(true) }
Link to the full code can be found at the end of this article, now we will cover caveats in this method.
subscription_group_identifier
in each receipt
cancellation_date
field in a receipt
:if receipt["cancellation_date"] != nil { // if user made a refund, no need to check for eligibility callback(false) return }
pending_renewal_info
there will be a new field: grace_period_expires_date
. In this case you have to give access to premium content and shouldn't determine eligibility at all.To determine user eligibility for introductory offer in Apphud SDK just call:
Apphud.checkEligibilityForIntroductoryOffer(product: myProduct) { result in if result { // User is eligible to purchase introductory offer } }
You can also check eligibility for multiple products at one call:
func checkEligibilitiesForIntroductoryOffers(products: \[SKProduct\], callback: ApphudEligibilityCallback)
It's easier to determine eligibility for promotional offer: it's only available for current or lapsed subscribers. Checking pending_renewal_info
for existence should be enough.
In Apphud SDK just call:
Apphud.checkEligibilityForPromotionalOffer(product: myProduct) { result in if result { // User is eligible to purchase promotional offer } }
Or check eligibility for multiple products at one call:
func checkEligibilitiesForPromotionalOffers(products: \[SKProduct\], callback: ApphudEligibilityCallback)
The full method code can be found here.
In Apphud SDK determining introductory and promotional eligibility are already implemented using our server. Besides that, Apphud helps to track subscriptions, analyse key metrics, grow your revenue by reducing voluntary and involuntary churn, etc. Use Apphud now for free.