第一印象觉得In-App Purchase(简称IAP)非常简单。Apple提供的大量文档应该让开发者很快熟悉地熟悉。那么,为什么在你的应用中集成IAP特性就如此令人生厌呢?
这是因为在开发过程中不可避免会出现一些错误。而但这些错误发生的时候,你就抓瞎了。虽然Apple提供了有关IAP的大量文档,但他们并未提及集成IAP的详细步骤。而且对StoreKit集成过程中出现的问题也没有一个核对清单。另外对于为什么诸如产品ID非法之类的问题也没有提供NSError之类的对象来告诉你原因。
在试用了各种可能的解决方案后,你只能身心疲惫,彷徨无助。
为了提高你的效率和减少你的痛苦,我觉定利用此文来介绍一下实现IAP的详细步骤。本文很详细,有点长。甚至可能太长了,但不像Apple的文档,它提供了为实现IAP的每一个步骤。
废话少说,我们直入主题吧。
IAP能正常工作的秘诀:分成两个步骤:
第一个步骤是你可能遇到问题的部分。一旦你在代码中成功地获取了产品描述,编写购买产品的代码不过是小菜一碟。
我们先看看步骤1。
下面是有关创建产品及提取其描述的非常粗略的步骤:
提取产品描述的代码非常简单,但其他步骤则很容易错。
注意: 为提取产品描述,你并不需要在iTunes Connect中创建IAP测试用户。
为支持IAP,你的App ID不能包括通配符(“*”)。为确定你的App Id是否包括通配符,请登录http://developer.apple.com/iphone,在 iPhone Developer Program Portal中选择左边菜单中的 “App IDs”检查你的 App ID。
下面是一个唯一的App ID:
7DW89RZKLY.com.runmonster.runmonsterfree
下面不是一个唯一的 App ID:
7DW89RZKLY.com.runmonster.*
如果你还没有一个唯一的App ID,按如下步骤创建一个:
在创建了新的App ID后,你需要生成一个指向这个App ID的新provisioning profile。
下面就是令人痛苦的生成和安装新provisioning profile的详细步骤:
在Xcode中安装了 profile 文件后,你需要对使用此provisiong profile的项目进行一些编辑工作:
如果你的程序已经发表到App Store了,那么可以略过此步骤。
在你将产品添加到 iTunes Connect之前,你必须添加此产品所需的程序。如果你的程序还没有100%完成也无需担心,你可以先提交具有部分数据的程序,最后再提交真实的程序。
注意: 只有 SKU 和 version(版本)部分是以后不可修改的
Apple的文档中没有任何地方提及详情,但它却是必须的步骤。要成功测IAP功能,你必须提交程序的二进制码。即使你的程序还没有100%完成,你仍然需要提交二进制码。然而,你也可以立即摈弃你的二进制码,使其不会进入审核阶段。
下面这些步骤非常关键,我可是因为少做了某些步骤而度过了一段非常痛苦的时间:
不用担心,由于程序的状态是“Developer Rejected”,Apple是不会对其进行审核的。你可以在任何时候提交程序的新版本并使其状态为“Developer Rejected”,这不会对以后程序正式提交的等待时间有任何影响。
完成了以上所有步骤后,我们最终可以向iTunes Connect中添加产品了。
下面我们开始编写代码对刚加入到iTunes Connect中的产品信息进行提取。我访问产品数据,我们需要使用 StoreKit framework。
注意: StoreKit 无法在模拟器上工作。你必须在真机上进行测试。
// InAppPurchaseManager.h #import <StoreKit/StoreKit.h> #define kInAppPurchaseManagerProductsFetchedNotification @"kInAppPurchaseManagerProductsFetchedNotification" @interface InAppPurchaseManager : NSObject <SKProductsRequestDelegate> { SKProduct *proUpgradeProduct; SKProductsRequest *productsRequest; }
注意: InAppPurchaseManager 是一个单例类,它处理程序中所有IAP任务。它是本文中的示例程序。
// InAppPurchaseManager.m - (void)requestProUpgradeProductData { NSSet *productIdentifiers = [NSSet setWithObject:@"com.runmonster.runmonsterfree.upgradetopro" ]; productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers]; productsRequest.delegate = self; [productsRequest start]; // we will release the request object in the delegate callback } #pragma mark - #pragma mark SKProductsRequestDelegate methods - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { NSArray *products = response.products; proUpgradeProduct = [products count] == 1 ? [[products firstObject] retain] : nil; if (proUpgradeProduct) { NSLog(@"Product title: %@" , proUpgradeProduct.localizedTitle); NSLog(@"Product description: %@" , proUpgradeProduct.localizedDescription); NSLog(@"Product price: %@" , proUpgradeProduct.price); NSLog(@"Product id: %@" , proUpgradeProduct.productIdentifier); } for (NSString *invalidProductId in response.invalidProductIdentifiers) { NSLog(@"Invalid product id: %@" , invalidProductId); } // finally release the reqest we alloc/init’ed in requestProUpgradeProductData [productsRequest release]; [[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerProductsFetchedNotification object:self userInfo:nil]; }
上面代码有几点需要注意:
// SKProduct+LocalizedPrice.h #import <Foundation/Foundation.h> #import <StoreKit/StoreKit.h> @interface SKProduct (LocalizedPrice) @property (nonatomic, readonly) NSString *localizedPrice; @end
// SKProduct+LocalizedPrice.m #import "SKProduct+LocalizedPrice.h" @implementation SKProduct (LocalizedPrice) - (NSString *)localizedPrice { NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init]; [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle]; [numberFormatter setLocale:self.priceLocale]; NSString *formattedString = [numberFormatter stringFromNumber:self.price]; [numberFormatter release]; return formattedString; } @end
加入上述代码,测试一下。你应该在控制台窗口中看见产品信息了。然而更大的可能是,你得到了一个无效的产品id。我下一篇文章将介绍怎样对这个问题进行调试。但是,下面的步骤8有可能是阻碍你前进的障碍。
遵循了上述所有步骤,但是你的产品仍然是无效的?你是否两次,三次,四次不懈努力地确认你是否遵循了上面提到的每个步骤?你是否已经对网上IAP信息少得可怜而感到绝望?
那么,你应该等待。
你的产品要进入iTunes Connect使得Apple准备好沙箱环境需要一些时间。对于我而言,我是经过了无数次产品无效错误的绝望。而在24小时后,我没有修改任何一行代码,但产品id变为有效。我认为要使产品发布到Apple的网络系统需要几个小时的时间,但如果你有时间的话,你可以像我一样等上24个小时。
至此你应该已经成功地获取了 SKProduct 描述。比较而言,支持购买产品相对简单些。仅需下面三个步骤:
我们从编写支持事务所需代码开始。
首先注意:你将负责开发产品购买的用户界面。StoreKit 未提供任何与用户界面相关的元素。如果你希望你的购买用户界面与App Store一样,那么你要自己完成。
下面所有代码都是有关事务处理的后台部分。这是一个单独的类只有一条简单的API以供外部类(比如view controller)调用进行购买。如果你找到将其集成到你程序的购买部分的方法,那么我推荐你使用类似方案。
首先,需要遵循 SKPaymentTransactionObserver 协议:
// InAppPurchaseManager.h // add a couple notifications sent out when the transaction completes #define kInAppPurchaseManagerTransactionFailedNotification @"kInAppPurchaseManagerTransactionFailedNotification" #define kInAppPurchaseManagerTransactionSucceededNotification @"kInAppPurchaseManagerTransactionSucceededNotification" … @interface InAppPurchaseManager : NSObject <SKProductsRequestDelegate, SKPaymentTransactionObserver> { … } // public methods - (void)loadStore; - (BOOL)canMakePurchases; - (void)purchaseProUpgrade; @end上面我们定义了两个新的notification,它们将作为购买事务的结果被发送。在上例中我们仍然使用与获取产品描述同一个InAppPurchaseManager类。
// InAppPurchaseManager.m #define kInAppPurchaseProUpgradeProductId @"com.runmonster.runmonsterfree.upgradetopro" … #pragma - #pragma Public methods // // call this method once on startup // - (void)loadStore { // restarts any purchases if they were interrupted last time the app was open [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; // get the product description (defined in early sections) [self requestProUpgradeProductData]; } // // call this before making a purchase // - (BOOL)canMakePurchases { return [SKPaymentQueue canMakePayments]; } // // kick off the upgrade transaction // - (void)purchaseProUpgrade { SKPayment *payment = [SKPayment paymentWithProductIdentifier:kInAppPurchaseProUpgradeProductId]; [[SKPaymentQueue defaultQueue] addPayment:payment]; } #pragma - #pragma Purchase helpers // // saves a record of the transaction by storing the receipt to disk // - (void)recordTransaction:(SKPaymentTransaction *)transaction { if ([transaction.payment.productIdentifier isEqualToString:kInAppPurchaseProUpgradeProductId]) { // save the transaction receipt to disk [[NSUserDefaults standardUserDefaults] setValue:transaction.transactionReceipt forKey:@"proUpgradeTransactionReceipt" ]; [[NSUserDefaults standardUserDefaults] synchronize]; } } // // enable pro features // - (void)provideContent:(NSString *)productId { if ([productId isEqualToString:kInAppPurchaseProUpgradeProductId]) { // enable the pro features [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"isProUpgradePurchased" ]; [[NSUserDefaults standardUserDefaults] synchronize]; } } // // removes the transaction from the queue and posts a notification with the transaction result // - (void)finishTransaction:(SKPaymentTransaction *)transaction wasSuccessful:(BOOL)wasSuccessful { // remove the transaction from the payment queue. [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:transaction, @"transaction" , nil]; if (wasSuccessful) { // send out a notification that we’ve finished the transaction [[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerTransactionSucceededNotification object:self userInfo:userInfo]; } else { // send out a notification for the failed transaction [[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerTransactionFailedNotification object:self userInfo:userInfo]; } } // // called when the transaction was successful // - (void)completeTransaction:(SKPaymentTransaction *)transaction { [self recordTransaction:transaction]; [self provideContent:transaction.payment.productIdentifier]; [self finishTransaction:transaction wasSuccessful:YES]; } // // called when a transaction has been restored and and successfully completed // - (void)restoreTransaction:(SKPaymentTransaction *)transaction { [self recordTransaction:transaction.originalTransaction]; [self provideContent:transaction.originalTransaction.payment.productIdentifier]; [self finishTransaction:transaction wasSuccessful:YES]; } // // called when a transaction has failed // - (void)failedTransaction:(SKPaymentTransaction *)transaction { if (transaction.error.code != SKErrorPaymentCancelled) { // error! [self finishTransaction:transaction wasSuccessful:NO]; } else { // this is fine, the user just cancelled, so don’t notify [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; } } #pragma mark - #pragma mark SKPaymentTransactionObserver methods // // called when the transaction status is updated // - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { for (SKPaymentTransaction *transaction in transactions) { switch (transaction.transactionState) { case SKPaymentTransactionStatePurchased: [self completeTransaction:transaction]; break; case SKPaymentTransactionStateFailed: [self failedTransaction:transaction]; break; case SKPaymentTransactionStateRestored: [self restoreTransaction:transaction]; break; default: break; } } }
要测试上面的新代码,你还需要编写调用 loadStore, canMakePurchases 以及 purchaseProUpgrade 方法的代码。
有关上述代码的详细解释,请参考官方 In App Purchase Programming Guide (IAP编程指南)
上述代码有几个部分是针对我的程序的。例如,在 provideContent:中,NSUserDefaults 中的@”isProUpgradePurchased” BOOL 字段被设定为 YES。程序的其他部分将检查此BOOL值以确定是否需要启动专业版功能。如果你正好也要实现免费升级专业版的功能,那么你可以使用同样的方法。
为测试上述代码,你需要在 iTunes Connect 中创建测试用户以对IAP功能进行测试。你可以使用测试帐号购买产品而不被Apple收取费用。
按以下步骤创建测试用户:
测试时你需要输入这些email地址和密码。
在进行程序购买功能测试前,你必须在你的设备中退出iTunes Store。遵循以下步骤:
现在,终于可以开始进行IAP功能的测试了。测试很简单:
如果你使用同一账户进行购买时,系统将提示你已经购买了此产品。按“Yes”就可以再次下载此产品。
实现IAP功能比想象的要复杂许多。我可是经过无数痛苦的经历才完成我的程序。希望能够帮助其他开发者减轻他们的痛苦。
原文见:In App Purchases: A Full Walkthrough