ios 内购 “不自动更新”类订阅 购买指南(In-App Purchases: Non-Renewing Subscription Tutorial)

http://www.raywenderlich.com/36270/in-app-purchases-non-renewing-subscription-tutorial

This post is also available in: Spanish

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:

  • Introduction to In-App Purchases in iOS 6. You will be extending the In App Rage app from this tutorial. There’s always enough rage to go around!
  • In-App Purchases in iOS6 Tutorial: Consumables and Receipt Validation. It’s highly recommended to use receipt validation when implementing subscriptions, so this gives you a great platform upon which to build.
  • How to Easily Create a Web Backend for Your Apps with Parse. You’ll be using Parse as the backend service for storing your users’ subscription information.

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

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

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.

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 beenupdated 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:

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.

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:

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:

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.

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.

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 EnglishDisplay Name to 3 Months of Rage andDescription 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 EnglishDisplay Name to 6 Months of Rage andDescription 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 

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 kSubscriptionExpirationDateKeyconstant 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 thecomponents: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.

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.

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 aSubscribe button for all subscription items, or a Renew button if there’s already an active subscription.

Build and run.

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

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

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:

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:

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 daysRemainingOnSubscriptionmethod 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!

你可能感兴趣的:(iOS,ios,内购,订阅)