In-App Purchases: Non-Renewing Subscriptions Tutorial

Learn to offer access to time-limited content to users and monetize your app using In-App Purchases in this Non-Renewing Subscriptions Tutorial. By Owen L Brown.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Adding Your Subscriptions to the Product List

Previously in the tutorial, you added subscriptions and a consumable to iTunes Connect. They provide the user with a choice of two subscription durations, three months or six months. A subscription needs to benefit the user in some way and that’s where the Random Owl product comes in. A subscribed user has the unlimited awesomeness to view random owl images throughout eternity!

Add the new product identifiers to the starter project now.
Open OwlProducts.swift and add the following code below the PurchaseNotification property:

static let randomProductID = "com.back40.InsomniOwl.RandomOwls"
static let productIDsConsumables: Set<ProductIdentifier> = [randomProductID]
static let productIDsNonRenewing: Set<ProductIdentifier> = ["com.back40.InsomniOwl.3monthsOfRandom", "com.back40.InsomniOwl.6monthsOfRandom"]

static let randomImages = [
    UIImage(named: "CarefreeOwl"),
    UIImage(named: "GoodJobOwl"),
    UIImage(named: "CouchOwl"),
    UIImage(named: "NightOwl"),
    UIImage(named: "LonelyOwl"),
    UIImage(named: "ShyOwl"),
    UIImage(named: "CryingOwl"),
    UIImage(named: "GoodNightOwl"),
    UIImage(named: "InLoveOwl")
  ]

The code above lists out each product id and groups them nicely based on the type of purchase. The array of UIImages lists out the random images the user can cycle through.

Note: Again, be sure to enter the product ids based on your website and app name to match the iTunes Connect entries but don’t change product name itself. Ex: com.yourwebsite.yourappname.3monthsOfRandom.

The IAPHelper needs to know about the new productIDs. Update the store initialization:

public static let store = IAPHelper(productIds: OwlProducts.productIDsConsumables
  .union(OwlProducts.productIDsNonConsumables)
  .union(OwlProducts.productIDsNonRenewing))

This combines all the purchasing types into one Set and passes it to the IAPHelper initializer.

Expiration Handling

The user’s local device needs to know the status of its subscription expiration. The starter project already has some variables setup for you to do this. Open UserSettings.swift and take a peek.

  • expirationDate: last known subscription date
  • randomRemaining: number of remaining random images user can view
  • lastRandomIndex: last random image index
  • increaseRandomExpirationDate: increments expiration date by months
  • increaseRandomRemaining: increases the number of random images user can view

Notice that UserDefaults is the local persistence for the variables. Something not obvious is later in the project you’ll also save each product id to UserDefaults with a Bool value indicating the purchased state.

As mentioned earlier, you need code to generate and handle the expiration date of a subscription purchase. This date needs to be saved locally and on the Parse Server so it is available to all the user’s devices. Open OwlProducts.swift and add the following to the bottom of the OwlProduct struct:

public static func setRandomProduct(with paidUp: Bool) {
  if paidUp {
    UserDefaults.standard.set(true, forKey: OwlProducts.randomProductID)
    store.purchasedProducts.insert(OwlProducts.randomProductID)
  } else {
    UserDefaults.standard.set(false, forKey: OwlProducts.randomProductID)
    store.purchasedProducts.remove(OwlProducts.randomProductID)
  }
}

This is the first of several helper functions you’ll need for subscription handling. The above code updates the local UserDefaults with a Bool depending on if the user paid up. It also updates the IAHelper.purchasedProducts array which keeps track of the paid status of all the product ids.

Next, add the following below setRandomProduct(with:):

public static func daysRemainingOnSubscription() -> Int {
  if let expiryDate = UserSettings.shared.expirationDate {
    return Calendar.current.dateComponents([.day], from: Date(), to: expiryDate).day!
  }
  return 0
}

This code returns the number of days remaining on the subscription. If the subscription has expired, it will return 0 or negative.

Next, add the following below daysRemainingOnSubscription():

public static func getExpiryDateString() -> String {
  let remaining = daysRemainingOnSubscription()
  if remaining > 0, let expiryDate = UserSettings.shared.expirationDate {
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "dd/MM/yyyy"
    return "Subscribed! \nExpires: \(dateFormatter.string(from: expiryDate)) (\(remaining) Days)"
  }
  return "Not Subscribed"
}

This function formats and returns the expiration date or Not Subscribed when expired.

Since there are two ways a user can purchase random owl images (by subscription or in batches of 5), you’ll need a convenient way to determine if a random owl product is viewable. Add the following code below getExpiryDateString():

public static func paidUp() -> Bool {
  var paidUp = false
  if OwlProducts.daysRemainingOnSubscription() > 0 {
    paidUp = true
  } else if UserSettings.shared.randomRemaining > 0 {
    paidUp = true
  }
  setRandomProduct(with: paidUp)
  return paidUp
}

This code checks to see if any days remain on the subscription. If not, it checks to see if the user purchased unused owl images. Once finished, it calls setRandomProduct(with:) and returns the paid up status.

Hang in there, Ole Bird!

Non-Renewing Subscriptions

Parse Server Sync

As mentioned before, the local and remote server subscription date need to be synchronized. This requires a trip to the Parse Server. Add the following code below paidUp() to handle this.

public static func syncExpiration(local: Date?, completion: @escaping (_ object: PFObject?) -> ()) {
  // Query Parse for expiration date.
    
  guard let user = PFUser.current(),
    let userID = user.objectId,
    user.isAuthenticated else {
      return
  }
    
  let query = PFQuery(className: "_User")
  query.getObjectInBackground(withId: userID) {
    object, error in
      
    // TODO: Find latest subscription date.

    completion(object)
  }
}

The code communicates with the Parse Server, retrieving the data for the currently logged in user. The functions goal is to synchronize the locally stored subscription date with the remote server. The first function parameter is the local date, the second parameter is a completion handler passing back the PFObject returned by Parse.

It’s necessary to compare the local and server date to determine which is more recent. This avoids a potential problem where a user has renewed their subscription on one device and then tries to renew it on a different device, before restoring any existing purchases.
Insert the following code inside the Parse background callback at the TODO: marker.

let parseExpiration = object?[expirationDateKey] as? Date
      
// Get to latest date between Parse and local.
var latestDate: Date?
if parseExpiration == nil {
  latestDate = local
} else if local == nil {
  latestDate = parseExpiration
} else if parseExpiration!.compare(local!) == .orderedDescending {
  latestDate = parseExpiration
} else {
  latestDate = local
}
      
if let latestDate = latestDate {
  // Update local
  UserSettings.shared.expirationDate = latestDate
        
  // See if subscription valid
  if latestDate.compare(Date()) == .orderedDescending {
    setRandomProduct(with: true)
  }
}

The first line grabs the value of the expirationDateKey from Parse. Next, it decides which date represents the latest subscription, local or remote. Next, the code updates the local UserSetting’s synced date. Finally, call setRandomProduct if the subscription is valid.

Now the foundations are in place, you’re able to write the subscription purchasing method. Add the following below syncExpiration(local:completion:):

private static func handleMonthlySubscription(months: Int) {
  // Update local and Parse with new subscription.
    
  syncExpiration(local: UserSettings.shared.expirationDate) {
    object in
      
    // Increase local
    UserSettings.shared.increaseRandomExpirationDate(by: months)
    setRandomProduct(with: true)
      
    // Update Parse with extended purchase
    object?[expirationDateKey] = UserSettings.shared.expirationDate
    object?.saveInBackground()
      
    NotificationCenter.default.post(name: NSNotification.Name(rawValue: PurchaseNotification),
                                    object: nil)
  }    
}

For each new subscription purchase, the expiration date increases by the months parameter. Time to talk about what is going on here.

  1. syncExpiration syncs the latest expiration date with UserSettings
  2. increaseRandomExpirationDate increments the local expiration date by X months
  3. setRandomProduct updates the variable & product array pertaining to the random product id
  4. Parse remote server receives the new subscription value
  5. A notification rings throughout the app broadcasting the new purchase
Owen L Brown

Contributors

Owen L Brown

Author

Darren Ferguson

Tech Editor

Essan Parto

Final Pass Editor

Andy Obusek

Team Lead

Over 300 content creators. Join our team.