iap 详细

附:本文来自IOS6-Tutorias的翻译,本做笔记之用,故语言简练。 一,可用的IAP类型: Non-Consumable:用户只需购买一次,不需要再次购买,即可在多台设备上拥有之(restore技术)。 Consumable:用户可以购买多次(不限定次数)。例如金币。 Auto-Renewable Subscriptions:为了收到app更新的内容,用户需要定期支付款项。(目前仅适用于杂志或者新闻类型的app)。 Free Subscriptions:类似于Auto-Renewable Subscriptions类型,但免费,仅适用于杂志类型的app。 Non-Renewing Subscriptions:假如你可用Auto-Renewable(你的app不属于杂志或者新闻类型),但是你仍想提供基于时间限制的访问内容,可以选择Non-Renewing来试试。比如你想仅允许用户一周的时间去访问某个特殊的功能,逾期则否。 二,在apple上注册IAP: 在你的app中提供iap产品之前,你需要让apple知道这个产品,即需要将其注册在iTunesConnect上。 流程很简单,稍后你将亲自实践。目前,你需要了解iTunesConnect上的信息:  上图展示了你预填写的product ID,该字串可以认为是一个IAP产品的唯一标示,其价格亦然。 上图展示了你的IAP产品的展示信息(本地化名称和描述)。 现在你已经完成了IAP的注册。 三,在app中实现IAP: 总共有7个步骤来实现IAP。 1,加载产品identifiers:在开始购买之前,app需要知道你在iTunesConnect中注册的IAP产品的produnct identifiers。(该列表可以硬编码到你的app中,也可以从本地服务器那获取)。 NSSet* productIdentifiers = [NSSetsetWithObjects:@"com.razeware.hangman.tenhints",@"com.razeware.hangman.hundredhints",nil]; 2,请求Product 信息:接下来,app将链接AppStore获取产品详细信息。在delegate callback中获取到产品信息,并存贮在SKProduct对象中。 _productsRequest= [[SKProductsRequestalloc]initWithProductIdentifiers:productIdentifiers]; _productsRequest.delegate= self;[_productsRequeststart]; 3,呈现产品(Present the store):接下来在app中呈现可用的products列表。apple没有提供现成的VC去展示lists,你需要自己创建,因为此,可以做出差异化的购买界面。 例如下图: 4,触发购买请求:当用户选择一个IAP产品条目时,购买请求API将被触发,之后会自动弹出信息,形如‘你确定购买**产品吗?’ SKPayment* payment = [SKPaymentpaymentWithProduct:skProduct]; [[SKPaymentQueuedefaultQueue]addPayment:payment]; 5,交易进程:IAP产品API将要求用户支付对应费用,并监听交易成功与否的信息。此时,你可以选择通过向apple servers验证该购买是否有效。 (在app新启动时,也将会注册接受交易成功与否的通知。) - (void)paymentQueue:(SKPaymentQueue*)queueupdatedTransactions:(NSArray*)transactions { for(SKPaymentTransaction* transaction intransactions) { switch(transaction.transactionState){ caseSKPaymentTransactionStatePurchased: [selfcompleteTransaction:transaction]; break; caseSKPaymentTransactionStateFailed: [selffailedTransaction:transaction]; break; caseSKPaymentTransactionStateRestored: [selfrestoreTransaction:transaction]; break; default: break; } } } 6,开放购买内容(unlock the content):这是很关键的步骤,你的app此时应当将购买的内容呈现给用户(即某项功能对用户可用)。 7,接受本次交易:最后一步,请求IAP的API,告知本次交易接受。 否则app将认为该交易没有结束,并在下次启动app时,再次提交该交易请求。 [[SKPaymentQueuedefaultQueue]finishTransaction: transaction]; ------------------------------------ 实例: 接下来,将开始一个简单的游戏项目,用来展示IAP购买。 找到本章资源,解压HangmanCh9Starter项目,打开并运行之。 你可能曾经玩过这个游戏。你的目标是猜测屏幕底部的单词,点击label弹出键盘,并键入你认为对的字母。 加入你是对的,字母将会显示,反之你的hangman将会被吊起来(没玩过)。假如错误达到一定次数,hangman即死亡。很怪异的游戏,不是吗? 试着能不能搞定这个游戏,假如你思维卡住,你可以点击hint按钮来获取一个字母的提示,但是你只有20次的机会。 运行下该app的各个地方,你会看到有个setting界面,你可以在里面设置app。 在右上角有个‘store’按钮,点击进去,里面是空的,点击restore按钮,将呈现一个新的界面即‘store details’界面。这便是接下来的主要任务。 玩成了本任务的项目,用户将会获取更多的单词库,更多的提示来避免hangman死翘翘。(当然对你来讲,你知道最真实的原因:mooooney)。 添加IAP到hangmanapp中:1,允许用户购买额外的hint。2,解锁更多的单词库。 现有代码初探: 略。 HMStoreListViewController.m和HMStoreDetailViewController.m目前几乎是空的,这是本章节要完善的内容。 设计思考: 在写IAP相关代码前还有一件事需要讨论。 有2件事已经使IAP简单化: 在这部分,我们讲讨论下述2点使IAP简单的原因,或许你会发现一些有用的技术可以借鉴。 1,主题和单词库已经被设计为基于文件化的。 如果你打算购买一个新的theme,只需要建一个StickmanTheme实例,返回相应的图片和声音元素即可。不需要写新的代码。 代码越多,bug出现几率越大。 你无法在IAP购买时下载代码。所以增加新内容时你不得不发布新的版本。 2,每个主题和单词库以其私有字典被保存。 假如你清楚的知道主题和单词库被存放在具体的私有字典中,这将使效果实现更容易。 比如,你想要更改到某个theme,你只要代入字典的URL,即可获取相应的theme。 这也使得增加主题成为可能,写入文件即可。 注:此时文件是以文件夹的形式存贮在项目中。 作用:当文件被赋值到app的bundle中时,他们将原封不动的赋值文件下的路径和文件。 开工: 添加IAP产品到项目(theme,words,hints): 前提: 拥有ios developer Program账号; 确定你已同意iTunes Connect中最新的IOS Developer Program Lisence Agreement; 确定你已经完成在iTunes Connect中ios Paid Applications Contract。 1,登陆IOS Provisioning Portal,点击‘App IDs‘选项,如下图示: 点击’New App ID‘,将出现新建app ID的界面: bundle identifier基于你管理的域名(或者以项目名称即可),例如:com.mypro.pro。 注意:App ID不能包含通配符,IAP仅支持明确的App IDs。 登陆iTunes Connect ,点击’Manage Your Applications‘,->’Add New App‘,并选择IOS App类型,创建一个基于上述App ID 的app。 注意:此时你的app name应当和我的不一样,app name是唯一的,并且我已经创建了该app。 点击继续,接下来的2页将会要求你填写关于该app的详细信息,现在可以暂时以占位字母替代,因为你这些信息稍后是可以修改的。为了创建的app可用,你不得不填写所有的信息,包括icon和screenshot(图片大小有严格的要求)。 完成app创建,图示: 点击右上角的’Manage In-App Purchase‘,然后点击’Create New‘,将出现创建IAP产品的界面: 选择IAP 的类型,此处的hint购买,不限制用户购买的次数的,故用’Consumable‘类型。 现在拟提供2种hint产品,一个提供10hints,一个100hints。 10个hints创建: 注:对于Product ID,你应用你自己的反转DNS标示法,比如:com.mypro.pro.tenhins 在页面底部,显示In-App Purchase Detaile\lauguage部分,点击’Add Lunguage‘,填写信息,并点击保存之。滚到页面底部,再次保存页面。一个10hints的IAP产品注册成功。 100hints 的IAP产品创建: 类似10hits的,此处翻译略。 你已经完成了在iTunesConnect上的工作,结束进程前,你需要确定的是项目的bundle Identifier是正确的。 (折回项目中,点击info-plist文件,找到’Bundle Identifier‘,设置与上述对应的AppId值: 完成修改之后,为了避免Xcode使用旧的bundle Id,可以操作如下: 1,点击Product\Clean in Xcode。 2,删除在device或者在simulator中的app。 3,重启Xode,simulator和device。 4,运行之。 最后一项,调用IAP的API,你需要在项目中添加 StoreKit framework。 基于上述操作,已经完成在iTunes Connect中注册IAP产品;项目可用IAP。 接下来,将是代码的东东了。 ------------------------ 获取Products: 加载产品product identifiers并请求product 信息。 讲所有这部分代码放在一个helper的类中,方便IAP代码的集中化管理。 创建新的Group,命名为’IAP‘。 在IAP文件夹下创建NSObject子类文件,命名为“IAPHelper”。 在IAPHelper.h中: @interfaceIAPHelper : NSObject - (void)requestProductsWithProductIdentifiers: (NSSet*)productIdentifiers;@end 上述书写的方法,将用于向appstore请求IAP产品信息(produce identifires集合)。 在IAPHelper.m中: // 1 #import"IAPHelper.h" #import // 2 @interfaceIAPHelper() @end @implementationIAPHelper { SKProductsRequest* _productsRequest;// 3 } - (void)requestProductsWithProductIdentifiers: (NSSet*)productIdentifiers { // 4 _productsRequest= [[SKProductsRequestalloc]initWithProductIdentifiers:productIdentifiers]; _productsRequest.delegate= self; [_productsRequeststart]; } @end 释义: 1,导入storekit头文件,用于调用IAP的APIs。 2,为了获取products列表,此处实现了改协议。 3,创建request实例,用以请求产品列表。a,保存对request的引用。b,判断其是否运行。 4,通过IAP ids产品集合,想appstore请求其对应产品的详细信息,并设定委托,来判断请求的成功与否,并获取成功的信息。 在IAPHelper.m @end之前,写入委托方法: #pragma mark - SKProductsRequestDelegate - (void)productsRequest:(SKProductsRequest*)requestdidReceiveResponse:(SKProductsResponse*)response { NSLog(@"Loaded list of products..."); _productsRequest= nil; if(!response.products || response.products.count ==0) { return; } NSArray* skProducts = response.products; for(SKProduct* skProduct inskProducts) { NSLog(@"Found product: %@ %@ %0.2f",skProduct.productIdentifier,skProduct.localizedTitle,skProduct.price.floatValue); } } - (void)request:(SKRequest*)requestdidFailWithError:(NSError*)error { NSLog(@"Failed to load list of products."); _productsRequest= nil; } 2个代理方法成功和失败的回调。 成功:打印出返回的products信息;设置requst==nil。 失败:设置request==nil。 创建HMIAPHelper。这是因为IAPHelper是完全独立于该项目的,之后在其他的项目可以重用之。有关该项目的代码(比如本项目的IAP产品列表),将添加在特定项目文件中(HMIAPHelper)。 创建同上IAPHelper的方法。 在HMIAPHelper.h中: #import"IAPHelper.h" @interfaceHMIAPHelper : IAPHelper + (HMIAPHelper*)sharedInstance; - (void)requestProducts; @end 静态方法:返回一个本类的单例。 实例方法:请求产品列表。(无参,将把IAP列表硬编码在里面) 在HMIAPHelper.m中 #import"HMIAPHelper.h" @implementationHMIAPHelper + (HMIAPHelper*)sharedInstance { staticdispatch_once_tonce; staticHMIAPHelper * sharedInstance;dispatch_once(&once, ^{ sharedInstance = [[selfalloc]init];}); returnsharedInstance;} - (void)requestProducts { NSSet* productIdentifiers = [NSSetsetWithObjects: @"com.razeware.hangman.tenhints", @"com.razeware.hangman.hundredhints",nil]; return[superrequestProductsWithProductIdentifiers: productIdentifiers]; } @end 在requestProducts中,产品列表是硬编码其中的。 注意将列表内容换成实际创建的product identifiers。 切换到HMStoreListViewController中, #import"HMIAPHelper.h" 在viewDidLoad中: [[HMIAPHelpersharedInstance]requestProducts]; 注意: 当在iTunesConnect中创建了IAP产品,将会延迟一段时间才能响应返回产品信息。一般是延迟1~30分钟。 有时可能会收到’Cannot connect to iTunes Store‘。这意味着网络有问题,或者iTunes sandbox是关闭的。 可以检查下面的URl,如果无回应,则表明是关闭的。 https://sandbox.itunes.apple.com/verifyReceipt 更多bug检测,请查看: http://www.raywenderlich.com/forums//viewtopic.php?f=2&t=188 展示products: 目前已请求到产品列表,现在需要将产品展示出来。 从appStore返回的列表信息展示给用户,但是除了SKProduct类提供的,你还需要更多的产品信息。 为了信息的清晰化,你需要创建一个类,用以包含产品的信息,即IAPProduct(基于NSObject) 在IAPProduct.h中 @classSKProduct; @interfaceIAPProduct : NSObject - (id)initWithProductIdentifier:(NSString*)productIdentifier; - (BOOL)allowedToPurchase; @property(nonatomic,assign)BOOLavailableForPurchase; @property(nonatomic,strong)NSString* productIdentifier; @property(nonatomic,strong)SKProduct* skProduct; @end 你可能认为获取到的列表都是可用的,但事实应该通过AppStore检测它们是否可用,并且展示出可用的列表。 1,你可能将一个产品刚放加到iTunesConnect上,但它还未同步。 2,在app中硬编码的某个产品,可能未在iTunesConnect上创建。 打开IAPProduct.m: #import"IAPProduct.h" @implementationIAPProduct - (id)initWithProductIdentifier:(NSString*)productIdentifier { if((self= [superinit])) { self.availableForPurchase =NO; self.productIdentifier = productIdentifier; self.skProduct =nil; } return self;} - (BOOL)allowedToPurchase { if(!self.availableForPurchase) return NO; return YES; } @end 注意:关于allowedToPurchase方法,当产品可以购买时,返回YES,但是过会你将折回到这里,在产品是其它状态时更改以阻止用户购买。 折回到IAPHelper和HMIAPHelper,应用IAPProduct新类并返回该类组成的list 更改IAPHelper.h如下: typedef void(^RequestProductsCompletionHandler)(BOOLsuccess, NSArray* products); @interfaceIAPHelper : NSObject @property(nonatomic,strong)NSMutableDictionary* products; - (id)initWithProducts:(NSMutableDictionary*)products; - (void)requestProductsWithCompletionHandler: (RequestProductsCompletionHandler)completionHandler; @end 2处不同点: 1,初始化方法的参数变为一个字典:key:product identifier value:一个IAPProduct实例。 即该字典含有可用的产品列表。稍后你将不得不打开iTunesConnect。 2,requestProducts方法参数是一个block。 在IAPHelper.m中,导入IAPProduct头文件: #import"IAPProduct.h" 添加2个新的变量 @implementationIAPHelper { SKProductsRequest* _productsRequest; RequestProductsCompletionHandler_completionHandler; } - (id)initWithProducts:(NSMutableDictionary*)products {if((self= [superinit])) { _products= products; } return self; } - (void)requestProductsWithCompletionHandler:(RequestProductsCompletionHandler)completionHandler { // 1 _completionHandler= [completionHandlercopy]; // 2 NSMutableSet* productIdentifiers = [NSMutableSetsetWithCapacity:_products.count]; for(IAPProduct* product in_products.allValues) { product.availableForPurchase= NO; [productIdentifiers addObject:product.productIdentifier]; } // 3 _productsRequest= [[SKProductsRequestalloc]initWithProductIdentifiers:productIdentifiers]; _productsRequest.delegate= self; [_productsRequeststart]; } 1,在将block传给实例变量前,需要copy: This isimportant because if the block that is passed in is on the stack, it won’t beavailable when you need it unless you copy it first as shown here. delegate: - (void)productsRequest:(SKProductsRequest*)requestdidReceiveResponse:(SKProductsResponse*)response { NSLog(@"Loaded list of products..."); _productsRequest=nil; // 1 NSArray* skProducts = response.products; for(SKProduct* skProductinskProducts) { IAPProduct* product =_products[skProduct.productIdentifier]; product.skProduct= skProduct; product.availableForPurchase=YES; } // 2 for(NSString* invalidProductIdentifierin response.invalidProductIdentifiers) { IAPProduct* product =_products[invalidProductIdentifier]; product.availableForPurchase=NO; NSLog(@"Invalid product identifier, removing: %@", invalidProductIdentifier); } // 3 NSMutableArray* availableProducts = [NSMutableArrayarray]; for(IAPProduct* productin_products.allValues) { if(product.availableForPurchase) { [availableProductsaddObject:product]; } } _completionHandler(YES, availableProducts); _completionHandler=nil; } - (void)request:(SKRequest*)request didFailWithError:(NSError*)error { NSLog(@"Failed to load list of products."); _productsRequest=nil; // 4 _completionHandler(FALSE,nil); _completionHandler = nil; } 1,该循环得到的SKProducts,是可用的,并在IAPProducts中找到对应的,并设置其可用。 2,除了返回可用列表,也返回不可用列表,可以用invalidProductIdentifiers获取。并设置对应的IAPProduct为不可用。(非必须,但是debug效果显著) 3,将可用的列表放进一个数组中,并传给block。 4,失败时的block返回。 HMIAPHelper的更改 打开HMIAPHelper.h,删除requestProducts方法: #import"IAPHelper.h" @interfaceHMIAPHelper : IAPHelper+ (HMIAPHelper*)sharedInstance;@end 传到HMIAPHelper.m中同样删除requestProducts方法: #import"IAPProduct.h" - (id)init { IAPProduct* tenHints = [[IAPProductalloc] initWithProductIdentifier: @"com.razeware.hangman.tenhints"]; IAPProduct* hundredHints = [[IAPProductalloc] initWithProductIdentifier: @"com.razeware.hangman.hundredhints"]; NSMutableDictionary* products = [@{ tenHints.productIdentifier: tenHints,hundredHints.productIdentifier: hundredHints}mutableCopy]; if((self= [superinitWithProducts:products])) {} return self; } 打开HMStoreListViewController.m,导入头文件和2个实例: #import"IAPProduct.h" #import @implementationHMStoreListViewController { NSArray* _products; NSNumberFormatter* _priceFormatter; } ——priceFormatter:数字格式化,将有助于讲产品的价格本地化。 在viewDidLoad中删除requestProduct,代之以: _priceFormatter= [[NSNumberFormatteralloc]init]; [_priceFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; [_priceFormattersetNumberStyle:NSNumberFormatterCurrencyStyle]; self.refreshControl= [[UIRefreshControlalloc]init]; [self.refreshControladdTarget:selfaction:@selector(reload) forControlEvents:UIControlEventValueChanged]; [selfreload]; [self.refreshControlbeginRefreshing]; - (void)reload { //1 _products=nil; [self.tableViewreloadData]; //2 [[HMIAPHelpersharedInstance] requestProductsWithCompletionHandler:^(BOOLsuccess,NSArray*products) { if(success) { _products= products; [self.tableViewreloadData]; } [self.refreshControlendRefreshing];}]; } 1,清空tableView上原有数据。 2,请求产品列表,假如请求成功:保存列表,并展示在tableview上。 更改tableview的delgate: - (NSInteger)tableView:(UITableView*)tableViewnumberOfRowsInSection:(NSInteger)section { return_products.count; } - (UITableViewCell*)tableView:(UITableView*)tableViewcellForRowAtIndexPath:(NSIndexPath*)indexPath { staticNSString*CellIdentifier =@"Cell"; HMStoreListViewCell*cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; IAPProduct*product =_products[indexPath.row]; cell.titleLabel.text= product.skProduct.localizedTitle; cell.descriptionLabel.text=product.skProduct.localizedDescription; //1 [_priceFormattersetLocale:product.skProduct.priceLocale]; cell.priceLabel.text= [_priceFormatter stringFromNumber:product.skProduct.price]; returncell; } 运行app: ---------------------------------------- 展示详细视图 打开HMStoreDetailViewController.h更改如下: @classIAPProduct; @interfaceHMStoreDetailViewController :UIViewController@property(nonatomic,strong)IAPProduct* product; @end 在HMStoreDetailViewController.m中 #import"IAPProduct.h" #import @implementationHMStoreDetailViewController { NSNumberFormatter* _priceFormatter; } - (void)viewDidLoad{ [superviewDidLoad]; self.view.backgroundColor= [UIColor colorWithPatternImage:[UIImageimageNamed:@"bg.png"]]; _priceFormatter= [[NSNumberFormatteralloc]init]; [_priceFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; [_priceFormatter setNumberStyle:NSNumberFormatterCurrencyStyle]; } - (void)refresh { self.title=_product.skProduct.localizedTitle; self.titleLabel.text=_product.skProduct.localizedTitle; self.descriptionTextView.text=_product.skProduct.localizedDescription; [_priceFormattersetLocale:_product.skProduct.priceLocale]; self.priceLabel.text= [_priceFormatter stringFromNumber:_product.skProduct.price]; self.versionLabel.text= @"Version 1.0"; if(_product.allowedToPurchase) { self.navigationItem.rightBarButtonItem= [[UIBarButtonItemalloc]initWithTitle:@"Buy"style:UIBarButtonItemStyleBordered target:selfaction:@selector(buyTapped:)]; self.navigationItem.rightBarButtonItem.enabled=YES; }else{ self.navigationItem.rightBarButtonItem=nil; } self.pauseButton.hidden=YES; self.resumeButton.hidden=YES; self.cancelButton.hidden=YES; } - (void)viewWillAppear:(BOOL)animated { [superviewWillAppear:animated]; self.statusLabel.hidden=YES; [selfrefresh]; } 转到HMStoreListViewController.m中: #import"HMStoreDetailViewController.h" 加入新方法: #pragma mark - Table view delegate - (void)tableView:(UITableView*)tableViewdidSelectRowAtIndexPath:(NSIndexPath*)indexPath { [selfperformSegueWithIdentifier:@"PushDetail" sender:indexPath]; } #pragma mark - Segues - (void)prepareForSegue:(UIStoryboardSegue*)seguesender:(id)sender { if([segue.identifierisEqualToString:@"PushDetail"]) { HMStoreDetailViewController* detailViewController = (HMStoreDetailViewController*)segue.destinationViewController; NSIndexPath* indexPath = (NSIndexPath*)sender; IAPProduct*product = _products[indexPath.row]; detailViewController.product= product; } } 因为产品类型是consumable,故清空restoreTapped:方法体。 购买产品的实现: 现在将开始等待许久的购买模块。 该模块将包含4步: 1,发送支付请求 2,交易进程和结果。 3,解锁内容(购买成功) 4,结束交易 当你购买一个产品时,不允许再次购买同一产品,除非购买中的已完成。(虽然这是不必须的,但是对于用户是好的,以及使app更简洁)。 为了达到改目的,打开IAPProduct.h文件,添加变量: @property(nonatomic,assign)BOOLpurchaseInProgress; 在IAPProduct.m中更改如下: - (BOOL)allowedToPurchase { if(!self.availableForPurchase) return NO; if(self.purchaseInProgress) return NO; return YES; } 当iTunesConnect返回产品不可用和该产品正在购买时,用户是不允许购买的。如此甚好。 接下来,在IAPHelper中添加一个新方法使用户购买产品: 在IAPHelper.h中: @classIAPProduct; - (void)buyProduct:(IAPProduct*)product; 在IAPHelper.m中: - (void)buyProduct:(IAPProduct*)product { //1 NSAssert(product.allowedToPurchase,@"This product isn't allowed to be purchased!"); NSLog(@"Buying %@...", product.productIdentifier); product.purchaseInProgress=YES; SKPayment* payment = [SKPayment paymentWithProduct:product.skProduct]; [[SKPaymentQueuedefaultQueue]addPayment:payment]; } 1,检测产品是否允许购买。 现在需要添加部分代码去鉴定交易是否进行中。 在IAPHelper.m中添加交易监听: @interfaceIAPHelper() 方法: - (id)initWithProducts:(NSMutableDictionary*)products { if((self= [superinit])) { _products= products; [[SKPaymentQueuedefaultQueue] addTransactionObserver:self]; return self; }} 当IAPHelper初始化时,它便开始监听交易的进行。即apple将实时告诉你有无交易完成。 注:关键在于当用户开始一个交易或者完成一个交易的付款,在apple返回成功与否之前,用户突然掉线,终端交易,但用户仍然希望返回购买的东西。 所幸,apple已经有解决该问题的办法。即apple将追踪app上没有完成的交易,并通知给监听。但是为了使之工作良好,你需要在app启动时就讲类注册为交易的监听对象。 在AppDelegate.m中的解决代码: #import"HMIAPHelper.h" 在application:didFinishLaunchingWithOptiions:的开始: [HMIAPHelpersharedInstance]; 现在在app启动后,它将创建HMIAPHelper单例,并将自己注册为交易监听的对象,并随时被提醒没完成的交易。 你需要完成交易监听的协议,在IAPHelper.m中: - (void)paymentQueue:(SKPaymentQueue*)queueupdatedTransactions:(NSArray*)transactions { for(SKPaymentTransaction* transactionintransactions) { switch(transaction.transactionState){ caseSKPaymentTransactionStatePurchased: [selfcompleteTransaction:transaction]; break; caseSKPaymentTransactionStateFailed: [selffailedTransaction:transaction]; break; caseSKPaymentTransactionStateRestored: [selfrestoreTransaction:transaction]; default: break; }}; } 这是交易监听协议仅要求的一个方法。它提供给你一个正在更新运行的交易列表,唯一要做的就是轮询它们,并根据状态来实施相应的动作。 关于complete,和failed状态自不必多讲,关于restored,它是应用于’non-consumable‘类型的IAP产品上的。对于多设备同一app,restored是很重要的,当然也包括同一设备,重装app的情况。 3个具体的方法实现: - (void)completeTransaction:(SKPaymentTransaction*)transaction { NSLog(@"completeTransaction..."); [sel provideContentForTransaction:transaction productIdentifier:transaction.payment.productIdentifier]; } - (void)restoreTransaction:(SKPaymentTransaction*)transaction { NSLog(@"restoreTransaction“); [self provideContentForTransaction:transaction productIdentifier:transaction.originalTransaction.payment.productIdentifier]; } - (void)failedTransaction:(SKPaymentTransaction*)transaction { NSLog(@"failedTransaction..."); if(transaction.error.code!=SKErrorPaymentCancelled){ NSLog(@"Transaction error: %@",transaction.error.localizedDescription); } IAPProduct* product =_products[transaction.payment.productIdentifier]; [selfnotifyStatusForProductIdentifier:transaction.payment.productIdentifierstring:@"Purchase failed."]; product.purchaseInProgress=NO; [[SKPaymentQueuedefaultQueue]finishTransaction: transaction]; } complete和restore做了相同的事情,即提供内容。failed调用一个方法,来发出失败的通知,并将产品标记为不再购买中,并结束之。 注:结束交易异常重要,否则storeKit无法知道你已结束了该交易,并在app每次启动时都激活该交易。 - (void)notifyStatusForProductIdentifier: (NSString*)productIdentifier string:(NSString*)string { IAPProduct* product =_products[productIdentifier]; [self notifyStatusForProduct:product string:string]; } - (void)notifyStatusForProduct:(IAPProduct*)productstring:(NSString*)string { } - (void)provideContentForTransaction:(SKPaymentTransaction*)transactionproductIdentifier:(NSString*)productIdentifier { IAPProduct* product =_products[productIdentifier]; [selfprovideContentForProductIdentifier:productIdentifier];[selfnotifyStatusForProductIdentifier:productIdentifier string:@"Purchase complete!"]; product.purchaseInProgress=NO; [[SKPaymentQueuedefaultQueue]finishTransaction: transaction]; } - (void)provideContentForProductIdentifier:(NSString*)productIdentifier { } 方法一获取对应的product,并执行另外一个目前为空的notifyStatusForProduct方法(在HMIAPHelper中完善,使该部分功能独立于app)。 方法provideContentForTransaction类似于failedTransaction,均是使交易得以结束。但在此之前,它将调用一个目前为空的provideContentForProductIdentifier方法(同样在子类中完成)。 接下来,打开HMIAPHelper.m吧。添加代码: #import "HMContentController.h" #import "JSNotifier.h" #import 关于JSNotifier文件:它是Jonah Siegle写的,是在屏幕底部弹出警示框以显示正确或者错误信息。在此用来显示上述提及的notifications。 在文件底部添加: - (void)provideContentForProductIdentifier:(NSString *)productIdentifier { if ([productIdentifierisEqualToString:@"com.razeware.hangman.tenhints"]) { int curHints = [HMContentController sharedInstance].hints; [[HMContentController sharedInstance] setHints:curHints + 10]; } else if ([productIdentifierisEqualToString:@"com.razeware.hangman.hundredhints"]) { int curHints = [HMContentController sharedInstance].hints; [[HMContentController sharedInstance] setHints:curHints + 100]; }} - (void)notifyStatusForProduct:(IAPProduct *)productstring:(NSString *)string { NSString * message = [NSString stringWithFormat:@"%@: %@",product.skProduct.localizedTitle, string]; JSNotifier *notify =[[JSNotifier alloc]initWithTitle:message]; [notify showFor:2.0]; } 方法一是简单的增加购买提示数目(+10 || +100)。 方法二弹出警示信息。 最后一个步骤是在HMStoreDetailViewController.m中添加购买代码: #import "HMIAPHelper.h" 在空方法buyTapped中添加: - (void)buyTapped:(id)sender { NSLog(@"Buy tapped!"); [[HMIAPHelper sharedInstance] buyProduct:self.product]; } 现在将开始测试购买: 测试购买,你需要一个在App Store sandbox的测试账户,假如目前你还没有,那么登陆iTunes Connect创建吧,选择Manage Users,选择Test User,并Add New User,完成表格并保存之。 写下来需要注意的是,如果你在真机上测试,记得将你真实的AppStore账号登出。即Settings/iTunes &App Stores,登出。 运行项目,打开product,点击购买,将弹出如下: 点击‘buy’,初次购买的话将出现: 点击‘Use Existing Apple ID’,填写你刚刚创建的Test User,稍等片刻,你就可以看到购买成功的提示。 可以运行程序验证hints数目。 -------------------------------------------------------------------- 购买一个Non-Consumable 类型的IAP产品。 现在可以向用户兜售hints,你即将迈步小资生活。 加入可以向用户兜售额外的单词库,你将会赚取更多的钱。 在之前你很明智的将单词库以文件形式保存,这将有利于一次性添加额外的单词库。 现在我们来添加‘Harde words’和‘IOS words’库。 实现此,大致需要五步: 1,把冰箱门打开。 。。。。。。 1,测试单词库。 2,在iTunes Connect中注册IAP产品。 3,修改代码部分。 4,恢复(restore)购买 5,结束。 1,测试单词库 在做任何有关IAP之前,第一步便是需要确保你的单词库可以正常工作。(略。即2个plist格式的单词库添加到项目中)。 此处单词库是以子文件夹的形式置于项目中的。 2,在iTunes Connect中注册IAP产品 该步骤非常类似于添加hints产品,但区别在于这边的IAP产品类型要选择Non-consumable。 打开iTunes Connect,点击‘Manage Your Applications’,点击项目并进入‘Manage In-App Purchases’,点击‘Create New’,选择Non-consumable\Select,将显示如下: 滚动到底部In-App Purchase Details\Language,点击‘Add Language’,填写如下: 保存,整体保存。 同样步骤完成IOS Words库。 3,修改代码。 和consumable类型的不同点: 1,保持用户是否购买成功的记录,当用户重启app时,解锁对应的产品。 2,一个用户仅允许购买一次Non-consumable类型的产品,并非不限次数。 3,允许用户在其他的设备上使用已购买的non-consumable类型的产品。 基于上述三点,我们来完成相应代码。 首先,打开IAPProduct.h,添加新变量来辨别用户是否之前已购买non-consumable产品。 @property (nonatomic, assign) BOOL purchase; 在IAPProduct.m中: - (BOOL)allowedToPurchase { if (!self.availableForPurchase) return NO; if (self.purchaseInProgress) return NO; if (self.purchase) return NO; return YES; } IAPHelper不用做任何修改,但需要在HMIAPHelper中做些许修改。 首先,添加一个新方法:通过给定的produnctIdentifier和目录解锁单词库: - (void)unlockWordsForProductIdentifier:(NSString *)productIdentifier directory:(NSString *)directory { // 1 IAPProduct * product = self.products[productIdentifier]; product.purchase = YES; // 2 [[NSUserDefaults standardUserDefaults] setBool:YES forKey:productIdentifier]; [[NSUserDefaults standardUserDefaults] synchronize]; // 3 NSURL * resourceURL = [NSBundle mainBundle].resourceURL; [[HMContentController sharedInstance] unlockWordsWithDirURL:[resourceURLURLByAppendingPathComponent:directory]]; } 1,通过identifier找到该product,并设置其为‘已购买’。 2,记录某产品是否已购买,最简单的方式便是将该信息保存在本地。 3,解锁购买内容。 现在在provideContentForProductIdentifier的if/else的结尾添加: else if ([productIdentifierisEqualToString:@"com.razeware.hangman.hardwords"]) { [self unlockWordsForProductIdentifier:@"com.razeware.hangman.hardwords" directory:@"HardWords"]; } else if ([productIdentifierisEqualToString:@"com.razeware.hangman.ioswords"]) { [self unlockWordsForProductIdentifier:@"com.razeware.hangman.ioswords" directory:@"iOSWords"]; } 在购买IAP产品成功时调用该方法,完成对应产品的解锁。 另外,在app新启动时,你需要调用解锁方法。即通过NSUserDefault来来判断是否解锁内容。 在HMISAPHelper.m中替换: - (id)init { IAPProduct * tenHints = [[IAPProduct alloc] initWithProductIdentifier:@"com.razeware.hangman.tenhints"]; IAPProduct * hundredHints = [[IAPProduct alloc]initWithProductIdentifier:@"com.razeware.hangman.hundredhints"]; // 1 IAPProduct * hardWords = [[IAPProduct alloc]initWithProductIdentifier:@"com.razeware.hangman.hardwords"]; IAPProduct * iosWords = [[IAPProduct alloc] initWithProductIdentifier:@"com.razeware.hangman.ioswords"]; NSMutableDictionary * products = [@{tenHints.productIdentifier: tenHints,hundredHints.productIdentifier: hundredHints,hardWords.productIdentifier: hardWords,iosWords.productIdentifier: iosWords} mutableCopy]; if ((self = [super initWithProducts:products])) { // 2 if ([[NSUserDefaults standardUserDefaults] boolForKey:@"com.razeware.hangman.hardwords"]) { [self unlockWordsForProductIdentifier:@"com.razeware.hangman.hardwords" directory:@"HardWords"]; } if ([[NSUserDefaults standardUserDefaults]boolForKey:@"com.razeware.hangman.ioswords"]) { [self unlockWordsForProductIdentifier:@"com.razeware.hangman.ioswords"directory:@"iOSWords"]; } } return self; } 1,将新增加的non-consumable产品加到Product列表中。如此,在父类IAPHelper中,将会把其添加到SKProductReuest的请求产品列表中。 2,在新启动app时,将会依据本地保存的标示,来决定是否解锁对应的内容。 打开HMStoreListViewController.m文件,修改以下: 在tableView:cellForRowAtindexpath:中,用以下代码替换cell.pricelabel.text: if (product.purchase) { cell.priceLabel.text = @"Installed"; } else { cell.priceLabel.text = [_priceFormatter stringFromNumber:product.skProduct.price]; } 当用户查看产品列表时,假如已购买的项目,将会显示‘installed’。否则显示购买价格。 运行程序,你会看到新建立的产品将会显示在列表中。 打开IOS Words,并购买,购买成功显示详细视图: 购买成功后查看words列表,你将看到成功添加了新的ios 单词库。但是此时在iap列表详细,你还看不到有任何变化,我们将稍后修改此问题。 4,恢复(restore)购买 假如你的app中含有non-consumable产品,apple要求你的应用必须允许用户可以恢复购买的功能。 首先,打开IAPHelper.h文件,声明一个新方法: - (void)restoreCompletedTransactions; 在IAPHelper.m中: - (void)restoreCompletedTransactions {[[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; } 该方法使用storeKit链接至appStore,获取用户已经购买的non-consumable产品,接着调用paymentQueue:updatedTransactions方法中的SKPaymentTransactionStateRestored:(已经完善)。 让我们看下GUI部分,打开HMStoreListViewController.m,添加新的类扩展,即实现UIAlertView的委托事件: @interface HMStoreListViewController() @end 在restoreTapped:中: - (void)restoreTapped:(id)sender { NSLog(@"Restore tapped!"); UIAlertView * alertView = [[UIAlertView alloc] initWithTitle:@"Restore Content" message:@"Would you like to check for and restore anyprevious purchases?" delegate:self cancelButtonTitle:@"Cancel"otherButtonTitles:@"OK", nil]; alertView.delegate = self; [alertView show]; } #pragma mark - UIAlertViewDelegate - (void)alertView:(UIAlertView *)alertViewdidDismissWithButtonIndex:(NSInteger)buttonIndex { if (buttonIndex == alertView.firstOtherButtonIndex) { [[HMIAPHelper sharedInstance] restoreCompletedTransactions]; } } 删除app,重装,并点击‘restore’,你的应用应该会执行以上功能。 5,结束。 在继续之前,你有2个需要完成的点击。 1,在你购买一个non-consumable产品后,你会发现仍然是购买按钮显示,并未显示‘installed’字样。点击将会崩溃。因为你有一个断言Assert,去保证这个产品是否允许购买(此处不可购买,因为你已经完成了购买)。你有多种方式来实现更改界面的功能:监听,委托或者其他。但在此,我们将使用KVO。 KVO:允许你关注在一个对象中的任何属性的实时变化,此处只需关注purchaseInprogress或者purchase的变化即可,接着刷新。 完成该功能,进入HMStoreDetailViewController.m文件,在viewWillAppear中,替换如下: - (void)viewWillAppear:(BOOL)animated {[super viewWillAppear:animated];self.statusLabel.hidden = YES;[self refresh]; [self.product addObserver:selfforKeyPath:@"purchaseInProgress" options:0 context:nil]; [self.product addObserver:selfforKeyPath:@"purchase" options:0 context:nil]; } - (void)viewWillDisappear:(BOOL)animated {[super viewWillDisappear:animated];[self.product removeObserver:self forKeyPath:@"purchaseInProgress" context:nil];[self.product removeObserver:self forKeyPath:@"purchase" context:nil]; } - (void)observeValueForKeyPath:(NSString *)keyPathofObject:(id)object change:(NSDictionary *)changecontext:(void *)context { [self refresh];} 当willAppear视图时,将关注2个属性,当disappear的时候,取消关注。 当关注的属性有变化时,将调用observeValueForKeyPath,刷新之。 2,在产品列表界面需要知道产品状态的变化。 打开HMStoreListViewController.m文件,添加变量: @implementation HMStoreListViewController { NSArray * _products; NSNumberFormatter * _priceFormatter; BOOL _observing; } 添加: - (void)viewWillAppear:(BOOL)animated {[super viewWillAppear:animated];[self addObservers];[self.tableView reloadData]; } - (void)viewWillDisappear:(BOOL)animated {[super viewWillDisappear:animated];[self removeObservers]; } #pragma mark - KVO - (void)addObservers { if (_observing || _products == nil) return;_observing = TRUE; for (IAPProduct * product in _products) { [product addObserver:self forKeyPath:@"purchaseInProgress" options:0 context:nil]; [product addObserver:self forKeyPath:@"purchase" options:0context:nil]; }} - (void)removeObservers { if (!_observing) return; _observing = FALSE; for (IAPProduct * product in _products) { [product removeObserver:selfforKeyPath:@"purchaseInProgress" context:nil]; [product removeObserver:self forKeyPath:@"purchase"context:nil]; }} - (void)observeValueForKeyPath:(NSString *)keyPathofObject:(id)object change:(NSDictionary *)changecontext:(void *)context { IAPProduct * product = (IAPProduct *)object; int row = [_products indexOfObject:product]; NSIndexPath * indexPath = [NSIndexPath indexPathForRow:row inSection:0]; [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; } - (void)setProducts:(NSArray *)products {[self removeObservers]; _products = products; [self addObservers]; } 包括添加和删除对product数组元素的监听。 修改reload方法,应用setProducts:方法: - (void)reload { [self setProducts:nil];[self.tableView reloadData];[[HMIAPHelper sharedInstance] requestProductsWithCompletionHandler:^(BOOL success, NSArray*products) { if (success) { [self setProducts:products];[self.tableView reloadData]; } [self.refreshControl endRefreshing];}]; } 运行程序,检测下,完美。 验证购买的可用性 当你进行IAp购买,对于返回的ok信息你不能100%保证是真从apple那里得来的,加入没有使用‘receipt validation’的话。 当你进行IAP购买时,apple将返回给你一块数据,叫做‘receipt’。这是一个带有本次交易的加密签名信息的私有数据块。这么做的主要目当然是为了安全。现在你需要将这个receipt信息发送到apple提供的一个特殊的‘receipt validation’服务器上,用来确保这个是有效的。 (不执行这一步的危险性已经被证实:俄罗斯的一个黑客易安装的IAP应用,使用户可以接受任何免费的IAP产品:你配置的DNS使你的设备路由到黑客提供的服务器上,而不是apple的购买服务器上,并且在设备上你还配置了一个欺骗证书,使黑客服务器是可信的。之后不论何时,你进行一个IAP购买请求,请求将转向何可服务器,并且黑客服务器一直返回‘done and paid for’已经购买成功的信息,接着,app将解锁购买的内容--------该黑客在IOS6将不能发挥作用了。然而还有其他的黑客可以实现该欺骗手法)。 更多IAP黑客讨论:http://www.macworld.com/article/1167677/hacker_exploits_ios_flaw_for_free_in_app_purchases.html 无服务器解决方案: apple官方建议:链接你自己的服务器去验证信息,然后才是建议链接apple的服务器去验证。前者安全性更高。 在此暂时介绍无自己服务器的情况即链接apple服务器去验证 在解锁内容之前,需要验证。 在本项目资源文件中,你将看到有个文件夹:VerificationController,将其拖拽到libs中。并导入security.framework库。 在IAPHelper.m中: #import "VerificationController.h" 添加方法: - (void)validateReceiptForTransaction:(SKPaymentTransaction *)transaction { IAPProduct * product =_products[transaction.payment.productIdentifier]; VerificationController * verifier =[VerificationController sharedInstance]; [verifier verifyPurchase:transactioncompletionHandler:^(BOOL success) { if (success) { NSLog(@"Successfully verified receipt!"); [self provideContentForTransaction:transaction productIdentifier:transaction.payment.productIdentifier]; } else { NSLog(@"Failed to validate receipt."); product.purchaseInProgress = NO; [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; }}]; } 这个方法是简单的调用apple的代码(略改动)去验证交易的有效性,并据此是否解锁内容。 最后是在completetrasaction和restoreTransaction中调用该方法,而不是直接立即提供解锁内容。 - (void)completeTransaction:(SKPaymentTransaction *)transaction { NSLog(@"completeTransaction..."); [self validateReceiptForTransaction:transaction]; } - (void)restoreTransaction:(SKPaymentTransaction *)transaction { NSLog(@"restoreTransaction..."); [self validateReceiptForTransaction:transaction]; } 运行程序,结果如前,但有了更好的安全性。 (引入验证时,注意在verifyPurchase:complete:方法中,用的是sandbox接口,你在发布app前要将其改成buy实际的购买验证接口)。

posted on 2014-08-19 09:09  三十一  阅读(1344)  评论(0编辑  收藏  举报

导航