iOS 苹果内购

2020-03-13  本文已影响0人  XieHenry

公司性质乃是少儿英语在线教育培训。app内需要上课。因此苹果规定需要集成内部购买功能,做之前一直不了解怎么形成一个好的闭环。参阅网上多个博客以及自己的思考。记录一下,希望对大家有一个帮助,以及对自己的总结。

本文章的重点问题是:

1.iTunes Connect的配置(网上自行搜索,很详细)
2.常用的购买代码
3.漏单问题处理
4.后台交互

2.常用的购买代码

首选需要引入头文件以及添加代理。

#import <StoreKit/StoreKit.h>
<SKProductsRequestDelegate,SKPaymentTransactionObserver>
#define AppStore_BuyClassHours @"appstore_buyclasshours"  //购买课时

1.在viewDidLoad内需要添加监听。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"购买课时";

    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}

2.通过购买事件触发 购买。

-(void)buyHourClassClick {
    if([SKPaymentQueue canMakePayments]){
        [self requestProductData:self.currentProId];   //self.currentProId是当前购买产品的 产品Id(在itunes connect配置的)
    } else { 
        UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"内购未开启" message:@"进入【设置】-【屏幕使用时间】-【内容和隐私访问限制】-【iTunes Store 与 App Store 购买项目】-【App内购买项目】- 选择“允许”,将该功能开启" preferredStyle:UIAlertControllerStyleAlert];
        
        UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
            [self dismissViewControllerAnimated:YES completion:nil];
        }];
        [alert addAction:defaultAction];
        [self presentViewController:alert animated:YES completion:nil];
    }
}

3.客户端向苹果发起请求,获取内购的商品

- (void)requestProductData:(NSString *)type{
    dispatch_async(dispatch_get_main_queue(), ^{
        [MBProgressHUD showLoadingAndMessage:@"开始请求商品列表" toView:self.view];
    });
    NSArray *product = [[NSArray alloc] initWithObjects:type,nil];
    NSSet *nsset = [NSSet setWithArray:product];
    SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
    request.delegate = self;
    [request start];
}

4.客户端获取全部商品成功,根据你选择的产品id去苹果服务器发起支付请求

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
    
    NSLog(@"--------------收到产品反馈消息---------------------");
    NSArray *product = response.products;
    if([product count] == 0){
        dispatch_async(dispatch_get_main_queue(), ^{
            [MBProgressHUD hideHUDForView:self.view];
        });
        NSLog(@"--------------没有商品------------------");
        return;
    }
    
    dispatch_async(dispatch_get_main_queue(), ^{
        [MBProgressHUD showLoadingAndMessage:@"商品添加进列表" toView:self.view];
    });
    for (SKProduct *pro in product) {
        NSLog(@"SKProduct 描述信息:%@", [pro description]);
        NSLog(@"localizedTitle 产品标题:%@", [pro localizedTitle]);
        NSLog(@"localizedDescription 产品描述信息:%@", [pro localizedDescription]);
        NSLog(@"price 价格:%@", [pro price]);
        NSLog(@"productIdentifier Product id:%@", [pro productIdentifier]);
        
        if([pro.productIdentifier isEqualToString:_currentProId]){
            // 1.创建票据
            SKPayment *skpayment = [SKPayment paymentWithProduct:pro];
            // 2.将票据加入到交易队列
            [[SKPaymentQueue defaultQueue] addPayment:skpayment];
        }
    }
}

5.监听购买结果

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
    /*
     SKPaymentTransactionStatePurchasing,    正在购买
     SKPaymentTransactionStatePurchased,     已经购买
     SKPaymentTransactionStateFailed,        购买失败
     SKPaymentTransactionStateRestored,      回复购买中
     SKPaymentTransactionStateDeferred       交易还在队列里面,但最终状态还没有决定
     */
    
    
    for(SKPaymentTransaction *tran in transactions) {
        
        switch (tran.transactionState) {
            case SKPaymentTransactionStatePurchasing: {
                NSLog(@"商品正在购买...");
                
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    [MBProgressHUD hideHUDForView:self.view];
                    [MBProgressHUD showLoadingAndMessage:@"商品正在购买..." toView:self.view];
                });
            }
                break;
                
            case SKPaymentTransactionStatePurchased:{
                if (!IsStrEmpty(self.orderIDStr)) {
                    NSLog(@"购买成功之后才保存订单id,否则有订单,没购买成功,也会在首页弹窗");
                    
                    //保存凭证--自定义操作
                    NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
                    NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];
                    /*1.传输的是BASE64编码的字符串
                     2.BASE64常用的编码方案,通常用于数据传输,以及加密算法的基础算法,传输过程中能够保证数据传输的稳定性
                     3.BASE64是可以编码和解码的
                     */
                    NSString *receiptString = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
                    
                    NSDictionary *buyClassHourSaveDic = @{@"orderID":self.orderIDStr,@"appleReceipt":receiptString};
                    [UserDefaults() setObject:buyClassHourSaveDic forKey:AppStore_BuyClassHours];
                    [UserDefaults() synchronize];
                }
                
                [MBProgressHUD hideHUDForView:self.view];
                [self verifyPurchaseWithPaymentTransaction:tran];
            }
                
                break;
                
            case SKPaymentTransactionStateRestored:{
                NSLog(@"已经购买过商品");
                [MBProgressHUD showMessage:@"已经购买过商品" toView:self.view];
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];
            }
                break;
            case SKPaymentTransactionStateFailed:{//只有自己取消购买,才会弹窗。
                NSLog(@"交易失败");
                [MBProgressHUD hideHUDForView:self.view];

                if (!IsStrEmpty(self.orderIDStr)) {
                    [self buyErrorAlert:tran];
                } else {
                    [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                    [MBProgressHUD showMessage:@"交易失败" toView:self.view];
                }
                
            }
                break;
            case SKPaymentTransactionStateDeferred://交易延迟
                break;
            default:
                break;
        }
    }
}
-(void)buyErrorAlert:(SKPaymentTransaction *)transaction {
    
    self.alert = [UIAlertController alertControllerWithTitle:@"交易提醒" message:@"支付遇到问题?别着急,一键获取支付帮助" preferredStyle:UIAlertControllerStyleAlert];
    
    UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:@"取消支付" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        //置空。
        [UserDefaults() setObject:@{} forKey:AppStore_BuyClassHours];
        [UserDefaults() synchronize];
        [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
        
        [self dismissViewControllerAnimated:YES completion:nil];
    }];
    [self.alert addAction:defaultAction];
    
    UIAlertAction *successAction = [UIAlertAction actionWithTitle:@"继续支付" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
      
        [self dismissViewControllerAnimated:YES completion:nil];
        
        [self buyHourClassClick];
    }];
    [self.alert addAction:successAction];
    
    [self presentViewController:self.alert animated:YES completion:nil];
}
//请求失败
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
    [MBProgressHUD showMessage:@"支付失败" toView:self.view];
    NSLog(@"------------------错误-----------------:%@", error);
}
- (void)requestDidFinish:(SKRequest *)request{
    dispatch_async(dispatch_get_main_queue(), ^{
        [MBProgressHUD hideHUDForView:self.view];
    });
    NSLog(@"------------反馈信息结束-----------------");
}
//交易结束
- (void)completeTransaction:(SKPaymentTransaction *)transaction{
    NSLog(@"交易结束");
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}


- (void)dealloc{
    NSLog(@"控制器--%@--销毁了", [self class]);
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}

6.购买成功后的二次验证

//MARK:2.2支付成功后,苹果返回给你一个receipt收据,收据包含你这次交易的全部信息,产品id,交易号,时间等。
-(void)verifyPurchaseWithPaymentTransaction:(SKPaymentTransaction *)transaction {
    if (self.alert) {
        [self dismissViewControllerAnimated:YES completion:nil];//进入之后,走了失败,会dissmiss
    }
    
    [MBProgressHUD showLoading:self.view];
    
    
    NSString *orderIdStr = @"";    //预下订单id
    NSString *receiptString = @""; //苹果返回的凭证
    NSDictionary *buyClassHourSaveDic = [UserDefaults() objectForKey:AppStore_BuyClassHours];
    if ([buyClassHourSaveDic isKindOfClass:[NSDictionary class]] && [buyClassHourSaveDic count] > 0) {
        orderIdStr = [NSString stringWithFormat:@"%@",[buyClassHourSaveDic objectForKey:@"orderID"]];
        receiptString = [NSString stringWithFormat:@"%@",[buyClassHourSaveDic objectForKey:@"appleReceipt"]];
    } else {
        NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
        NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];
        receiptString = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];//转化为base64字符串
        orderIdStr = self.orderIDStr;
    }
    
    
    
    if (!IsStrEmpty(receiptString) && !IsStrEmpty(orderIdStr)) {
        NSDictionary *postDic = @{@"appleReceipt":receiptString,@"AP":orderIdStr};
        
        [[BaseService share] sendPostRequestWithPath:URL_ApplePurchaseNotify parameters:postDic token:YES viewController:self showMBProgress:YES success:^(id responseObject) {
            [MBProgressHUD hideHUDForView:self.view];
            
            
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
            //购买成功之后,置空。并删除本地的凭证。 在首页对这个字段进行判断。如果不为空,证明有未完成的订单。
            [UserDefaults() setObject:@{} forKey:AppStore_BuyClassHours];
            [UserDefaults() synchronize];
            
            [self getProductData];
            
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                [MBProgressHUD showMessage:responseObject[@"msg"] toView:self.view];
            });
            
        } failure:^(NSError *error) {
            
            //其他状态置空---只要走这个接口,不管失败成功,后台都可以将这两个字段对应保存。
            [UserDefaults() setObject:@{} forKey:AppStore_BuyClassHours];
            [UserDefaults() synchronize];
            
            [MBProgressHUD hideHUDForView:self.view];
            
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
            
            [self getProductData];
            
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                [MBProgressHUD showMessage:error.userInfo[xc_returnMsg] toView:self.view];
            });
        }];
        
        
    } else { //如果为空,取消队列,重新购买、
        [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [MBProgressHUD showMessage:@"支付遇到问题,请联系客服" toView:self.view];
        });
    }
    
}


3.漏单问题处理

因为苹果是先扣款再进行交易验证,因此部分情况下会造成用户付款后,没有验证,造成未发道具给用户的情况。就造成了漏单问题。
我们是后台自己先做了一个预下订单,即代码中的self.orderIDStr。如果我购买成功,我就将这个【订单号和苹果购买凭证】保存到本地。二次验证成功,就置空。
1.如果苹果扣款成功,2次验证失败,可以在本页面根据自己的需要在6中操作。
2.如果苹果扣款成功,还未做验证就退出了app。可以在首页判断本地plist中AppStore_BuyClassHours是否为空,做一个弹窗,点击跳转到购买页,自动执行代理。然后二次验证。
3.如果苹果扣款成功,还未做验证就卸载了app。然后还换了手机或者重新安装了,就只能联系客服发付款截图给课时了。

ps:我的逻辑也不是太严谨,根据自己的逻辑处理吧。

4.后台交互

app内可以加二次验证的,但是怕后续出现问题太多。(百度可以查到好多app内验证的博客)比如:
1.沙盒付款,发送到苹果正式环境
2.appstore付款,发送到了沙盒环境,等多种问题。

后台代码博客

我就直接让后台(如果遇到问题了,后台更新也快)做了这个处理。

上一篇下一篇

猜你喜欢

热点阅读