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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
In-App Purchases: Non-Renewing Subscriptions Tutorial
30 mins
- When to Use Non-Renewing Subscriptions
- Implementing Non-Renewing Subscriptions: Overview
- Getting Started
- To Do List
- Parse Server & Heroku
- iTunes Connect
- Add In-App Purchase Items
- Sandbox Surprises
- Adding Your Subscriptions to the Product List
- Expiration Handling
- Parse Server Sync
- New Product ID Handling
- Progress Check!
- Providing Subscription Content
- Clean Up
- Where to Go from Here?
Sandbox Surprises
Ok, you have the goods, but something is still missing. A sandbox user!
Testing In-App Purchases requires at least one sandbox user. This lets you make purchases in the app without actually paying the cash. Add one now:
- In iTunes Connect, click Users and Roles
- Click Sandbox Testers
- Add a tester with an email address that is not used as an Apple ID.
You must always test In-App purchases on an actual device. Sorry Mr. Simulator, you’re out of job.
Open Settings on your iOS device and logout of iTunes & App Store. Do NOT login as your sandbox user email. Doing so will make it an Apple ID and the email will no longer be valid for sandbox testing.
Now you have the goods to sell and a sandbox tester, run the app again. You should now see the list of items you entered. But why are only the non-consumables showing? Good question. It is because the starter app doesn’t support the other types yet. You’ll be fixing that shortly.
You have to check out at least one of the owl images. So go ahead and make a purchase. Its on me!
When it prompts you to sign into iTunes Store, be sure to use a sandbox user.
Non-consumable items show up in the app and are ready for sale. Now you’re ready to begin implementing non-renewing subscriptions!
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 UIImage
s 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.
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.
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.
-
syncExpiration
syncs the latest expiration date withUserSettings
-
increaseRandomExpirationDate
increments the local expiration date by X months -
setRandomProduct
updates the variable & product array pertaining to the random product id - Parse remote server receives the new subscription value
- A notification rings throughout the app broadcasting the new purchase