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实际的购买验证接口)。