九月份的时候来到公司,开始制作一个游戏的sdk,包括了登录注册,初始化,信息收集等功能... 这些相对来说简单些,最主要的是内购.为什么要做内购呢?
游戏里面包含了虚拟商品(金币,药水等等...)
我们要进行虚拟商品的购买,就必须要通过苹果的内购简称IAP(In-App Purchase)
在内购之前我也查询了一些资料:
iOS内购实现及测试Check List
[iOS]应用内支付(内购)的个人开发过程及坑!
iOS证书说明和发布内购流程整理
真·iOS内购的完整流程
当然还有官方文档.
开始内购前需要做准备:
- 要有开发者账号,开通内购功能
- 税务信息(公司市场部的搞定的)
- 在itunes connect添加应用,并在应用下面添加商品(我们老大添加的)
- 创建沙箱测试账号
我开发前老大给了我如下几样东西:
- itunes connect上注册的商品id
- 应用对应的bundle id
- 沙箱测试的账号和密码
接下来我们就开始开发了:
首先我们要认识这几个类
SKProductSKProduct
对象提供有关您在iTunes Connect中注册的产品的信息。官方文档解释如下
SKProduct objects are returned as part of an SKProductsResponse object. Each product object provides information about a product you previously registered in iTunes Connect.
SKPaymentSKPayment
类定义了一个支付请求, 支付封装了标识特定产品的字符串以及用户想要购买的那些项目的数量。官方文档解释如下
The SKPayment class defines a request to the Apple App Store to process payment for additional functionality offered by your application. A payment encapsulates a string that identifies a particular product and the quantity of those items the user would like to purchase.
SKProductsRequest
SKProductsRequest对象用于从Apple App Store获得产品信息的。官方文档解释如下
An SKProductsRequest object is used to retrieve localized information about a list of products from the Apple App Store. Your application uses this request to present localized prices and other information to the user without having to maintain that list itself.
SKProductsResponse
SKProductsResponse对象存储了从Apple App Store获取得产品信息。官方文档解释如下
An SKProductsResponse object is returned by the Apple App Store in response to a request for information about a list of products.
SKPaymentTransaction
SKPaymentTransaction类定义驻留在支付队列中的支付交易对象。 每当付款被添加到支付队列时,创建这个支付交易对象。 当App Store处理完付款后,交易就会传送到您的应用程式。 完成的交易提供收据和交易标识符,您的应用程序可以使用该标识来保存已处理付款的永久记录。(谷歌翻译,觉得这个解释的挺好)
The SKPaymentTransaction class defines objects residing in the payment queue. A payment transaction is created whenever a payment is added to the payment queue. Transactions are delivered to your application when the App Store has finished processing the payment. Completed transactions provide a receipt and transaction identifier that your application can use to save a permanent record of the processed payment.
上代码之前先说一下我的逻辑:
- 先添加一个遮罩层,防止连续多次点击(因为内购的反应真的慢)
- 请求商品列表(我没有将所有的商品请求下来,我是买哪个就请求哪个),如果失败就提示用户,并移除遮罩层,如果成功就进行下一步
- 得到商品后调用下单接口(我们公司服务器的下单接口,因为要统计),若失败取消购买,并移除遮罩层,如果成功,将部分订单信息本地化(根据公司的要求),进行下一步
- 将订单信息添加到支付队列,添加支付监听,若失败移除遮罩层,如果成功就更新本地存储的数据,进行下一步
- 验签,无论成功失败,更新本地数据,做响应的处理(根据各个公司的情况),移除遮罩层,移除支付监听
接下来我们就要上代码了
//1-添加遮罩层,我直接把内购功能写成了一个控制器,每次调用内购的时候我就将这个控制器设置为子控制器,并将它的view添加视图上
DYStoreKitController *paymentVC = [[DYStoreKitController alloc] initWithOrder:order];
[viewController.view addSubview:paymentVC.view];
[viewController addChildViewController:paymentVC];
//通过这个方法开始内购的流程
[paymentVC startObserverAndProductRequest];
//2-请求商品列表
- (void)startObserverAndProductRequest {
if ([SKPaymentQueue canMakePayments]) {
//开始监听
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
//请求支付列表
NSSet *IDSet = [NSSet setWithArray:proIDs];
SKProductsRequest *productRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:IDSet];
productRequest.delegate = self;
[productRequest start];
}
else {
[DYHudTool showText:@"用户未授权内购" hideDelayAfter:1.0];
[self payFinishWithTrinsaction:nil state:@"用户未授权"];
}
}
//这个是用来处理支付结束的,封装好了方便调用
- (void)payFinishWithTrinsaction:(SKPaymentTransaction *)transaction state:(NSString *)stateString {
//如果传了这个参数就完成这个订单的支付
if (transaction) {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
//如果有这个数据就执行代理方法回调这个字符串,因为我做的是SDK,需要把结果回调个调用方,故有如下操作
if (stateString) {
if ([self.delegate respondsToSelector:@selector(paymentHandler:)]) {
[self.delegate paymentHandler:stateString];
}
}
//移除控制器和它的view
[self.view removeFromSuperview];
[self removeFromParentViewController];
}
#pragma mark - SKProductsRequestDelegate控制器要遵守这个协议
//请求商品成功的返回结果
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
//NSLog(@"products = %@",response.products);
//得到产品
self.products = response.products;
//如果有产品就开始下单并添加到支付队列
if (self.products.count > 0) {
[self addPaymentToPamentQueue];
}
//没有产品就结束
else {
[self payFinishWithTrinsaction:nil state:nil];
}
//无效的商品id处理
for (NSString *invalidProductId in response.invalidProductIdentifiers)
{
//无效的invalidProductId
}
}
//请求失败的时候
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
//结束
[self payFinishWithTrinsaction:nil state:@"requestFailed"];
}
//3-请求成功后就调用下单接口
- (void)addPaymentToPamentQueue {
for (SKProduct *product in self.products) {
//调服务器的下单接口
[self orderWithProduct:product Handler:^(NSString *orderID) {
//下单成功回调,将需要存储的数据存到本地,根据不同的需求处理,这里就不上代码了
//根据产品城建支付并添加到支付队列
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}];
}
}
- (void)orderWithProduct:(SKProduct *)product Handler:(void(^)(NSString *orderID))handler {
NSLog(@"开始下单");
NSDictionary *parameters = @{
//参数
};
[[DYNetworingManager sharedManager] requestWithType:RequestTypePost URLString:URL_CREATER_ORDER parameters:parameters progress:nil success:^(NSURLSessionDataTask *task, id response) {
int status = [[response objectForKey:@"status"] intValue];
//如果status等于1的时候请求成功
//若成功就回调
if (status == 1) {
NSLog(@"下单成功");
NSDictionary *dataDic = [response objectForKey:@"data"];
NSString *orderid = [dataDic objectForKey:@"orderid"];
//拿到orderid回调
handler(orderid);
}
//失败就结束
else {
[self payFinishWithTrinsaction:nil state:@"orderFailed"];
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
//网络出错也结束
[self payFinishWithTrinsaction:nil state:@"netFailed"];
}];
}
//4-监听支付结果
#pragma mark - 内购状态回调 要遵守SKPaymentTransactionObserver协议
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
NSLog(@"transactions回调来了");
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchased: {
//更新本地数据 这里要写更新本地数据的代码
// 发送到苹果服务器验证凭证
[self verifyPurchaseWithPaymentTransaction:transaction];
}
break;
case SKPaymentTransactionStateFailed: {
//失败结束
[self payFinishWithTrinsaction:transaction state:@"支付失败"];
}
break;
case SKPaymentTransactionStateRestored: {
[self payFinishWithTrinsaction:transaction state:@"恢复已经购买的商品"];
}
break;
case SKPaymentTransactionStatePurchasing: {
//商品添加进购买队列
}
break;
default: {
}
break;
}
}
}
//5-验证购买,避免越狱软件模拟苹果请求达到非法购买问题
-(void)verifyPurchaseWithPaymentTransaction:(SKPaymentTransaction *)transaction{
//从沙盒中获取交易凭证并且拼接成请求体数据
NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];
NSData *transactionReceipt=[NSData dataWithContentsOfURL:receiptUrl];
//将数据进行base64编码,这个方法是从别地方粘贴过来的
NSDictionary *requestContents = @{
@"receipt-data": [self encode:(uint8_t *)transactionReceipt.bytes length:transactionReceipt.length]};
//将数据转换为json格式
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents options:0 error:nil];
//再转换为字符串,来发送请求
NSString *dataString = [[NSString alloc] initWithData:requestData encoding:NSUTF8StringEncoding];
NSString *transid = transaction.transactionIdentifier;
NSDictionary *parms = @{
@"dataorg":dataString ? dataString : @"",//验签数据,刚处理好的
@"transid":transid ? transid : @"",//transid从transaction中获取的
};
[[DYNetworingManager sharedManager] requestParm:parms success:^(id response) {
NSLog(@"验签response = %@",response);
int status = [[response objectForKey:@"status"] intValue];
if (status == 1) {
//更新本地数据
//结束交易
[self payFinishWithTrinsaction:transaction state:@"支付成功"];
}
else {
//更新本地数据
//结束交易
[self payFinishWithTrinsaction:transaction state:@"验签失败,可能是非法的途径,可能是越狱的手机"];
}
} failed:^(NSError *error) {
//网络问题也要结束
[self payFinishWithTrinsaction:nil state:nil];
}];
}
//Base64编码
- (NSString *)encode:(const uint8_t *)input length:(NSInteger)length {
static char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
NSMutableData *data = [NSMutableData dataWithLength:((length + 2) / 3) * 4];
uint8_t *output = (uint8_t *)data.mutableBytes;
for (NSInteger i = 0; i < length; i += 3) {
NSInteger value = 0;
for (NSInteger j = i; j < (i + 3); j++) {
value <<= 8;
if (j < length) {
value |= (0xFF & input[j]);
}
}
NSInteger index = (i / 3) * 4;
output[index + 0] = table[(value >> 18) & 0x3F];
output[index + 1] = table[(value >> 12) & 0x3F];
output[index + 2] = (i + 1) < length ? table[(value >> 6) & 0x3F] : '=';
output[index + 3] = (i + 2) < length ? table[(value >> 0) & 0x3F] : '=';
}
return [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
}
- (void)dealloc {
//控制器销毁的时候要移除监听
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
代码基本如上,本地数据部分没有写,这个需要根据不用的需求来做,就没有附上去
遇到的问题说一下吧
- 验签地址问题验签有两个地址
//沙盒测试环境验证
#define SANDBOX @"https://sandbox.itunes.apple.com/verifyReceipt"
//正式环境验证
#define AppStore @"https://buy.itunes.apple.com/verifyReceipt"
无论如何都先到正式的环境验证,如果返回的是21007,那么说明这是一个沙盒测试,再去沙盒测试环境去验证,避免了代码的修改,方便,上线后也是这样.
- 验签数据的问题
//从沙盒中获取交易凭证并且拼接成请求体数据
NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];
NSData *transactionReceipt=[NSData dataWithContentsOfURL:receiptUrl];
//将数据进行base64编码,这个方法是从别地方粘贴过来的
NSDictionary *requestContents = @{
@"receipt-data": [self encode:(uint8_t *)transactionReceipt.bytes length:transactionReceipt.length]};
//将数据转换为json格式
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents options:0 error:nil];
//再转换为字符串,来发送请求
NSString *dataString = [[NSString alloc] initWithData:requestData encoding:NSUTF8StringEncoding];
从沙盒中获取数据后,一定要将其先进行base64编码后,再转换为json格式字符串再来验证,验证可以在客户端,也可以在服务端,我们是在服务端验证,所以我只要把处理好的数据发给服务端就好了
- 付过钱后没有网络了,导致没有验签怎么办
这种情况是一个比较少见的情况,但是还是要考虑,这种情况会导致用户付过钱了但是没有收到商品首先先说明一下内购的一个机制:
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
每一个支付,我只要不执行上面这句代码,这个支付就不会被完成,也就是说系统会一直给你发消息通过下面这个方法:
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
当然前提是你添加了监听:
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
而且并没有被移除这样就算我们这次交易成功付过款了,但是没有验签,这个时候我们是没有执行下面这句代码的,
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
也就是说,当我们下次再开启支付的时候,添加了监听的话,这个订单还会去验签,这样我们就可以确保没问题了(这样有一个缺点,你这个支付在队列里,什么时候来验签就是一个问题了,即时性不好)
但是这样还有一个问题:
只有在我再次进行交易的时候才会去验签,如果我一直不去交易怎么办,或者玩家想,我给钱了,又不给我东西,再也不买了怎么办.
针对这一个问题,我们做了一个处理就是手动验签,每次当我处理过验签数据的时候,现将数据根据订单存储到本地,在去调用验签接口我们有一个支付记录的列表,上面显示了各条订单的支付状态,当状态是支付过了还没有验签的时候,就可以点击进行手动验签,手动验签的时候,还是调用原来的验签接口,但是数据都是从本地来拿,这样就解决了这个问题.
但是这个时候我还是没有办法去调用
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
来结束这个交易,也就是说,下次交易的时候还会验签一遍,但是验签两次没有什么影响
写了半天,有问题的地方还望指出,继续改进