What’s new in Apphud: Refund Requests Win Back Solution, Web-to-App Match Quality, Integration Improvements, and SDK UpdatesLet’s see
Apphud
Why Apphud?PricingContact
Ren
Ren
October 15, 2019
5 min read

Determining eligibility for introductory offers in Swift

If your app uses auto-renewable subscriptions with introductory offer (trial, pay as you go or pay up front), then you should determine eligibility for such offer before showing price in your in-app purchase screen.

Determining eligibility for introductory offers in Swift

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:

When a user can apply introductory offerWhen a user can apply introductory offer

So, user is eligible for introductory offer when:

  • introductory offer hasn't been used in the past

AND

  • subscription has never been purchased yet or lapsed.

To determine eligibility in iOS you should perform these 3 steps:

  • Validate App Store receipt and get array of transactions. If there are no any, then introductory offer can be applied. If there are transactions, then perform next two steps.
  • Check, whether user has previously used introductory offer.
  • Check current subscription status.

Let's take a closer look.

1. Validating App Store receipt

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.

2. Checking whether introductory offer has been already used

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

3. Checking current subscription status

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.

Caveats using this method

  • We covered only case, when you have just one subscription group. If you have more subscription groups in your app, you should pass group identifier and compare it with subscription_group_identifier in each receipt
  • We didn't cover case with refunds. In a case of refund, you should check for 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
}
  •  We also didn't cover Billing Grace Period. If a user is currently in billing grace period, then in 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.

Determining eligibility using Apphud SDK

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)

Determining eligibility for promotional offer

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)

Conclusion

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.

Ren
Ren
Co-founder at Apphud
Ex iOS app and game developer. 11 years in the industry since iOS 3. More than 50 apps are in the background with 4 exits. Entrepreneur and traveler.

Related Posts