In-App Purchases: Non-Renewing Subscription Tutorial

Neil North

This post is also available in: Spanish

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 has over 10 years experience in software development, database design and indie game development.

You can find Neil on Twitter.

User Comments

14 Comments

  • Hi, could you please upload the finished project (link is not working)? Thanks!
    joedoe222
  • Hi Joe ... thanks for the letting us know. You should be able to download the the finished project now. Thanks!
    VegetarianZombie
  • Neil,

    Thanks for a great tutorial. I have one fundamental point of confusion. If I register a user (JohnSmith with password John2013), what's to prevent him from sharing his login and password with the world - and then no one else would have to pay for the subscription. I just tried it with an app that offers a non-renewing subscription and used the login and password on each of my iOS devices (with different Apple IDs) and didn't have to pay a second time for the subscription.

    Thanks!

    Sam
    sam.takoy
  • In the tutorial it says:
    "By requiring a user to create an account in order to subscribe,"
    however there doesn't seem to be anything in this tutorial about creating an account using Parse other than the line:

    [query getObjectInBackgroundWithId:[PFUser currentUser].objectId block:^(PFObject *object, NSError *error) {

    So, is creating an account an additional step that we need to do?
    mikew
  • Hi,
    Thanks for the great tutorial.
    I have just one question: if I've created two subscriptions (3 month and 6 month) is there a way to manage the case when the user wants to purchase the 6-month subscription but he has, for example, on month left from his previously purchased 3-month subscription? I suppose user expects to expand his subscription for six months after his actual expiration date ( 7 months remaining).

    Thanks.
    skizzato
  • Cannot get past first step to open AppDelegat.m. Where is it? Parse? Was I supposed to make anew app in Xcode?
    bobcubsfan
  • I want to know if I do Auto-Renewable Subscription The difference between the code Non-Renewing Subscription.

    Thank you
    Sarawut
  • I handle purchases on the back office by updating a DB. All works fine and good with consumable, but when I tested non renewing subscription I get to the message you also mentioned: "You have already subscribed..." that, even on confirmation, does not lead to:
    - (void) verifyReceiptOnComplete:(void (^)(void)) completionBlock onError:(void (^)(NSError*)) errorBlock
    where I trigger the back-office actions, but instead goes some strange way leading to sending notification: name:kSubscriptionsPurchasedNotification
    without knowing if the user would also be charged for nothing.
    How do I also route this kind of events the right way?

    Thanks, Fabrizio
    fbartolom
  • tutorials as always top notch :) but as someone stated already, what with matching apple id with user? how to avoid creating multiple user accounts and restoring transactions with only one apple id? Afaik, you cannot fetch iTunes credentials... :/
    Chwastek
  • Great tutorials, Ray. I actually ended up buying the apprentice and ios7 bundles today after spending a couple of days going through a couple of the tutorials on the site. Keep up the great work.

    I am banging my head against the wall on this particular issue. I have gotten to the point where I am building and running after adding the additional code to provideContentForProductIdentifier and I am getting a log message indicating "2014-02-07 01:13:58.124 In App Rage[4993:a0b] Failed to load list of products."

    I have no error messages and I have gone over every single piece of code up to this point and it all jibes. Can anyone please help with this? I would really appreciate it. Thanks in advance.
    C_Dub
  • Hi Ray! Thanks for this tutorial!

    In my app i'm thinking about Non-renewing subscription. But i'm not sure.

    My idea is that initially the user can access to the full content of the app for a limited time (counting from the first download).
    If the user likes the content then just pay once for non limit full access, if he doesn't pay, the access then is restricted.

    I think is interesting to show the full app before buying. Then user can choose if is usefull or not.

    Is there a way to know the first time (date) the app has been downloaded?I though in non-consumables and storing in NSUserDefaults, but there is a issue when user deletes and reinstalls the app, the counter returns to initial value.

    Thanks!
    Wilb
  • Hi Ray,

    great tutorial thanks Guy. Now-a-days you and StackOverflow are my main go-to resources.

    The reason for this comment :
    - the project as in 'Here is the completed sample project for this tutorial' above, looks like it is missing this line :

    NSString *const kSubscriptionExpirationDateKey = @"ExpirationDate";

    in IAPHelper.m

    Cheers,
    Guy
    GuyThackray
  • Is there a similar tutorial on auto-renewing subscriptions ? I just want to know how to determine if an auto-renewing subscription has been cancelled or not

    Thanks,
    Guy
    GuyThackray
  • Thank you for the tutorial. I am interested in setting up a non-renewing subscription for an initially low introductory price to build a user base. Then after adding some additional features and refining the app further I'd like to increase the subscsription price. Would you recommend going into the iTunes Connect site and setting up a new tier pricing for all future renewals to begin on a specific date? We would announce to the users well in advance of this of course. Is there a way to do this without using Apples pricing tier within the iTunes structure?
    trichards

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: Non-Gaming!

    Loading ... Loading ...

Last week's winner: Best iOS Animations in 2014. [Read Now]!

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 October: Xcode 6 Tips and Tricks!

Sign Up - October

Our Books

Our Team

Tutorial Team

  • Jean-Pierre Distler
  • Barbara Reichart
  • Corinne Krych

... 49 total!

Update Team

  • Ray Fix
  • Andy Pereira

... 15 total!

Editorial Team

... 22 total!

Code Team

  • Orta Therox

... 3 total!

Translation Team

  • Vitaliy Zarubin
  • Wilson Lin

... 32 total!

Subject Matter Experts

  • Richard Casey

... 4 total!