在iOS开发中若是涉及到虚拟物品的购买,就须要使用IAP服务,咱们今天来看看如何实现。html
在实现代码以前咱们先作一些准备工做,一步步来看。ios
IAP流程分为两种,一种是直接使用Apple的服务器进行购买和验证,另外一种就是本身假设服务器进行验证。因为国内网络链接Apple服务器验证很是慢,并且也为了防止黑客伪造购买凭证,通用作法是本身架设服务器进行验证。git
下面咱们经过图来看看两种方式的差异:github
简单说下第二中状况的流程:服务器
搞清楚了本身架设服务器是如何完成IAP购买的流程了以后,咱们下一步就是登陆到iTunes Connet建立应用和指定虚拟物品价格表网络
以下图所示,咱们须要建立一个本身的APP,要注意的是这里的Bundle ID必定要跟你的项目中的info.plist中的Bundle ID保证一致。也就是图中红框部分。app
消耗品(Consumable products):好比游戏内金币等。dom
不可消耗品(Non-consumable products):简单来讲就是一次购买,终身可用(用户可随时从App Store restore)。iphone
自动更新订阅品(Auto-renewable subscriptions):和不可消耗品的不一样点是有失效时间。好比一全年的付费周刊。在这种模式下,开发者按期投递内容,用户在订阅期内随时能够访问这些内容。订阅快要过时时,系统将自动更新订阅(若是用户赞成)。ide
非自动更新订阅品(Non-renewable subscriptions):通常使用场景是从用户从IAP购买后,购买信息存放在本身的开发者服务器上。失效日期/可用是由开发者服务器自行控制的,而非由App Store控制,这一点与自动更新订阅品有差别。
免费订阅品(Free subscriptions):在Newsstand中放置免费订阅的一种方式。免费订阅永不过时。只能用于Newsstand-enabled apps。
类型二、三、5都是以Apple ID为粒度的。好比小张有三个iPad,有一个Apple ID购买了不可消耗品,则三个iPad上均可以使用。
类型一、4通常来讲则是现买现用。若是开发者本身想作更多控制,通常选4
其中产品id是字母或者数字,或者二者的组合,用于惟一表示该虚拟物品,app也是经过请求产品id来从apple服务器获取虚拟物品信息的。
这一步必须设置,否则是没法从apple获取虚拟产品信息。
设置成功后以下所示:
更多关于iTunes Connet的操做请才看这篇博文http://openfibers.github.io/blog/2015/02/28/in-app-purchase-walk-through/
完成了上面的准备工做,咱们就能够开始着手IAP的代码实现了。
咱们假设你已经完成了从后台服务器获取虚拟物品列表这一步操做了,这一步后台服务器还会返回每一个虚拟物品所对应的productionIdentifier,假设你也获取到了,并保存在属性self.productIdent中。
须要在工程中引入 storekit.framework。
咱们来看看后续如何实现IAP
//移除监听 -(void)dealloc { [[SKPaymentQueue defaultQueue] removeTransactionObserver:self]; } //添加监听 - (void)viewDidLoad{ [super viewDidLoad]; [self.tableView.mj_header beginRefreshing]; [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; } - (void)buyProdution:(UIButton *)sender{ if ([SKPaymentQueue canMakePayments]) { [self getProductInfo:self.productIdent]; } else { [self showMessage:@"用户禁止应用内付费购买"]; } }
若是用户容许IAP,那么就能够发起购买操做了
//从Apple查询用户点击购买的产品的信息 - (void)getProductInfo:(NSString *)productIdentifier { NSArray *product = [[NSArray alloc] initWithObjects:productIdentifier, nil]; NSSet *set = [NSSet setWithArray:product]; SKProductsRequest * request = [[SKProductsRequest alloc] initWithProductIdentifiers:set]; request.delegate = self; [request start]; [self showMessageManualHide:@"正在购买,请稍后"]; } // 查询成功后的回调 - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { [self hideHUD]; NSArray *myProduct = response.products; if (myProduct.count == 0) { [self showMessage:@"没法获取产品信息,请重试"]; return; } SKPayment * payment = [SKPayment paymentWithProduct:myProduct[0]]; [[SKPaymentQueue defaultQueue] addPayment:payment]; } //查询失败后的回调 - (void)request:(SKRequest *)request didFailWithError:(NSError *)error { [self hideHUD]; [self showMessage:[error localizedDescription]]; }
//购买操做后的回调 - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { [self hideHUD]; for (SKPaymentTransaction *transaction in transactions) { switch (transaction.transactionState) { case SKPaymentTransactionStatePurchased://交易完成 self.receipt = [GTMBase64 stringByEncodingData:[NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]]]; [self checkReceiptIsValid];//把self.receipt发送到服务器验证是否有效 [self completeTransaction:transaction]; break; case SKPaymentTransactionStateFailed://交易失败 [self failedTransaction:transaction]; break; case SKPaymentTransactionStateRestored://已经购买过该商品 [self showMessage:@"恢复购买成功"]; [self restoreTransaction:transaction]; break; case SKPaymentTransactionStatePurchasing://商品添加进列表 [self showMessage:@"正在请求付费信息,请稍后"]; break; default: break; } } } - (void)completeTransaction:(SKPaymentTransaction *)transaction { [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; } - (void)failedTransaction:(SKPaymentTransaction *)transaction { if(transaction.error.code != SKErrorPaymentCancelled) { UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:nil message:@"购买失败,请重试"delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"重试", nil]; [alertView show]; } else { [self showMessage:@"用户取消交易"]; } [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; } - (void)restoreTransaction:(SKPaymentTransaction *)transaction { [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; }
在这一步咱们须要向服务器验证Apple服务器返回的购买凭证的有效性,而后把验证结果通知用户
- (void)checkReceiptIsValid{ AFHTTPSessionManager manager]GET:@"后台服务器地址" parameters::@"发送的参数(必须包括购买凭证)" success:^(NSURLSessionDataTask * _Nonnull task, id _Nonnull responseObject) { if(凭证有效){ 你要作的事 }else{//凭证无效 你要作的事 } } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:nil message:@"购买失败,请重试"delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"重试", nil]; [alertView show]; } }
若是出现网络问题,致使没法验证。咱们须要持久化保存购买凭证,在用户下次启动APP的时候在后台向服务器再一次发起验证,直到成功而后移除该凭证。
保证以下define可在全局访问:
#define AppStoreInfoLocalFilePath [NSString stringWithFormat:@"%@/%@/", [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject],@"EACEF35FE363A75A"]
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { if (buttonIndex == 0) { [self saveReceipt]; } else { [self checkReceiptIsValid]; } } //持久化存储用户购买凭证(这里最好还要存储当前日期,用户id等信息,用于区分不一样的凭证) -(void)saveReceipt{ NSString *fileName = [AppUtils getUUIDString]; NSString *savedPath = [NSString stringWithFormat:@"%@%@.plist", AppStoreInfoLocalFilePath, fileName]; NSDictionary *dic =[ NSDictionary dictionaryWithObjectsAndKeys: self.receipt, Request_transactionReceipt, self.date DATE self.userId USERID nil]; [dic writeToFile:savedPath atomically:YES]; }
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{ NSFileManager *fileManager = [NSFileManager defaultManager]; //从服务器验证receipt失败以后,在程序再次启动的时候,使用保存的receipt再次到服务器验证 if (![fileManager fileExistsAtPath:AppStoreInfoLocalFilePath]) {//若是在改路下不存在文件,说明就没有保存验证失败后的购买凭证,也就是说发送凭证成功。 [fileManager createDirectoryAtPath:AppStoreInfoLocalFilePath//建立目录 withIntermediateDirectories:YES attributes:nil error:nil]; } else//存在购买凭证,说明发送凭证失败,再次发起验证 { [self sendFailedIapFiles]; } } //验证receipt失败,App启动后再次验证 - (void)sendFailedIapFiles{ NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *error = nil; //搜索该目录下的全部文件和目录 NSArray *cacheFileNameArray = [fileManager contentsOfDirectoryAtPath:AppStoreInfoLocalFilePath error:&error]; if (error == nil) { for (NSString *name in cacheFileNameArray) { if ([name hasSuffix:@".plist"])//若是有plist后缀的文件,说明就是存储的购买凭证 { NSString *filePath = [NSString stringWithFormat:@"%@/%@", AppStoreInfoLocalFilePath, name]; [self sendAppStoreRequestBuyPlist:filePath]; } } } else { DebugLog(@"AppStoreInfoLocalFilePath error:%@", [error domain]); } } -(void)sendAppStoreRequestBuyPlist:(NSString *)plistPath { NSString *path = [NSString stringWithFormat:@"%@%@", AppStoreInfoLocalFilePath, plistPath]; NSDictionary *dic = [NSDictionary dictionaryWithContentsOfFile:path]; //这里的参数请根据本身公司后台服务器接口定制,可是必须发送的是持久化保存购买凭证 NSMutableDictionary *params = [NSMutableDictionary dictionaryWithObjectsAndKeys: [dic objectForKey:USERID], USERID, [dic objectForKey:DATE], DATE, [dic objectForKey:Request_transactionReceipt], Request_transactionReceipt, nil]; AFHTTPSessionManager manager]GET:@"后台服务器地址" parameters:params success:^(NSURLSessionDataTask * _Nonnull task, id _Nonnull responseObject) { if(凭证有效){ [self removeReceipt] }else{//凭证无效 你要作的事 } } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { } } //验证成功就从plist中移除凭证 -(void)removeReceipt{ [AppUtils removeIapFailedPath:AppStoreInfoLocalFilePath]; } //AppUtils类方法,验证成功,移除存储的receipt + (void)removeIapFailedPath:(NSString *)plistPath{ NSString *path = [NSString stringWithFormat:@"%@/%@", AppStoreInfoLocalFilePath, plistPath]; NSFileManager *fileManager = [NSFileManager defaultManager]; if ([fileManager fileExistsAtPath:AppStoreInfoLocalFilePath]) { [fileManager removeItemAtPath:AppStoreInfoLocalFilePath error:nil]; } if ([fileManager fileExistsAtPath:path]) { [fileManager removeItemAtPath:path error:nil]; } }
至此,整个流程结束,有任何疑问欢迎你们留言