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<StoreKit/StoreKit.h>

// 2

@interfaceIAPHelper() <SKProductsRequestDelegate>

@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<StoreKit/StoreKit.h> 


@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<StoreKit/StoreKit.h> 


@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() <SKProductsRequestDelegate,SKPaymentTransactionObserver


方法:

- (id)initWithProducts:(NSMutableDictionary*)products 

{

if((self= [superinit])) 

{

_products= products;

[[SKPaymentQueuedefaultQueueaddTransactionObserver: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 <StoreKit/StoreKit.h> 


关于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 sharedInstanceunlockWordsWithDirURL:[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 allocinitWithProductIdentifier:@"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 standardUserDefaultsboolForKey:@"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() <UIAlertViewDelegate

@end 


在restoreTapped:中:

- (void)restoreTapped:(id)sender 

{
NSLog(@"Restore tapped!");


UIAlertView * alertView = [[UIAlertView allocinitWithTitle:@"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 sharedInstancerestoreCompletedTransactions];

}


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






















  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
欢迎您对 STM32 IAP(In-Application Programming)感兴趣!IAP是一种在应用程序运行时通过软件实现的固件升级方法。下面是一个入门指南,帮助您了解如何使用STM32进行IAP。 1. 硬件准备: - 一款支持IAP的STM32开发板。 - 一个串口线,用于与计算机进行通信。 2. 软件准备: - STM32CubeIDE:用于开发和编译STM32的集成开发环境。 - STM32Cube库:提供了用于IAP的API和示例代码。 3. 创建工程: - 打开STM32CubeIDE,创建一个新的STM32工程。 - 选择您的目标MCU型号和配置参数。 - 在工程中包含必要的库文件。 4. 配置串口: - 在工程中配置串口,以便与计算机进行通信。 - 设置合适的波特率、数据位、停止位和校验位。 5. 编写应用程序: - 在主程序中添加IAP功能的代码。 - 使用IAP API实现固件读取、擦除、编程等操作。 - 可以通过串口接收命令,触发固件升级操作。 6. 编译和烧录: - 编译并生成可执行文件。 - 将可执行文件烧录到STM32开发板中。 7. 测试: - 启动STM32开发板,并通过串口与计算机建立连接。 - 发送命令触发固件升级操作。 - 监视升级过程中的提示和状态信息。 请注意,具体的实现步骤可能因不同的STM32型号和开发环境而有所差异。建议查阅官方文档、参考例程和相关资料,以获取更详细的指导和帮助。祝您在STM32 IAP的学习和开发中取得成功!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值