1、ios开发之 内购
大家都知道做iOS开发本身的收入有三种来源:出售应用、内购和广告。国内用户通常很少直接购买应用,因此对于开发者而言(特别是个人开发者),内购和广告收入就成了主要的收入来源。内购营销模式,通常软件本身是不收费的,但是要获得某些特权就必须购买一些道具,而内购的过程是由苹果官方统一来管理的,所以和Game Center一样,在开发内购程序之前要做一些准备工作(下面的准备工作主要是针对真机的,模拟器省略Provisioning Profile配置过程):
- 前四步和Game Center基本完全一致,只是在选择服务时不是选择Game Center而是要选择内购服务(In-App Purchase)。
- 到iTuens Connect中设置“App 内购买项目”,这里仍然以上面的“KCTest”项目为例,假设这个足球竞技游戏中有三种道具,分别为“强力手套”(增强防御)、“金球”(增加金球率)和“能量瓶”(提供足够体力),前两者是非消耗品只用一次性购买,后者是消耗品用完一次必须再次购买。
- 到iTunes Connect中找到“协议、税务和银行业务”增加“iOS Paid Applications”协议,并完成所有配置后等待审核通过(注意这一步如果不设置在应用程序中无法获得可购买产品)。
- 在iOS“设置”中找到”iTunes Store与App Store“,在这里可以选择使用沙盒用户登录或者处于注销状态,但是一定注意不能使用真实用户登录,否则下面的购买测试不会成功,因为到目前为止我们的应用并没有真正通过苹果官方审核只能用沙盒测试用户(如果是模拟器不需要此项设置)。
- 有了上面的设置之后保证应用程序Bundle ID和iTunes Connect中的Bundle ID(或者说App ID中配置的Bundle ID)一致即可准备开发。
开发内购应用时需要使用StoreKit.framework,下面是这个框架中常用的几个类:
SKProduct:可购买的产品(例如上面设置的能量瓶、强力手套等),其productIdentifier属性对应iTunes Connect中配置的“产品ID“,但是此类不建议直接初始化使用,而是要通过SKProductRequest来加载可用产品(避免出现购买到无效的产品)。
SKProductRequest:产品请求类,主要用于加载产品列表(包括可用产品和不可用产品),通常加载完之后会通过其-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response代理方法获得响应,拿到响应中的可用产品。
SKPayment:产品购买支付类,保存了产品ID、购买数量等信息(注意与其对应的有一个SKMutablePayment对象,此对象可以修改产品数量等信息)。
SKPaymentQueue:产品购买支付队列,一旦将一个SKPayment添加到此队列就会向苹果服务器发送请求完成此次交易。注意交易的状态反馈不是通过代理完成的,而是通过一个交易监听者(类似于代理,可以通过队列的addTransactionObserver来设置)。
SKPaymentTransaction:一次产品购买交易,通常交易完成后支付队列会调用交易监听者的-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction方法反馈交易情况,并在此方法中将交易对象返回。
SKStoreProductViewController:应用程序商店产品展示视图控制器,用于在应用程序内部展示此应用在应用商店的情况。(例如可以使用它让用户在应用内完成评价,注意由于本次演示的示例程序没有正式提交到应用商店,所以在此暂不演示此控制器视图的使用)。
了解了以上几个常用的开发API之后,下面看一下应用内购买的流程:
- 通过SKProductRequest获得可购买产品SKProduct数组(SKProductRequest会根据程序的Bundle ID去对应的内购配置中获取指定ID的产品对象),这个过程中需要知道产品标识(必须和iTuens Connect中的对应起来),可以存储到沙盒中也可以存储到数据库中(下面的Demo中定义成了宏定义)。
- 请求完成后可以在SKProductRequest的-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response代理方法中获得SKProductResponse对象,这个对象中保存了products属性表示可用产品对象数组。
- 给SKPaymentQueue设置一个监听者来获得交易的状态(它类似于一个代理),监听者通过-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction方法反馈交易的变化状态(通常在此方法中可以根据交易成功、恢复成功等状态来做一些处理)。
- 一旦用户决定购买某个产品(SKProduct),就可以根据SKProduct来创建一个对应的支付对象SKPayment,只要将这个对象加入到SKPaymentQueue中就会触发购买行为(将订单提交到苹果服务器),一旦一个交易发生变化就会触发SKPaymentQueue监听者来反馈交易情况。
- 交易提交给苹果服务器之后如果不出意外的话通常就会弹出一个确认购买的对话框,引导用户完成交易,最终完成交易后(通常是完成交易,用户点击”好“)会调用交易监听者-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction方法将此次交易的所有交易对象SKPaymentTransaction数组返回,可以通过交易状态判断交易情况。
- 通常一次交易完成后需要对本次交易进行验证,避免越狱机器模拟苹果官方的反馈造成交易成功假象。苹果官方提供了一个验证的URL,只要将交易成功后的凭证(这个凭证从iOS7之后在交易成功会会存储到沙盒中)传递给这个地址就会给出交易状态和本次交易的详细信息,通过这些信息(通常可以根据交易状态、Bundler ID、ProductID等确认)可以标识出交易是否真正完成。
- 对于非消耗品,用户在完成购买后如果用户使用其他机器登录或者用户卸载重新安装应用后通常希望这些非消耗品能够恢复(事实上如果不恢复用户再次购买也不会成功)。调用SKPaymentQueue的restoreCompletedTransactions就可以完成恢复,恢复后会调用交易监听者的paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction方法反馈恢复的交易(也就是已购买的非消耗品交易,注意这个过程中如果没有非消耗品可恢复,是不会调用此方法的)。
下面通过一个示例程序演示内购和恢复的整个过程,程序界面大致如下:
主界面中展示了所有可购买产品和售价,以及购买情况。
选择一个产品点”购买“可以购买此商品,购买完成后刷新购买状态(如果是非消耗品则显示已购买,如果是消耗品则显示购买个数)。
程序卸载后重新安装可以点击”恢复购买“来恢复已购买的非消耗品。
程序代码:
// // KCMainTableViewController.m // kctest // // Created by Kenshin Cui on 14/4/5. // Copyright (c) 2015年 cmjstudio. All rights reserved. // #import "KCMainTableViewController.h" #import <StoreKit/StoreKit.h> #define kAppStoreVerifyURL @"https://buy.itunes.apple.com/verifyReceipt" //实际购买验证URL #define kSandboxVerifyURL @"https://sandbox.itunes.apple.com/verifyReceipt" //开发阶段沙盒验证URL //定义可以购买的产品ID,必须和iTunes Connect中设置的一致 #define kProductID1 @"ProtectiveGloves" //强力手套,非消耗品 #define kProductID2 @"GoldenGlobe" //金球,非消耗品 #define kProductID3 @"EnergyBottle" //能量瓶,消耗品 @interface KCMainTableViewController ()<SKProductsRequestDelegate,SKPaymentTransactionObserver> @property (strong,nonatomic) NSMutableDictionary *products;//有效的产品 @property (assign,nonatomic) int selectedRow;//选中行 @end @implementation KCMainTableViewController #pragma mark - 控制器视图方法 - (void)viewDidLoad { [super viewDidLoad]; [self loadProducts]; [self addTransactionObjserver]; } #pragma mark - UI事件 //购买产品 - (IBAction)purchaseClick:(UIBarButtonItem *)sender { NSString *productIdentifier=self.products.allKeys[self.selectedRow]; SKProduct *product=self.products[productIdentifier]; if (product) { [self purchaseProduct:product]; }else{ NSLog(@"没有可用商品."); } } //恢复购买 - (IBAction)restorePurchaseClick:(UIBarButtonItem *)sender { [self restoreProduct]; } #pragma mark - UITableView数据源方法 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.products.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *identtityKey=@"myTableViewCellIdentityKey1"; UITableViewCell *cell=[self.tableView dequeueReusableCellWithIdentifier:identtityKey]; if(cell==nil){ cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identtityKey]; } cell.accessoryType=UITableViewCellAccessoryNone; NSString *key=self.products.allKeys[indexPath.row]; SKProduct *product=self.products[key]; NSString *purchaseString; NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults]; if ([product.productIdentifier isEqualToString:kProductID3]) { purchaseString=[NSString stringWithFormat:@"已购买%i个",[defaults integerForKey:product.productIdentifier]]; }else{ if([defaults boolForKey:product.productIdentifier]){ purchaseString=@"已购买"; }else{ purchaseString=@"尚未购买"; } } cell.textLabel.text=[NSString stringWithFormat:@"%@(%@)",product.localizedTitle,purchaseString] ; cell.detailTextLabel.text=[NSString stringWithFormat:@"%@",product.price]; return cell; } #pragma mark - UITableView代理方法 -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ UITableViewCell *currentSelected=[tableView cellForRowAtIndexPath:indexPath]; currentSelected.accessoryType=UITableViewCellAccessoryCheckmark; self.selectedRow=indexPath.row; } -(void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath{ UITableViewCell *currentSelected=[tableView cellForRowAtIndexPath:indexPath]; currentSelected.accessoryType=UITableViewCellAccessoryNone; } #pragma mark - SKProductsRequestd代理方法 /** * 产品请求完成后的响应方法 * * @param request 请求对象 * @param response 响应对象,其中包含产品信息 */ -(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{ //保存有效的产品 _products=[NSMutableDictionary dictionary]; [response.products enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { SKProduct *product=obj; [_products setObject:product forKey:product.productIdentifier]; }]; //由于这个过程是异步的,加载成功后重新刷新表格 [self.tableView reloadData]; } -(void)requestDidFinish:(SKRequest *)request{ NSLog(@"请求完成."); } -(void)request:(SKRequest *)request didFailWithError:(NSError *)error{ if (error) { NSLog(@"请求过程中发生错误,错误信息:%@",error.localizedDescription); } } #pragma mark - SKPaymentQueue监听方法 /** * 交易状态更新后执行 * * @param queue 支付队列 * @param transactions 交易数组,里面存储了本次请求的所有交易对象 */ -(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{ [transactions enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { SKPaymentTransaction *paymentTransaction=obj; if (paymentTransaction.transactionState==SKPaymentTransactionStatePurchased){//已购买成功 NSLog(@"交易\"%@\"成功.",paymentTransaction.payment.productIdentifier); //购买成功后进行验证 [self verifyPurchaseWithPaymentTransaction]; //结束支付交易 [queue finishTransaction:paymentTransaction]; }else if(paymentTransaction.transactionState==SKPaymentTransactionStateRestored){//恢复成功,对于非消耗品才能恢复,如果恢复成功则transaction中记录的恢复的产品交易 NSLog(@"恢复交易\"%@\"成功.",paymentTransaction.payment.productIdentifier); [queue finishTransaction:paymentTransaction];//结束支付交易 //恢复后重新写入偏好配置,重新加载UITableView [[NSUserDefaults standardUserDefaults]setBool:YES forKey:paymentTransaction.payment.productIdentifier]; [self.tableView reloadData]; }else if(paymentTransaction.transactionState==SKPaymentTransactionStateFailed){ if (paymentTransaction.error.code==SKErrorPaymentCancelled) {//如果用户点击取消 NSLog(@"取消购买."); } NSLog(@"ErrorCode:%i",paymentTransaction.error.code); } }]; } //恢复购买完成 -(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue{ NSLog(@"恢复完成."); } #pragma mark - 私有方法 /** * 添加支付观察者监控,一旦支付后则会回调观察者的状态更新方法: -(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions */ -(void)addTransactionObjserver{ //设置支付观察者(类似于代理),通过观察者来监控购买情况 [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; } /** * 加载所有产品,注意产品一定是从服务器端请求获得,因为有些产品可能开发人员知道其存在性,但是不经过审核是无效的; */ -(void)loadProducts{ //定义要获取的产品标识集合 NSSet *sets=[NSSet setWithObjects:kProductID1,kProductID2,kProductID3, nil]; //定义请求用于获取产品 SKProductsRequest *productRequest=[[SKProductsRequest alloc]initWithProductIdentifiers:sets]; //设置代理,用于获取产品加载状态 productRequest.delegate=self; //开始请求 [productRequest start]; } /** * 购买产品 * * @param product 产品对象 */ -(void)purchaseProduct:(SKProduct *)product{ //如果是非消耗品,购买过则提示用户 NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults]; if ([product.productIdentifier isEqualToString:kProductID3]) { NSLog(@"当前已经购买\"%@\" %i 个.",kProductID3,[defaults integerForKey:product.productIdentifier]); }else if([defaults boolForKey:product.productIdentifier]){ NSLog(@"\"%@\"已经购买过,无需购买!",product.productIdentifier); return; } //创建产品支付对象 SKPayment *payment=[SKPayment paymentWithProduct:product]; //支付队列,将支付对象加入支付队列就形成一次购买请求 if (![SKPaymentQueue canMakePayments]) { NSLog(@"设备不支持购买."); return; } SKPaymentQueue *paymentQueue=[SKPaymentQueue defaultQueue]; //添加都支付队列,开始请求支付 // [self addTransactionObjserver]; [paymentQueue addPayment:payment]; } /** * 恢复购买,对于非消耗品如果应用重新安装或者机器重置后可以恢复购买 * 注意恢复时只能一次性恢复所有非消耗品 */ -(void)restoreProduct{ SKPaymentQueue *paymentQueue=[SKPaymentQueue defaultQueue]; //设置支付观察者(类似于代理),通过观察者来监控购买情况 // [paymentQueue addTransactionObserver:self]; //恢复所有非消耗品 [paymentQueue restoreCompletedTransactions]; } /** * 验证购买,避免越狱软件模拟苹果请求达到非法购买问题 * */ -(void)verifyPurchaseWithPaymentTransaction{ //从沙盒中获取交易凭证并且拼接成请求体数据 NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL]; NSData *receiptData=[NSData dataWithContentsOfURL:receiptUrl]; NSString *receiptString=[receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];//转化为base64字符串 NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", receiptString];//拼接请求数据 NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding]; //创建请求到苹果官方进行购买验证 NSURL *url=[NSURL URLWithString:kSandboxVerifyURL]; NSMutableURLRequest *requestM=[NSMutableURLRequest requestWithURL:url]; requestM.HTTPBody=bodyData; requestM.HTTPMethod=@"POST"; //创建连接并发送同步请求 NSError *error=nil; NSData *responseData=[NSURLConnection sendSynchronousRequest:requestM returningResponse:nil error:&error]; if (error) { NSLog(@"验证购买过程中发生错误,错误信息:%@",error.localizedDescription); return; } NSDictionary *dic=[NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:nil]; NSLog(@"%@",dic); if([dic[@"status"] intValue]==0){ NSLog(@"购买成功!"); NSDictionary *dicReceipt= dic[@"receipt"]; NSDictionary *dicInApp=[dicReceipt[@"in_app"] firstObject]; NSString *productIdentifier= dicInApp[@"product_id"];//读取产品标识 //如果是消耗品则记录购买数量,非消耗品则记录是否购买过 NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults]; if ([productIdentifier isEqualToString:kProductID3]) { int purchasedCount=[defaults integerForKey:productIdentifier];//已购买数量 [[NSUserDefaults standardUserDefaults] setInteger:(purchasedCount+1) forKey:productIdentifier]; }else{ [defaults setBool:YES forKey:productIdentifier]; } [self.tableView reloadData]; //在此处对购买记录进行存储,可以存储到开发商的服务器端 }else{ NSLog(@"购买失败,未通过验证!"); } } @end
运行效果(这是程序在卸载后重新安装的运行效果,卸载前已经购买”强力手套“,因此程序运行后点击了”恢复购买“):