If you use introductory offers in your subscription application (trial, pay as you use or prepayment), then before you show the price on the payment screen, you need to determine the availability of the introductory offer to the user. If the user has already issued a trial, then for him you must display the full price.
Hi everyone, I ’m in touch Renat from Apphud - a service that simplifies the work with subscriptions in iOS-applications. Today I will tell you how to determine whether a single user has the right to activate an introductory sentence or not.
The introductory offer is valid within the same subscription group. This means that the user can issue a regular weekly subscription without a trial, unsubscribe and later issue a trial for a monthly subscription.
The Apple documentation has a diagram showing when an introductory sentence is available to the user:
It turns out that the user can use the introductory sentence if:
- he had not used an introductory sentence before
AND
- the subscription has either not yet been issued or has expired
To check the availability of the introductory sentence, you need to perform 3 steps:
1) Validate the App Store-check and pull out the array of transactions. If there are no transactions, then we do not check anything, an introductory sentence is available. If there are transactions, then perform the following two steps.
2) Check if the introductory sentence was previously used
3) Check current subscription status
Let's consider these steps in more detail.
1. Validation of the App Store check
To validate the check, you need to send a request to Apple, passing receiptData
and sharedSecret
. Replace the sharedSecret
value with your own. If you do not know your sharedSecret
, then it is described here where to get it.
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() }
The example above uses the#if DEBUG
macro to determine the type of subscription:sandbox
orproduction
. If you use other macros, then you will need to update the code in this place.
2. Check if an introductory sentence has been used before
After receiving a response from Apple, we translate it into Dictionary
and get an array of transactions:
// 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
We go through the array of transactions and look at the values of is_trial_period
and is_in_intro_offer_period
. If one of the values is true
, then the user has already drawn up an introductory sentence. These values come as a string, so for reliability we will try to convert the value in both Bool and 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
3. Checking the current status of the subscription
To find out the current status of the subscription, we need to find the latest expires_date
and compare with the current date. If the subscription has not expired, the 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) }
You can find a link to the full code of the method at the end of the article, however there is a lot of "But" in this method.
Underwater rocks
In this example, we examined only the case with one group of subscriptions. If you use more than one subscription group in the application, then you must pass the subscription group identifier to this method and verify it by the value of
subscription_group_identifier
inreceipt
.
In this example, the case of subscription refunds is not taken into account. To do this, check the presence of the
cancellation_date
field:
if receipt["cancellation_date"] != nil{ // if user made a refund, no need to check for eligibility callback(false) return }
And here the grace period (Billing Grace Period) is not taken into account. If the user is in the grace period at the time the check is validated, then the
pending_renewal_info
field will be ingrace_period_expires_date
. In this case, you, as a developer, are required to provide premium functionality to the user without displaying a payment screen. And accordingly, it makes no sense to check the availability of the introductory sentence.
There is a problem with checking the expiration date. The system time on the iOS device can be unscrewed and then our code will give an incorrect result: the subscription will be considered active.
Validation of a check on the device itself is not recommended by Apple. They spoke about this several times at WWDC (from 5:50) and this is indicated in the documentation . This is unsafe because an attacker can intercept data using a man-in-the-middle attack. Apple recommends using your server to validate checks.
Checking the availability of a promotional offer
The condition for the availability of the promotional offer is simpler - the main thing is that the user has an active or expired subscription. To do this, look for the presence of pending_renewal_info
for your subscription group.
How it is implemented in the Apphud SDK
It is enough to call one method, passing your product
into it, which will return the result to you:
Apphud.checkEligibilityForIntroductoryOffer(product: myProduct) { result in if result { // User is eligible to purchase introductory offer } }
And similarly for the promotional offer:
Apphud.checkEligibilityForPromotionalOffer(product: myProduct) { result in if result { // User is eligible to purchase promotional offer } }
There are also methods for checking availability for several products at once in one call:
func checkEligibilitiesForIntroductoryOffers(products: [SKProduct], callback: ApphudEligibilityCallback) func checkEligibilitiesForPromotionalOffers(products: [SKProduct], callback: ApphudEligibilityCallback)
Conclusion
The full method code can be downloaded here .
We at Apphud have already implemented a check on the availability of introductory and promotional offers in a convenient open-source SDK. Apphud also helps you track subscription status, analyze key metrics, automatically offer discounts to unsubscribed users, and much more. If you experience pain while working with subscriptions, try our solution for free.