iOS - 内购IAP

内购

内购的概念

  • IAP,即in-App Purchase,是一种智能移动终端应用程序付费的模式,在苹果(Apple)iOS、谷歌安卓(Google Android)、微软WindowsPhone等智能移动终端操作系统中都有相应的实现。-- 百度百科

  • In-App Purchase:

    Offer users additional content and services through purchases made within your app.

账户请求流程

App store connect地址:https://appstoreconnect.apple.com/login

  1. 注册app (新建App)


    新建App.png
  1. 填写协议、税务和银行业务
填写协议、税务和银行业务.png
  1. 配置内购产品ID


    配置内购产品ID.png

    根据产品类型来添加:


    产品类型.png
  2. 添加沙盒测试员


    添加沙盒测试员入口.png

    添加沙盒测试员.png

    添加测试员就好了。此时注册的AppleID是一个虚拟的AppleID

  • 沙盒账号是什么

iOS应用里面用到了苹果应用内付费(IAP)功能,在项目上线前一定要进行功能测试。测试肯定是需要的,何况这个跟money有关。。。开发完成了之后,如何进行测试呢?难道我测试个内购功能要自己掏钱?就算是公司掏钱,但是苹果要吃掉3成的啊,想想如果是99刀的商品,点下购买的时候心里都有点发慌。。。
苹果当然没这么坑了,测试内购,苹果提供了沙盒账号(也叫沙箱账号)的方式。这个沙箱账号其实是虚拟的AppleID,在开发者账号后台的iTune Connect上配置了之后就能使用沙盒账号测试内购,有了沙盒账号,就能体验一把土豪的感觉了,游戏钻石什么的随便充,反正不用我的钱。

App代码集成

  1. 获取商品信息
#pragma mark - 获取商品ID成功的代理方法

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    //返回的是SKProduct对象数组
    //如果你上面请求的是多个,那么这里返回的也是多个
    if (response) {
        SKProduct *product = [response.products firstObject];
        //查询成功,开始支付
        [self startPaymentWithProduct:product];
    }
}
  1. 拿到商品信息,创建支付对象
#pragma mark -- 拿到商品信息,创建支付对象
- (void)startPaymentWithProduct:(SKProduct *)product {
    SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
    payment.applicationUsername = @"myOrderID";
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}
  1. 把交易添加到队列 (上面的addPayment:方法)

  2. 监听支付结果(paymentQueue:updatedTransactions:),如果支付成功,Apple会把支付成功的凭证(recipt)存到沙盒中

#pragma mark -  监听的代理方法
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
    [transactions enumerateObjectsUsingBlock:^(SKPaymentTransaction * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        SKPaymentTransaction *transation = obj;
        switch (transation.transactionState) {
            case SKPaymentTransactionStatePurchasing:
            {
                NSLog(@"购买中");
            }
                break;
            case SKPaymentTransactionStatePurchased:
            {
                NSLog(@"交易完成");
                //获取透传字段
                NSString *orderNo = transation.payment.applicationUsername;
                //transactionIdentifier:相当于Apple的订单号
                NSString *transationId = transation.transactionIdentifier;
                NSLog(@"orderNo = %@, 交易ID = %@", orderNo, transationId);
                //从沙盒中获取交易凭证
                NSData *reciptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
                //转化成Base64字符串(用于校验)             
                /*
                 //其作用是将生成的Base64字符串按照64个字符长度进行等分换行。
                 NSDataBase64Encoding64CharacterLineLength = 1UL << 0,
                 
                 //其作用是将生成的Base64字符串按照76个字符长度进行等分换行。
                 NSDataBase64Encoding76CharacterLineLength = 1UL << 1,
                 
                 //其作用是将生成的Base64字符串以回车结束。
                 NSDataBase64EncodingEndLineWithCarriageReturn = 1UL << 4,
                 
                 //其作用是将生成的Base64字符串以换行结束。
                 NSDataBase64EncodingEndLineWithLineFeed = 1UL << 5,
                 */              
                //NSString *reciptString = [reciptData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
                NSString *reciptString = [reciptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
                //NSLog(@"%@",reciptString);
                
                //传给后台做二次验证
                [self checkReceipt:reciptString];
                [[SKPaymentQueue defaultQueue] finishTransaction:transation];
            }
                break;
            case SKPaymentTransactionStateFailed:
            {
                //localizedDescription可以作为提示信息(交易失败无法连接到 iTunes Store)
                NSLog(@"交易失败%@", transation.error.localizedDescription);
                [[SKPaymentQueue defaultQueue] finishTransaction:transation];
            }
                break;
            case SKPaymentTransactionStateRestored:
            {
                NSLog(@"恢复购买完成");
                //恢复完成(对应restoreCompletedTransactions)方法
            }
                break;
            case SKPaymentTransactionStateDeferred:
            {
                NSLog(@"交易推迟, 等待外部操作");
                //交易推迟
                //官方解释是:交易已经加入队列,但是需要等待外部操作
                //主要用于儿童模式,需要询问家长同意。这种情况下不能关闭订单(完成交易),否则这类充值将无法处理。
            }
                break;
            default:
                break;
        }
    }];
}
  1. 我们从沙盒中取到凭证(recipt),发送给我们自己后台进行二次验证,验证成功表示支付成功.
#pragma mark -- 从沙盒中取到凭证,发送给我们自己后台进行二次验证,验证成功表示支付成功
- (void)checkReceipt:(NSString *)receipt {
    //NSLog(@"%@",receipt);
    // 拼接请求数据
    NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\":\"%@\"}", receipt];
    NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
    
    //1.创建会话对象
    NSURLSession *session = [NSURLSession sharedSession];
    //2.根据会话对象创建task
    NSURL *url = [NSURL URLWithString:@"https://sandbox.itunes.apple.com/verifyReceipt"];
    
    //3.创建可变的请求对象
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    
    //4.修改请求方法为POST
    request.HTTPMethod = @"POST";
    
    //5.设置请求体
    request.HTTPBody = bodyData;
    
    //6.根据会话对象创建一个Task(发送请求)
    /*
     第一个参数:请求对象
     第二个参数:completionHandler回调(请求完成【成功|失败】的回调)
     data:响应体信息(期望的数据)
     response:响应头信息,主要是对服务器端的描述
     error:错误信息,如果请求失败,则error有值
     */
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        
        //8.解析数据
        //NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
        NSLog(@"%@",[NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil]);
        
    }];
    
    //7.执行任务
    [dataTask resume];
}
  • 验证凭证时可能失败:凭证有可能丢失
  • 凭证校验出错可以使用SKReceiptRefreshRequest刷新,阅读SKReceiptRefreshRequest官方文档。
- (void)refreshReceiptData
{
    self.receiptRefreshRequest = [[SKReceiptRefreshRequest alloc] init];
    self.receiptRefreshRequest.delegate = self;
    [self.receiptRefreshRequest start];
}
  • 凭证校验地址:

开发环境: https://sandbox.itunes.apple.com/verifyReceipt
生产环境: https://buy.itunes.apple.com/verifyReceipt

  • 凭证校验异常 code 参照码:

内购验证凭据返回结果状态码说明(status 状态)
21000 App Store无法读取你提供的JSON数据
21002 收据数据不符合格式
21003 收据无法被验证
21004 你提供的共享密钥和账户的共享密钥不一致
21005 收据服务器当前不可用
21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中
21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证
21008 收据信息是产品环境中使用,但却被发送到测试环境中验证

demo完整代码

#import "ViewController.h"
#import 
@interface ViewController () 
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    UIButton *btn1 = [[UIButton alloc]initWithFrame:CGRectMake(100, 50, 100, 100)];
    [btn1 setTitle:@"60游戏币" forState:UIControlStateNormal];
    [btn1 addTarget:self action:@selector(clickBtn1) forControlEvents:UIControlEventTouchUpInside];
    btn1.backgroundColor = [UIColor blueColor];
    [self.view addSubview:btn1];
    
    // Do any additional setup after loading the view, typically from a nib.
    UIButton *btn2 = [[UIButton alloc]initWithFrame:CGRectMake(100, 180, 100, 100)];
    [btn2 setTitle:@"120游戏币" forState:UIControlStateNormal];
    [btn2 addTarget:self action:@selector(clickBtn2) forControlEvents:UIControlEventTouchUpInside];
    btn2.backgroundColor = [UIColor blueColor];
    [self.view addSubview:btn2];
    
    
}

- (void)requestDidFinish:(SKRequest *)request {
    NSLog(@"requestDidFinish");
    //从沙盒中获取交易凭证
    NSData *reciptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
    NSString *reciptString = [reciptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
    [self checkReceipt:reciptString];
}

-(void)clickBtn1 {
    NSSet *sets = [NSSet setWithObjects:@"dollar1", nil];
    SKProductsRequest *productrequest = [[SKProductsRequest alloc] initWithProductIdentifiers:sets];
    productrequest.delegate = self;
    [productrequest start];
    //首先我们要在 viewDidLoad 方法中添加监听对象
    
}
-(void)clickBtn2 {
    NSSet *sets = [NSSet setWithObjects:@"dollar2", nil];
    SKProductsRequest *productrequest = [[SKProductsRequest alloc] initWithProductIdentifiers:sets];
    productrequest.delegate = self;
    [productrequest start];
    //首先我们要在 viewDidLoad 方法中添加监听对象
    
}

#pragma mark - 获取商品ID成功的代理方法
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    //返回的是SKProduct对象数组
    //如果你上面请求的是多个,那么这里返回的也是多个
    if (response) {
        SKProduct *product = [response.products firstObject];
        //查询成功,开始支付
        [self startPaymentWithProduct:product];
    } 
}

#pragma mark -- 拿到商品信息,创建支付对象
- (void)startPaymentWithProduct:(SKProduct *)product {
    SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
    payment.applicationUsername = @"myOrderID";
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}

#pragma mark -  监听的代理方法
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
    [transactions enumerateObjectsUsingBlock:^(SKPaymentTransaction * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        SKPaymentTransaction *transation = obj;
        switch (transation.transactionState) {
            case SKPaymentTransactionStatePurchasing:
            {
                NSLog(@"购买中");
            }
                break;
            case SKPaymentTransactionStatePurchased:
            {
                NSLog(@"交易完成");
                //获取透传字段
                NSString *orderNo = transation.payment.applicationUsername;
                //transactionIdentifier:相当于Apple的订单号
                NSString *transationId = transation.transactionIdentifier;
                NSLog(@"orderNo = %@, 交易ID = %@", orderNo, transationId);
                //从沙盒中获取交易凭证
                NSData *reciptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
                //转化成Base64字符串(用于校验)            
                /*
                 //其作用是将生成的Base64字符串按照64个字符长度进行等分换行。
                 NSDataBase64Encoding64CharacterLineLength = 1UL << 0,               
                 //其作用是将生成的Base64字符串按照76个字符长度进行等分换行。
                 NSDataBase64Encoding76CharacterLineLength = 1UL << 1,                
                 //其作用是将生成的Base64字符串以回车结束。
                 NSDataBase64EncodingEndLineWithCarriageReturn = 1UL << 4,                 
                 //其作用是将生成的Base64字符串以换行结束。
                 NSDataBase64EncodingEndLineWithLineFeed = 1UL << 5,
                 */              
                //NSString *reciptString = [reciptData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
                NSString *reciptString = [reciptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
                //NSLog(@"%@",reciptString);
                //传给后台做二次验证
                [self checkReceipt:reciptString];
                [[SKPaymentQueue defaultQueue] finishTransaction:transation];
            }
                break;
            case SKPaymentTransactionStateFailed:
            {
                //localizedDescription可以作为提示信息(交易失败无法连接到 iTunes Store)
                NSLog(@"交易失败%@", transation.error.localizedDescription);
                [[SKPaymentQueue defaultQueue] finishTransaction:transation];
            }
                break;
            case SKPaymentTransactionStateRestored:
            {
                NSLog(@"恢复购买完成");
                //恢复完成(对应restoreCompletedTransactions)方法
            }
                break;
            case SKPaymentTransactionStateDeferred:
            {
                NSLog(@"交易推迟, 等待外部操作");
                //交易推迟
                //官方解释是:交易已经加入队列,但是需要等待外部操作
                //主要用于儿童模式,需要询问家长同意。这种情况下不能关闭订单(完成交易),否则这类充值将无法处理。
            }
                break;
            default:
                break;
        }
    }];
}

#pragma mark -- 从沙盒中取到凭证,发送给我们自己后台进行二次验证,验证成功表示支付成功
- (void)checkReceipt:(NSString *)receipt {
    //NSLog(@"%@",receipt);
    // 拼接请求数据
    NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\":\"%@\"}", receipt];
    NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
    
    //1.创建会话对象
    NSURLSession *session = [NSURLSession sharedSession];
    //2.根据会话对象创建task
    NSURL *url = [NSURL URLWithString:@"https://sandbox.itunes.apple.com/verifyReceipt"];    
    //3.创建可变的请求对象
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];    
    //4.修改请求方法为POST
    request.HTTPMethod = @"POST";   
    //5.设置请求体
    request.HTTPBody = bodyData;    
    //6.根据会话对象创建一个Task(发送请求)
    /*
     第一个参数:请求对象
     第二个参数:completionHandler回调(请求完成【成功|失败】的回调)
     data:响应体信息(期望的数据)
     response:响应头信息,主要是对服务器端的描述
     error:错误信息,如果请求失败,则error有值
     */
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {        
        //8.解析数据
        //NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
        NSLog(@"%@",[NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil]);       
    }];  
    //7.执行任务
    [dataTask resume];
}
@end

参考文章:

  1. iOS开发支付篇——内购(IAP)详解
  2. IOS内购(IAP)的那些事
  3. 【iOS】苹果IAP(内购)中沙盒账号使用注意事项

你可能感兴趣的:(iOS - 内购IAP)