In-App Purchases: Non-Renewing Subscription Tutorial

Neil North
Non-renewing subscriptions makes it easy to provide periodically available content to your users!

Non-renewing subscriptions make it easy to provide your users with periodical content

There are three major types of In-App Purchases in iOS:

  1. Non-consumables: These are things the user buys once (and only once), and then has access to forever. Some examples would be an extra level pack, a permanent item, or some downloadable content.
  2. Consumables: These are things the user can buy multiple times. Often they are “used up” so the user can buy them again. Some examples would be currency in a free-to-play game, or consumable items like healing potions or extra lives.
  3. Subscriptions:: You can also provide access to content in your app on a time-limited basis – making the user purchase a subscription to continue to access your content. Some examples would be subscribing to an electronic magazine, or subscribing to unlock an extra feature in an app for a month.

Subscription models are definitely worth considering in your apps, because since users can send payments on a regular basis, it can have a higher chance of generating a sustainable revenue stream.

There are three types of subscriptions – auto-renewable subscriptions, free subscriptions, and non-renewing subscriptions. This tutorial will be focusing on the third, as it’s the most appropriate for non-Newsstand apps.

In this tutorial, you will be adding non-renewing subscriptions to an app called “In-App Rage”, an app that allows you to browse rage comics. You will also be using Parse as a back-end provider for the app.

Before beginning, you should be sure to complete, or have experience equivalent to:

If you’re ready to level-up your IAP mastery, read on!

When to Use Non-Renewing Subscriptions

It may seem obvious, but let’s discuss a bit more about the type of subscriptions in iOS.

Auto-renewable subscriptions

Auto-Rewnewable Subscriptions

Auto-Rewnewable Subscriptions

When a user signs up for an auto-renewable subscription, they continue to be charged until they manually cancel it. This is obviously great from a developer’s point of view, because it takes a lot more effort to cancel something than to just let it continue.

You might already be familiar with a class of apps that use auto-renewable subscriptions already: Newsstand.

Newsstand was first introduced in iOS 5, and allows content providers to easily distribute their newspapers and magazines. With it, Apple introduced the auto-renewable subscription model, which allows you to set a subscription duration and manage renewals automatically through the StoreKit framework.

However, Apple has placed some very strict rules around auto-renewable subscriptions, meaning their usage is (usually) exclusive to Newsstand apps.

So sadly, if you want to provide content or features for a limited duration, outside of Newsstand, then your only option is to use non-renewing subscriptions.

Non-renewing subscription

Non-Renewing Subscriptions

Non-Renewing Subscriptions

When a user signs up for a non-renewing subscription, they subscribe for a set period of time (1 month, 3 months, etc). When the time runs out, their access to the content ends – but to continue to access the content, they have to re-subscribe.

Obviously this is not as ideal from a developer’s point of view as it forces customers to have to continually make the decision to subscribe, but if you don’t have a magazine-style app it’s the best you can do at this point :]

Here are a couple of examples of when you might consider including a non-renewing subscription:

  • An Optional Feature. Maybe you have a killer feature in your app, that you want people to be able to subscribe to on an optional basis. For example, Instapaper (shown on the right) allows users to sign up for full-text search on their documents on a time-limited basis.
  • Periodic Content. Maybe your app delivers content periodically, such as extra levels or bonus playable characters in a game. You could allow the user to purchase a subscription to access this extra content.

Implementing Non-Renewing Subscriptions: Overview

All right, so you’ve decided you want to begin building your non-Newsstand subscription empire. What does this mean when it comes to the nitty-gritty of development?

Unlike auto-renewable subscriptions, where subscription durations and renewals are handled through the StoreKit framework, non-renewing subscriptions require you to do all the heavy lifting.

StoreKit Y U No

Here are some things to consider when implementing non-renewing subscriptions:

  • The subscription duration is not managed for you by StoreKit, so you’ll need a way of calculating the duration at the point of purchase.
  • As with consumable products, your users should be able to purchase items multiple times. Thus, you’ll need a way of determining if there’s time remaining on an existing subscription, and of including that time in any new duration, should a user choose to renew.
  • You’re also required to make the subscription available to any device owned by the user. There are generally two feasible options you can use to accommodate this requirement:

    iCloud. Since the user’s iCloud account is exclusive to them, but shared across their devices, this is a simple and effective option. However, if your app is cross-platform, or has an companion web app, this won’t be the best choice since iCloud is restricted to iOS devices.

    Backend as a service, or BaaS. By requiring a user to create an account in order to subscribe, you can store any necessary data, such as the subscription expiry date, against their account on the server. This method will allow you to share a subscription across all platforms, simply by requiring a user to log in.

In this tutorial, you’ll be using Parse as the backend to store this information, as it is very popular and easy to use. So let’s get started!

Getting Started

When you’re ready to begin, download the starter project here.

Note: Be sure not to use either of the sample projects from the previous in-app purchase tutorials. For one thing, they do not include the Parse integration found in the above starter project.

Second, be aware that if you attempt to compile the sample project from the In-App Purchases in iOS 6 Tutorial: Consumables and Receipt Validation tutorial, you may well notice deprecation warnings. This is because Apple deprecated the UDID (a device-specific unique identifier) as of iOS 5, but the receipt validation code the sample depends upon relies on the UDID.

The good news is the receipt validation code was originally supplied by Apple and has since been updated to remove the reliance on the UDID. The starter project provided for this tutorial makes use of Apple’s updated code.

There are a few things in the starter project that need updating before you can get to work implementing subscriptions.

First, you’ll need to set up an app in Parse for this tutorial. To do this, do the following:

  1. Head over to Parse.com and either log in or sign up.
  2. If you’re taken straight to the dashboard, hit the + Create New App button. Otherwise, you’ll be prompted to create a new app. Enter In App Rage as the app name.
  3. You’ll then be shown the Getting Started dialog box, and from here you can find the Application ID and Client Key. Record this for later.

Note: An alternate way to find your Application ID and Client Key is to select your app from the dashboard, choose Settings and then Application Keys, as shown below:

Updated Parse interface

Once you have the Application ID and Client Key, open AppDelegate.m and do the following:

  1. Locate the application:didFinishLaunchingWithOptions: method.
  2. Find the [Parse setApplicationId:@"AppID" clientKey:@"ClientID"]; line.
  3. Replace AppID with your Application ID and ClientID with your Client Key.

Now update ITC_CONTENT_PROVIDER_SHARED_SECRET in the VerificationController.h file to your own shared secret:

  1. Log onto iTunes Connect and click Manage Your Apps.
  2. If you followed our previous tutorial, choose the In App Rage app and click Manage In-App Purchases. Otherwise, just create a new entry for this app – follow the previous tutorial if you get stuck.
  3. Scroll down and click View or generate a shared secret. You will be able to view your existing shared secret here, or create a new one by clicking Generate.

Screen Shot 2013-06-02 at 4.38.39 PM

What’s a shared secret? It’s a piece of data known only to the parties involved in secure communication. In this case, in order to verify a receipt with the Apple servers, your app has to provide the shared secret so it can be verified as a trusted source.

Open In App Rage-Info.plist and update your bundle identifier to match the one you created in your previous In App Rage project (or whatever bundle identifier you set up for this app).

If you can’t remember what bundle ID you used, log onto the iOS Dev Center and click Certificates, Identifiers & Profiles. Click Identifiers, locate the In App Rage app and note the value in the ID column. This is your bundle identifier.

Your final task is to replace all occurrences of the product identifiers found within the app with the product identifiers that you created on iTunes Connect for this app.

Here’s a useful tip: use the Xcode search navigator tab to do a project-wide find and replace. You’ll have those identifiers replaced in no time at all:

global-find-replace-xcode

Build and run. When the app launches for the first time, you’ll be required to create a new account before the products list is displayed.

Follow the steps to create a new account. When you’re done, you should see something like this:

iOS Simulator Screen shot 29.03.2013 2.23.12 PM

You’re now ready to begin implementing non-renewing subscriptions!

Creating Non-Renewing Subscriptions

You’re going to provide the user with a choice of two subscription durations, three months or six months.

Log onto iTunes Connect and click Manage Your Apps. Choose the In App Rage app. Click Manage In-App Purchases followed by the Create New button.

Choose type of in-app purchase

Find the Non-Renewing Subscription section and click Select.

Non-renewing subscriptions are, in principle, very similar to consumable products. The options should feel instantly familiar if you completed the In-App Purchases in iOS 6 Tutorial: Consumables and Receipt Validation.

Setting up an In-App Purchase

Fill out the In-App Purchase form as follows:

  • Set Reference Name to 3monthlyrage
  • Set Product ID to com.[insert your bundle indentifier].inapprage.3monthlyrageface
  • Set Price Tier to Tier 2
  • Click Add Language. Set Language to UK English, Display Name to 3 Months of Rage and Description to Purchase 3 Months of Rage

Then click Done to save the IAP details. Repeat the process for the six-month subscription using the following details:

  • Set Reference Name to 6monthlyrage
  • Set Product ID to com.[insert your bundle indentifier].inapprage.6monthlyrageface
  • Set Price Tier to Tier 4
  • Click Add Language. Set Language to UK English, Display Name to 6 Months of Rage and Description to Purchase 6 Months of Rage

If you completed both previous IAP tutorials, you should now have a total of eight in-app purchases on your list:

Note: It is imperative that you specify the duration of any subscription-based IAP, and the most common way to do this is in the display name or description. There’s a good chance your app will be rejected if it fails to clearly state the duration of any subscription.

Adding Your Subscriptions to the Product List

The first thing you need to do is add the new product identifiers you’ve created to the set of existing product identifiers found in the starter project.

Open RageIAPHelper.m and add the two new identifiers to the productIdentifiers set:

+ (RageIAPHelper *)sharedInstance {
    static dispatch_once_t once = 0;
    static RageIAPHelper *sharedInstance = nil;
    dispatch_once(&once, ^{
        NSSet * productIdentifiers = [NSSet setWithObjects:
                                      @"com.youridentifier.inapprage.drummerrage",
                                      @"com.youridentifier.inapprage.itunesconnectrage",
                                      @"com.youridentifier.inapprage.nightlyrage",
                                      @"com.youridentifier.inapprage.studylikeaboss",
                                      @"com.youridentifier.inapprage.updogsadness",
                                      @"com.youridentifier.inapprage.randomragefaces",
                                      //The two new subscription identifiers you've just created
                                      @"com.youridentifier.inapprage.3monthlyrageface",
                                      @"com.youridentifier.inapprage.6monthlyrageface",
                                     nil];
        sharedInstance = [[self alloc] initWithProductIdentifiers:productIdentifiers];
	});
    return sharedInstance;
}

As mentioned earlier, you need a method that generates the expiration date of a subscription at the point of purchase. It makes sense to add this method to the existing IAPHelper class.

Open IAPHelper.h and add the following just beneath the existing UIKIT_EXTERN statement:

UIKIT_EXTERN NSString *const kSubscriptionExpirationDateKey;

Then add the following method declarations below the existing ones:

- (int)daysRemainingOnSubscription;
- (NSString *)getExpirationDateString;
- (NSDate *)getExpirationDateForMonths:(int)months;
- (void)purchaseSubscriptionWithMonths:(int)months;

Now open IAPHelper.m and add the following #import statement at the top of the file:

#import <Parse/Parse.h>

Just below the #import statements, add this constant, which you’ll need later:

NSString *const kSubscriptionExpirationDateKey = @"ExpirationDate";

Before it can generate an expiration date, the app needs to check if there’s an existing subscription, and if so, whether it has any time remaining. Add the following to the bottom of the IAPHelper.m file:

- (int)daysRemainingOnSubscription {
    //1
    NSDate *expirationDate = [[NSUserDefaults standardUserDefaults] 
                              objectForKey:kSubscriptionExpirationDateKey];
 
    //2
    NSTimeInterval timeInt = [expirationDate timeIntervalSinceDate:[NSDate date]];
 
    //3
    int days = timeInt / 60 / 60 / 24;
 
    //4
    if (days > 0) {
        return days;
    } else {
        return 0;
    }
}

Here’s what’s going on in the code above:

  1. You retrieve the local representation of the current subscription’s expiration date from [NSUserDefaults standardUserDefaults]. Note the use of the kSubscriptionExpirationDateKey constant you defined earlier.
  2. You determine the number of seconds between the expiration date retrieved in step 1 and the current date.
  3. You calculate the number of days by dividing the number of seconds obtained in step 2 first by 60 (seconds per minute), then by 60 again (minutes per hour) and finally by 24 (hours per day).
  4. If the number of days obtained in step 3 is greater than 0, you return days, otherwise you return 0. This method will also return 0 if an expiration date isn’t found in [NSUserDefaults standardUserDefaults].

Note: Note that using NSUserDefaults to store the expiration date for the subscription isn’t a very secure way to implement this. It is relatively easy for someone with a jailbroken device, or access to software such as Macroplant’s iExplorer, to trick the app into providing a subscription they haven’t actually purchased.

There are two ways to think about this kind of thing – either don’t worry about piracy (with the thinking that most users are honest and will go the easy route of just purchasing something on the store if they want it and it’s available, and you can’t stop determined attackers anyway), or that a little bit of anti-piracy goes a long way.

In the end it’s up to you and your app. This tutorial favors simplicity over security, and you can use this as a foundation upon which to build your own, more secure implementation if you so desire.

Now that you can determine the current expiration date, if there is one, you can move onto implementing the getExpirationDateForMonths: method. It accepts a single parameter, which represents the length of a subscription in months, and calculates the expiration date:

Still in IAPHelper.m, add this method:

- (NSDate *)getExpirationDateForMonths:(int)months {
 
    NSDate *originDate = nil;
 
    //1
    if ([self daysRemainingOnSubscription] > 0) {
        originDate = [[NSUserDefaults standardUserDefaults] 
                      objectForKey:kSubscriptionExpirationDateKey];
    } else {
        originDate = [NSDate date];
    }
 
    //2
    NSDateComponents *dateComp = [[NSDateComponents alloc] init];
    [dateComp setMonth:months];
    [dateComp setDay:1]; //add an extra day to subscription because we love our users
 
    return [[NSCalendar currentCalendar] dateByAddingComponents:dateComp 
                                                         toDate:originDate
                                                        options:0];
}

There are two fairly simple steps in this method:

  1. Using the daysRemainingOnSubscription method you just implemented, you check to see if there’s an existing expiration date. If a date does exist and it’s valid, you use it as the origin date; otherwise you use today’s date.
  2. You use NSDateComponents to add the length of the subscription to the origin date, and you return the freshly calculated date.

Note: NSDateComponents is a Foundation class that is extremely useful when working with dates. By setting any of the properties that represent units of time, it can calculate dates in the past or into the future.

Here, you created the components manually and applied them to an existing date, but by using the components:fromDate: method of NSCalendar, you can do the opposite and extract the date components from an existing date. This could be useful if you had to determine within what week of the year a date falls, for example.

NSCalendar also provides dateFromComponents:, a useful method that can generate a date in situations where you may not have all the necessary information, but enough for NSCalendar to recognize it as a date. NSDateComponents, and related classes, are incredibly useful tools to have in your armory.

While you don’t need it just yet, you’ll use getExpirationDateString to generate the user-facing expiration date, including the amount of time remaining on the subscription, or an alternative message if the user isn’t subscribed. Add the following:

- (NSString *)getExpirationDateString {
    if ([self daysRemainingOnSubscription] > 0) {
        NSDate *today = [[NSUserDefaults standardUserDefaults] objectForKey:kSubscriptionExpirationDateKey];
        NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
        [dateFormat setDateFormat:@"dd/MM/yyyy"];
        return [NSString stringWithFormat:@"Subscribed! \nExpires: %@ (%i Days)",[dateFormat stringFromDate:today],[self daysRemainingOnSubscription]];
    } else {
        return @"Not Subscribed";
    }
}

Using the daysRemainingOnSubscription method you implemented earlier, you determine whether or not there’s a currently active subscription. If there is, you return a string containing the expiration date; otherwise you return the string “Not Subscribed”.

Now that the foundations are in place, you’re able to write the subscription purchasing method. Add the following:

- (void)purchaseSubscriptionWithMonths:(int)months {
    //1
    PFQuery *query = [PFQuery queryWithClassName:@"_User"];
 
    [query getObjectInBackgroundWithId:[PFUser currentUser].objectId block:^(PFObject *object, NSError *error) {
        //2
        NSDate * serverDate = [[object objectForKey:kSubscriptionExpirationDateKey] lastObject];
        NSDate * localDate = [[NSUserDefaults standardUserDefaults] objectForKey:kSubscriptionExpirationDateKey];
 
        //3
        if ([serverDate compare:localDate] == NSOrderedDescending) {
            [[NSUserDefaults standardUserDefaults] setObject:serverDate forKey:kSubscriptionExpirationDateKey];
            [[NSUserDefaults standardUserDefaults] synchronize];
        }
 
        //4
        NSDate * expirationDate = [self getExpirationDateForMonths:months];
 
        //5
        [object addObject:expirationDate forKey:kSubscriptionExpirationDateKey];
        [object saveInBackground];
 
        [[NSUserDefaults standardUserDefaults] setObject:expirationDate forKey:kSubscriptionExpirationDateKey];
        [[NSUserDefaults standardUserDefaults] synchronize];
 
    	NSLog(@"Subscription Complete!");
    }];
}

Let’s break this down step-by-step:

  1. To begin with, you query Parse using the PFQuery class to retrieve any expiration dates it has stored for the current user. When the user logs in, the ObjectID for their account is stored locally and can be accessed via the [PFUser currentUser].objectId property.
  2. You store the expiration dates saved on Parse in an array. You’re simply interested in the last object of that array, since that’ll be the most recent subscription’s expiration date.
  3. Next, you compare the local date and the server date to determine which is more recent; if it’s the server date, the local date is updated to match. 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 any existing purchases are restored.
  4. You generate a new expiration date.
  5. You then save the new expiration date both locally and on Parse.

You now need to update the IAPHelper class to make sure it’s aware of which IAPs are subscriptions. Add the following code to the provideContentForProductIdentifier: method:

- (void)provideContentForProductIdentifier:(NSString *)productIdentifier {
 
    if ([productIdentifier isEqualToString:@"com.youridentifier.inapprage.randomragefaces"]) {
        int currentValue = [[NSUserDefaults standardUserDefaults] integerForKey:@"com.youridentifier.inapprage.randomragefaces"];
        currentValue += 5;
        [[NSUserDefaults standardUserDefaults] setInteger:currentValue forKey:@"com.youridentifier.inapprage.randomragefaces"];
        [[NSUserDefaults standardUserDefaults] synchronize];
    } 
    // Start of the new code you need to add
    else if ([productIdentifier hasSuffix:@"monthlyrageface"]) {
        if ([productIdentifier isEqualToString:@"com.youridentifier.inapprage.3monthlyrageface"]) {
            [self purchaseSubscriptionWithMonths:3];
        } else {
            [self purchaseSubscriptionWithMonths:6];
        }
    // End of new code
    } else {
        [_purchasedProductIdentifiers addObject:productIdentifier];
        [[NSUserDefaults standardUserDefaults] setBool:YES forKey:productIdentifier];
        [[NSUserDefaults standardUserDefaults] synchronize];
    }
 
    [[NSNotificationCenter defaultCenter] postNotificationName:IAPHelperProductPurchasedNotification object:productIdentifier userInfo:nil];
}

The method now recognizes any product identifier suffixed with monthlyrageface as a subscription. The entire product identifier is subsequently used to determine the duration of the subscription, and the purchase is then performed accordingly.

Build and run.

Screenshot01

You should now see the subscriptions in the list. But before you try to purchase your newly-implemented subscriptions, there’s some more work to do. The app doesn’t provide any content yet, and there’s no way to tell whether or not there’s an active subscription and if so, how long before it expires.

Providing Subscription Content

You want to query the IAPHelper class to make sure the user has a valid subscription before you provide any content. Open MasterViewController.m and modify prepareForSeque:sender: to look like the following:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([segue.identifier isEqualToString:@"showDetail"]) {
        DetailViewController *detailViewController = (DetailViewController *) segue.destinationViewController;
        SKProduct *product = (SKProduct *) _products[self.tableView.indexPathForSelectedRow.row];
 
        // this is the statement you need to modify
        if ([[RageIAPHelper sharedInstance] productPurchased:product.productIdentifier] ||
			[[RageIAPHelper sharedInstance] daysRemainingOnSubscription] > 0) {
 
            if ([product.productIdentifier isEqualToString:@"com.youridentifier.inapprage.drummerrage"]) {
                detailViewController.image = [UIImage imageNamed:@"drummer.png"];
            } else if ([product.productIdentifier isEqualToString:@"com.youridentifier.inapprage.itunesconnectrage"]) {
                detailViewController.image = [UIImage imageNamed:@"iphonerage.png"];
            } else if ([product.productIdentifier isEqualToString:@"com.youridentifier.inapprage.nightlyrage"]) {
                detailViewController.image = [UIImage imageNamed:@"01_night.png"];
            } else if ([product.productIdentifier isEqualToString:@"com.youridentifier.inapprage.studylikeaboss"]) {
                detailViewController.image = [UIImage imageNamed:@"study.jpg"];
            } else if ([product.productIdentifier isEqualToString:@"com.youridentifier.inapprage.updogsadness"]) {
                detailViewController.image = [UIImage imageNamed:@"updog.png"];
            }
 
        } else {
            detailViewController.image = nil;
            detailViewController.message = @"Purchase to see comic!";
        }
    }
}

You’ll see you’ve added an extra condition to the if statement, guaranteeing the content is provided if it’s been purchased or there’s a valid subscription.

While you’re in your MasterViewController.m file, update the productPurchased: method to the following:

- (void)productPurchased:(NSNotification *)notification {
 
    NSString * productIdentifier = notification.object;
    [_products enumerateObjectsUsingBlock:^(SKProduct * product, NSUInteger idx, BOOL *stop) {
        if ([product.productIdentifier isEqualToString:productIdentifier]) {
            if ([product.productIdentifier hasSuffix:@"monthlyrageface"]) {
                [self reload];
                [self.refreshControl beginRefreshing];
            } else {
                [self.tableView reloadRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:idx inSection:0]] withRowAnimation:UITableViewRowAnimationFade];
            }
            *stop = YES;
        }
    }];
}

The above loops through your list of product identifiers. If the product is a subscription, it refreshes the full table; if it’s just a single purchase, it refreshes only that one line. You could refresh the entire table each time, but this way is cleaner.

That’s all you need to do for the non-consumables, but what about the random rage faces? You certainly don’t want your users missing out on those!

Open RandomFaceViewController.m and add the following #import statement at the top:

#import "RageIAPHelper.h"

Now modify refresh as follows:

- (void)refresh {
    if ([[RageIAPHelper sharedInstance] daysRemainingOnSubscription] > 0) {
        self.label.text = [[RageIAPHelper sharedInstance] getExpirationDateString];
    } else {
        int currentValue = [[NSUserDefaults standardUserDefaults] integerForKey:@"com.youridentifier.inapprage.randomragefaces"];
        self.label.text = [NSString stringWithFormat:@"Times Remaining: %d", currentValue];
    }
}

The UILabel of RandomFaceViewController.m currently displays the number of random faces remaining. This will look a bit odd if a user is subscribed and therefore has unlimited random images. The modified code determines if there’s a valid subscription and, if so, uses getExpirationDateString to set the label text accordingly.

Hang on, have you missed or forgotten anything?

Of course! The app has to actually provide those unlimited random faces if the user has a valid subscription. Make the following modification to buttonTapped: to sort that out:

- (IBAction)buttonTapped:(id)sender {
 
    int currentValue = [[NSUserDefaults standardUserDefaults] integerForKey:@"com.youridentifier.inapprage.randomragefaces"];
 
    // the is the statement you need to modify
    if (currentValue <= 0 && [[RageIAPHelper sharedInstance] 
                                daysRemainingOnSubscription] < 1) return;
 
    currentValue--;
    [[NSUserDefaults standardUserDefaults] setInteger:currentValue forKey:@"com.youridentifier.inapprage.randomragefaces"];
    [self refresh];
 
    int randomIdx = (arc4random() % 4) + 1;
    NSString * randomName = [NSString stringWithFormat:@"random%d.png", randomIdx];
    self.imageView.image = [UIImage imageNamed:randomName];
}

Build and run.

Screenshot01

Although this takes care of providing the content, there’s still not a lot to see yet because the interface doesn’t inform the user if they’re already a subscriber. Let’s take care of that next.

Displaying Subscription Details

It should always be clear to a user what they’ve purchased. With a few modifications, you can achieve exactly that.

When the user purchases a non-consumable, the Buy button is changed to a checkmark. If the user purchases a subscription, the button remains a button, even though the content is now available. Make the following modifications to MasterViewController.m to fix this poor and confusing experience:

In the tableView:cellForRowAtIndexPath: method, modify the if statement:

if ((![product.productIdentifier isEqualToString:@"com.youridentifier.inapprage.randomragefaces"] &&
    [[RageIAPHelper sharedInstance] productPurchased:product.productIdentifier] &&
    ![product.productIdentifier hasSuffix:@"monthlyrageface"]) || 
    ([[RageIAPHelper sharedInstance] daysRemainingOnSubscription] > 0 && 
    ![product.productIdentifier hasSuffix:@"monthlyrageface"]))

The extra conditions make sure that subscription items always display a Subscribe or Renew button as appropriate. The new code also ensures that all other (non-subscription) items display a checkmark if the user has a valid subscription.

Now modify the else branch of the same if statement:

UIButton *buyButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
buyButton.tag = indexPath.row;
cell.accessoryType = UITableViewCellAccessoryNone;
if ([product.productIdentifier hasSuffix:@"monthlyrageface"]) {
    if ([PFUser currentUser].isAuthenticated) {
        buyButton.frame = CGRectMake(0, 0, 92, 37);
        buyButton.tag = indexPath.row;
        [buyButton addTarget:self action:@selector(buyButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
        cell.accessoryView = buyButton;
 
        if ([[RageIAPHelper sharedInstance] daysRemainingOnSubscription] > 0) {
            [buyButton setTitle:@"Renew" forState:UIControlStateNormal];
        } else {
            [buyButton setTitle:@"Subscribe" forState:UIControlStateNormal];
        }
    }
} else {
    buyButton.frame = CGRectMake(0, 0, 72, 37);
    [buyButton setTitle:@"Buy" forState:UIControlStateNormal];
    [buyButton addTarget:self action:@selector(buyButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
    cell.accessoryView = buyButton;
}

Originally, this block of code simply added a Buy button for all un-purchased items. Now it displays a Subscribe button for all subscription items, or a Renew button if there’s already an active subscription.

Build and run.

iOS Simulator Screen shot 29.03.2013 6.17.29 PM

Much better. Tap one of the Subscribe buttons and admire the fruits of your labor. The usual confirmation dialog should appear:

iOS Simulator Screen shot 29.03.2013 6.18.25 PM

Once you’re purchased a subscription, try tapping on a Renew button. You may not have seen this dialog before:

iOS Simulator Screen shot 29.03.2013 6.19.39 PM

Note: If you have two or more subscription options, like you do here, it’s imperative to be aware of how the App Store behaves. Purchasing works on a per product basis, meaning if you were to subscribe to a three-month subscription and then subsequently renew with a six-month subscription, you wouldn’t see the renewal dialog as you may expect; you’ve actually chosen a different product. While this isn’t overly important, it does feel a little clumsy, is definitely something to be aware of, and may confuse your users.

The product list should now behave correctly. If you purchase everything, the Subscribe buttons should all change to Renew and the Buy buttons should all change to checkmarks:

iOS Simulator Screen shot 29.03.2013 6.27.27 PM

There’s still something missing though – the subscription status isn’t clear. There’s no indication of how much time is remaining.

Open MasterViewController.m and directly below tableView:cellForRowAtIndexPath:, add the following two methods:

//1
- (UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
    if (section == 0) {
        UIView *headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, tableView.bounds.size.width, 60)];
        [headerView setBackgroundColor:[UIColor grayColor]];
        UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(10, 2, tableView.bounds.size.width - 20, 60)];
        label.text = [[RageIAPHelper sharedInstance] getExpirationDateString];
        label.textColor = [UIColor whiteColor];
        label.numberOfLines = 0;
        label.textAlignment = NSTextAlignmentCenter;
        label.backgroundColor = [UIColor clearColor];
        [headerView addSubview:label];
        return headerView;
    }
    return nil;
}
 
//2
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
    return 66.0f;
}

The addition of these two methods creates a custom header for the table section so that the subscription information can be displayed at the very top of the tableview.

  1. First you create a UIView to become the section header, add a UILabel subview and set its text to the expiration date string generated by the helper class.
  2. Then you return the height of the header view.

Build and run. You should now see the subscription information in the header of the tableview section:

iOS Simulator Screen shot 29.03.2013 6.37.19 PM

Non-renewing subscriptions makes providing periodically available content to your users easy!

Note that this current design only works well if there’s a single thing you’re subscribing to – if you have multiple types of subscriptions in your app you’ll probably want to do things differently.

Restoring a Subscription

As a final check to make sure you’ve implemented everything correctly, delete the build from your device or Simulator and rerun the application. Log in as the same user and tap Restore.

Whoops! This button should restore a user’s purchases in the event that they have the same app on multiple devices, or if they delete and reinstall the app as you have. But you didn’t get your subscriptions back. Since you handle non-renewing subscriptions differently from consumables and non-consumables, you need to enhance the method that fires when a user taps the button.

Open MasterViewController.m. Find the restoreTapped: method and add the following:

- (void)restoreTapped:(id)sender {
    [[RageIAPHelper sharedInstance] restoreCompletedTransactions];
 
    //1
    if ([PFUser currentUser].isAuthenticated) {
        PFQuery *query = [PFQuery queryWithClassName:@"_User"];
 
        [query getObjectInBackgroundWithId:[PFUser currentUser].objectId block:^(PFObject *object, NSError *error) {
 
            //2
            NSDate *serverDate = [[object objectForKey:kSubscriptionExpirationDateKey] lastObject];
 
            [[NSUserDefaults standardUserDefaults] setObject:serverDate forKey:kSubscriptionExpirationDateKey];
            [[NSUserDefaults standardUserDefaults] synchronize];
 
            [self.tableView reloadData];
 
            NSLog(@"Restore Complete!");
        }];
    }
}

There are just a couple of simple steps:

  1. You determine if the current user is authenticated; if so, you query Parse to retrieve any expiration dates stored on the server.
  2. You save the most recent expiration date found on Parse in the NSUserDefaults object. You don’t care at this point whether the expiration date is valid, since the daysRemainingOnSubscription method handles that accordingly.

Now tap the Restore button again and make sure that all your goods have returned.

Where to Go from Here?

Here is the completed sample project for this tutorial.

Congratulations! You’ve now implemented every non-Newsstand in-app purchase type in your In App Rage app. You’re prepared for whatever business model you plan to integrate into your apps.

As mentioned in previous projects, for many simple apps this approach is more than sufficient. But if you want to take things even further and learn how develop a robust and extensible server-based system, check out iOS 6 by Tutorials.

I hope you enjoyed this tutorial – and if you have any questions or comments, please join the forum discussion below!

Neil North

Neil is an iOS developer / consultant at Northy Software Solutions and Asset management technical officer / lead iOS developer at Shepherd Services Pty Ltd. Neil is also creator of Swift Coder and Swift Gamer where you can find Swift video training and frequent Swift newsletters.

You can find Neil on Twitter.

User Comments

19 Comments

[ 1 , 2 ]
  • I am unable to edit the storyboard file in the latest XCode 6. Anyone else having this issue?
    itjunkii
  • The Storyboard file crashes my XCode 6 - anyone else having the issue? Any chance we can get an updated zip?
    itjunkii
  • @itjunkii - i had this problem too, and this solved the problem - http://stackoverflow.com/a/25993977/4028490
    AkmStudio
  • Hi Neil, thanks for great tutorial,

    Im trying to use this code in my current project in iOS8. Ive got everything set up and running however this was after using a different version of the VerificationController.m as i was having a lot of problem with this an errors.

    Once my app is working, i navigate to the list view which loads my subscriptions, but then the app crashes on line 81

    [verifier verifyPurchase:transaction completionHandler:^(BOOL success) {

    in IAPHelper.m,

    this is my debug output, any ideas how to fix, or get the code working on iOS8?

    *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[VerificationController verifyPurchase:completionHandler:]: unrecognized selector sent to instance 0x1700131f0'
    *** First throw call stack:
    (0x186202084 0x1967e80e4 0x186209094 0x186205e48 0x18610b08c 0x1000532d4 0x1000547dc 0x100054678 0x18a8e4e04 0x1860e506c 0x18a8e4d78 0x18a8e5960 0x18a8e6230 0x18a8e47bc 0x100418f20 0x100418ee0 0x10041d84c 0x1861b98dc 0x1861b7984 0x1860e5664 0x18f2275a4 0x18a9ea4f8 0x10005af64 0x196e56a08)
    libc++abi.dylib: terminating with uncaught exception of type NSException
    (lldb)

    thanks
    AkmStudio
[ 1 , 2 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Vote for Our Next Tutorial!

Every week, we alternate between Gaming and Non-Gaming tutorial votes. This week: Gaming!

    Loading ... Loading ...

Last week's winner: Apple TestFlight Tutorial.

Suggest a Tutorial - Past Results

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in January: WatchKit.

Sign Up - January

Our Books

Our Team

Tutorial Team

  • Pietro Rea

... 60 total!

Update Team

... 12 total!

Editorial Team

... 17 total!

Code Team

  • Orta Therox

... 3 total!

Subject Matter Experts

  • Richard Casey

... 4 total!