ios 内购 “不自动更新”类订阅 购买指南(In-App Purchases: Non-Renewing Subscription Tutorial)_you're currently subscribed through an in-app purc-程序员宅基地

技术标签: ios 内购 订阅  iOS  

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

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

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 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 <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 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.

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

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/logcabin/article/details/9815185

智能推荐

攻防世界_难度8_happy_puzzle_攻防世界困难模式攻略图文-程序员宅基地

文章浏览阅读645次。这个肯定是末尾的IDAT了,因为IDAT必须要满了才会开始一下个IDAT,这个明显就是末尾的IDAT了。,对应下面的create_head()代码。,对应下面的create_tail()代码。不要考虑爆破,我已经试了一下,太多情况了。题目来源:UNCTF。_攻防世界困难模式攻略图文

达梦数据库的导出(备份)、导入_达梦数据库导入导出-程序员宅基地

文章浏览阅读2.9k次,点赞3次,收藏10次。偶尔会用到,记录、分享。1. 数据库导出1.1 切换到dmdba用户su - dmdba1.2 进入达梦数据库安装路径的bin目录,执行导库操作  导出语句:./dexp cwy_init/[email protected]:5236 file=cwy_init.dmp log=cwy_init_exp.log 注释:   cwy_init/init_123..._达梦数据库导入导出

js引入kindeditor富文本编辑器的使用_kindeditor.js-程序员宅基地

文章浏览阅读1.9k次。1. 在官网上下载KindEditor文件,可以删掉不需要要到的jsp,asp,asp.net和php文件夹。接着把文件夹放到项目文件目录下。2. 修改html文件,在页面引入js文件:<script type="text/javascript" src="./kindeditor/kindeditor-all.js"></script><script type="text/javascript" src="./kindeditor/lang/zh-CN.js"_kindeditor.js

STM32学习过程记录11——基于STM32G431CBU6硬件SPI+DMA的高效WS2812B控制方法-程序员宅基地

文章浏览阅读2.3k次,点赞6次,收藏14次。SPI的详情简介不必赘述。假设我们通过SPI发送0xAA,我们的数据线就会变为10101010,通过修改不同的内容,即可修改SPI中0和1的持续时间。比如0xF0即为前半周期为高电平,后半周期为低电平的状态。在SPI的通信模式中,CPHA配置会影响该实验,下图展示了不同采样位置的SPI时序图[1]。CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在下降沿采样,并在上升沿移出CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在上升沿采样,并在下降沿移出。_stm32g431cbu6

计算机网络-数据链路层_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输-程序员宅基地

文章浏览阅读1.2k次,点赞2次,收藏8次。数据链路层习题自测问题1.数据链路(即逻辑链路)与链路(即物理链路)有何区别?“电路接通了”与”数据链路接通了”的区别何在?2.数据链路层中的链路控制包括哪些功能?试讨论数据链路层做成可靠的链路层有哪些优点和缺点。3.网络适配器的作用是什么?网络适配器工作在哪一层?4.数据链路层的三个基本问题(帧定界、透明传输和差错检测)为什么都必须加以解决?5.如果在数据链路层不进行帧定界,会发生什么问题?6.PPP协议的主要特点是什么?为什么PPP不使用帧的编号?PPP适用于什么情况?为什么PPP协议不_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输

软件测试工程师移民加拿大_无证移民,未受过软件工程师的教育(第1部分)-程序员宅基地

文章浏览阅读587次。软件测试工程师移民加拿大 无证移民,未受过软件工程师的教育(第1部分) (Undocumented Immigrant With No Education to Software Engineer(Part 1))Before I start, I want you to please bear with me on the way I write, I have very little gen...

随便推点

Thinkpad X250 secure boot failed 启动失败问题解决_安装完系统提示secureboot failure-程序员宅基地

文章浏览阅读304次。Thinkpad X250笔记本电脑,装的是FreeBSD,进入BIOS修改虚拟化配置(其后可能是误设置了安全开机),保存退出后系统无法启动,显示:secure boot failed ,把自己惊出一身冷汗,因为这台笔记本刚好还没开始做备份.....根据错误提示,到bios里面去找相关配置,在Security里面找到了Secure Boot选项,发现果然被设置为Enabled,将其修改为Disabled ,再开机,终于正常启动了。_安装完系统提示secureboot failure

C++如何做字符串分割(5种方法)_c++ 字符串分割-程序员宅基地

文章浏览阅读10w+次,点赞93次,收藏352次。1、用strtok函数进行字符串分割原型: char *strtok(char *str, const char *delim);功能:分解字符串为一组字符串。参数说明:str为要分解的字符串,delim为分隔符字符串。返回值:从str开头开始的一个个被分割的串。当没有被分割的串时则返回NULL。其它:strtok函数线程不安全,可以使用strtok_r替代。示例://借助strtok实现split#include <string.h>#include <stdio.h&_c++ 字符串分割

2013第四届蓝桥杯 C/C++本科A组 真题答案解析_2013年第四届c a组蓝桥杯省赛真题解答-程序员宅基地

文章浏览阅读2.3k次。1 .高斯日记 大数学家高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢?高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记_2013年第四届c a组蓝桥杯省赛真题解答

基于供需算法优化的核极限学习机(KELM)分类算法-程序员宅基地

文章浏览阅读851次,点赞17次,收藏22次。摘要:本文利用供需算法对核极限学习机(KELM)进行优化,并用于分类。

metasploitable2渗透测试_metasploitable2怎么进入-程序员宅基地

文章浏览阅读1.1k次。一、系统弱密码登录1、在kali上执行命令行telnet 192.168.26.1292、Login和password都输入msfadmin3、登录成功,进入系统4、测试如下:二、MySQL弱密码登录:1、在kali上执行mysql –h 192.168.26.129 –u root2、登录成功,进入MySQL系统3、测试效果:三、PostgreSQL弱密码登录1、在Kali上执行psql -h 192.168.26.129 –U post..._metasploitable2怎么进入

Python学习之路:从入门到精通的指南_python人工智能开发从入门到精通pdf-程序员宅基地

文章浏览阅读257次。本文将为初学者提供Python学习的详细指南,从Python的历史、基础语法和数据类型到面向对象编程、模块和库的使用。通过本文,您将能够掌握Python编程的核心概念,为今后的编程学习和实践打下坚实基础。_python人工智能开发从入门到精通pdf

推荐文章

热门文章

相关标签