• iOS内购


    一.内购了解

    1.内购的概述

    内购就是指,在APP内购买某些产品(还有另外的支付方式,比如微信,支付宝,Apple Pay等,这些一般通过集成第三方的SDK实现)。为什么要使用内购,苹果审核指南(https://developer.apple.com/cn/app-store/review/guidelines/#business)3.1.1规定如果您想要在 app 内解锁特性或功能 (解锁方式有:订阅、游戏内货币、游戏关卡、优质内容的访问权限或解锁完整版等),则必须使用 App 内购买项目。App 不得使用自身机制来解锁内容或功能,如许可证密钥、增强现实标记、二维码等。App 及对应元数据不得包含指引用户使用非 App 内购买项目机制进行购买的按钮、外部链接或其他行动号召用语。App 可以提供 App 内购买货币,供用户在 app 内“打赏”数字内容提供商。通过 App 内购买项目购买的所有点数和游戏货币不得过期,并且您应确保为所有可恢复的 App 内购买项目设计一套恢复机制。请务必指定正确的可购买类型,否则您的 app 将被拒绝。
    内购分成:在订阅者使用付费服务的首年内,您的收益率为 70%。当订阅者为同一订阅群组中的订阅产品累积一年的付费服务后,您的收益率将提高至 85%。同一群组中的升级订阅、降级订阅和跨级订阅不会中断付费服务的天数。转换至不同群组的订阅将重置付费服务的天数。赚取 85% 订阅价格这一规则适用于2016年6月之后生效的订阅续期。
    2.内购的产品类型

    消耗型项目

    用户可以购买各种消耗型项目 (例如游戏中的生命或宝石) 以继续 app 内进程。消耗型项目只可使用一次,使用之后即失效,必须再次购买。

    非消耗型项目

    用户可购买非消耗型项目以提升 app 内的功能。非消耗型项目只需购买一次,不会过期 (例如修图 app 中的其他滤镜)。

    自动续期订阅

    用户可购买固定时段内的服务或更新的内容 (例如云存储或每周更新的杂志)。除非用户选择取消,否则此类订阅会自动续期。

    非续期订阅

    用户可购买有时限性的服务或内容 (例如线上播放内容的季度订阅)。此类的订阅不会自动续期,用户需要逐次续订。

    3.内购流程图

     4.协议、税务和银行业务 信息填写

    这一块内容主要是在你的开发者账号上查看协议,输入联系人信息,输入银行信息和提交报税表。具体可以查看一下链接

    https://help.apple.com/app-store-connect/?lang=zh-cn#/devb6df5ee51

    二.开发部分

    1.创建内购商品

     首先点管理,点击+号创建你业务逻辑需要的产品类型,如上图。

     填写完信息后,就是这样子,我这边都是消耗性产品类型,如上图。这样子产品就创建好了。

    2.添加沙箱测试人员

    首先选择用户与访问,第二步找到测试员,第三部选择添加+,填写你的测试账号信息,最后就生成了下图4的测试账号信息了。(注意:测试账号信息是和开发者账号相关联的)

    3.代码实践部分

    首先说两个点,为了防止丢单,一般采用单例(整个应用都存在)和把单据信息存储本地策略(只要没有验证成功就不清除本地缓存)。好了上代码

    3.1.单例类

    #import <Foundation/Foundation.h>
    #import <StoreKit/StoreKit.h>

    NS_ASSUME_NONNULL_BEGIN

    typedef enum {
        IAPPurchSuccess = 0,//购买成功
        IAPPurchFailed = 1, //购买失败el
        IAPPurchCancel = 2, //取消购买
        IAPPurchVerFailed = 3, //订单校验失败
        IAPPurchVerSuccess = 4, //订单校验成功
        IAPPurchNotArrow = 5, //不允许内购
    }IAPPurchType;

    typedef void(^IAPCompletionHandleBlock)(IAPPurchType type, NSData *data);

    @interface JLKJApplePay : NSObject

    @property(nonatomic,copy)NSString*idNo;

    + (instancetype)shareIAPManager;

    //添加内购产品
    - (void)addPurchWithProductID:(NSString *)product_id completeHandle:(IAPCompletionHandleBlock)handle;

    @end

    NS_ASSUME_NONNULL_END

    /*注意事项:
    1.沙盒环境测试appStore内购流程的时候,请使用没越狱的设备。
    2.请务必使用真机来测试,一切以真机为准。
    3.项目的Bundle identifier需要与您申请AppID时填写的bundleID一致,不然会无法请求到商品信息。
    4.如果是你自己的设备上已经绑定了自己的AppleID账号请先注销掉,否则你哭爹喊娘都不知道是怎么回事。
    5.订单校验 苹果审核app时,仍然在沙盒环境下测试,所以需要先进行正式环境验证,如果发现是沙盒环境则转到沙盒验证。
    识别沙盒环境订单方法:
    1.根据字段 environment = sandbox。
    2.根据验证接口返回的状态码,如果status=21007,则表示当前为沙盒环境。
    苹果反馈的状态码:
    21000App Store无法读取你提供的JSON数据
    21002 订单数据不符合格式
    21003 订单无法被验证
    21004 你提供的共享密钥和账户的共享密钥不一致
    21005 订单服务器当前不可用
    21006 订单是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中
    21007 订单信息是测试用(sandbox),但却被发送到产品环境中验证
    21008 订单信息是产品环境中使用,但却被发送到测试环境中验证
    */
    #import "JLKJApplePay.h"

    @interface JLKJApplePay () <SKProductsRequestDelegate,SKPaymentTransactionObserver>
    {
        NSString *_purchID;
        IAPCompletionHandleBlock _handle;
    }

    @end

    @implementation JLKJApplePay

    + (instancetype)shareIAPManager {
        static JLKJApplePay *IAPManager = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            IAPManager = [[JLKJApplePay alloc] init];
        });
        return IAPManager;
    }
    - (instancetype)init {
        if ([super init]) {
            // 购买监听写在程序入口,程序挂起时移除监听,这样如果有未完成的订单将会自动执行并回调 paymentQueue:updatedTransactions:方法
            [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
        }
        return self;
    }
    - (void)dealloc{
        [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
    }
    //添加内购产品
    - (void)addPurchWithProductID:(NSString *)product_id completeHandle:(IAPCompletionHandleBlock)handle {
        //移除上次未完成的交易订单
        [self removeAllUncompleteTransactionBeforeStartNewTransaction];
        if (product_id) {
            if ([SKPaymentQueue canMakePayments]) {
                // 开始购买服务
                _purchID = product_id;
                _handle = handle;
                NSSet *nsset = [NSSet setWithArray:@[product_id]];
                SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
                request.delegate = self;
                [request start];
            }else{
                [self handleActionWithType:IAPPurchNotArrow data:nil];
            }
        }
    }

    - (void)handleActionWithType:(IAPPurchType)type data:(NSData *)data{
        switch (type) {
            case IAPPurchSuccess:
                 [[NSNotificationCenter defaultCenter]postNotificationName:@"buyResult" object:nil userInfo:@{@"type":[NSString stringWithFormat:@"%@",@"0"]}];
    //            [JKProgressHUD showMsgWithoutView:@"购买成功"];
                break;
            case IAPPurchFailed:
                 [[NSNotificationCenter defaultCenter]postNotificationName:@"buyResult" object:nil userInfo:@{@"type":[NSString stringWithFormat:@"%@",@"1"]}];
                [JKProgressHUD showMsgWithoutView:@"购买失败"];
                break;
            case IAPPurchCancel:
                 [[NSNotificationCenter defaultCenter]postNotificationName:@"buyResult" object:nil userInfo:@{@"type":[NSString stringWithFormat:@"%@",@"2"]}];
                [JKProgressHUD showMsgWithoutView:@"支付取消"];
                break;
            case IAPPurchVerFailed:
                 [[NSNotificationCenter defaultCenter]postNotificationName:@"buyResult" object:nil userInfo:@{@"type":[NSString stringWithFormat:@"%@",@"3"]}];
                [JKProgressHUD showMsgWithoutView:@"订单校验失败"];
                break;
            case IAPPurchVerSuccess:
                 [[NSNotificationCenter defaultCenter]postNotificationName:@"buyResult" object:nil userInfo:@{@"type":[NSString stringWithFormat:@"%@",@"4"]}];
                [JKProgressHUD showMsgWithoutView:@"订单校验成功"];
                break;
            case IAPPurchNotArrow:
                [[NSNotificationCenter defaultCenter]postNotificationName:@"buyResult" object:nil userInfo:@{@"type":[NSString stringWithFormat:@"%@",@"5"]}];
                [JKProgressHUD showMsgWithoutView:@"不允许程序内付费"];
                break;
            default:
                break;
        }
    }

    #pragma mark - SKProductsRequestDelegate// 交易结束
    - (void)completeTransaction:(SKPaymentTransaction *)transaction {
        // Your application should implement these two methods.
        NSString * productIdentifier = transaction.payment.productIdentifier;
        NSData *data = [productIdentifier dataUsingEncoding:NSUTF8StringEncoding];
        NSString *receipt = [data base64EncodedStringWithOptions:0];
        
        NSLog(@"%@",receipt);
        
        if ([productIdentifier length] > 0) {
            // 向自己的服务器验证购买凭证
            NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
            
            if (![[NSFileManager defaultManager] fileExistsAtPath:[receiptURL path]]) {
                // 取 receipt 的时候要判空,如果文件不存在,就要从苹果服务器重新刷新下载 receipt 了
                // SKReceiptRefreshRequest 刷新的时候,需要用户输入 Apple ID,同时需要网络状态良好
                SKReceiptRefreshRequest *receiptRefreshRequest = [[SKReceiptRefreshRequest alloc] initWithReceiptProperties:nil];
                receiptRefreshRequest.delegate = self;
                [receiptRefreshRequest start];
                return;
            }
            NSData *data = [NSData dataWithContentsOfURL:receiptURL];
            /** 交易凭证*/
            NSString *receipt_data = [data base64EncodedStringWithOptions:0];
            /** 事务标识符(交易编号)  交易编号(必传:防止越狱下内购被破解,校验 in_app 参数)*/
            NSString *transaction_id = transaction.transactionIdentifier;
            NSString *goodID = transaction.payment.productIdentifier;
            
            //这里缓存receipt_data,transaction_id 因为后端做校验的时候需要用到这两个字段
            [JLKJLocalCacheUserInfo savePurchasedInfoWithReceipt_data:receipt_data transaction_id:transaction_id orderId:self.idNo];
            
            NSLog(@"%@",receipt_data);
            NSLog(@"%@",transaction_id);
            
            [self retquestApplePay:receipt_data transaction_id:transaction_id goodsID:goodID];
        }
            
        [self verifyPurchaseWithPaymentTransaction:transaction isTestServer:NO];
        
        
    }

    - (void)retquestApplePay:(NSString *)receipt_data transaction_id:(NSString *)transaction_id goodsID:(NSString *)goodsId {
        NSMutableDictionary *param = [NSMutableDictionary new];
        
        param[@"transactionId"] = transaction_id;
        param[@"receiptData"] = receipt_data;
        param[@"orderId"] = self.idNo;
        NSLog(@"%@",param);
        
         [HttpsRequest requestPOSTWithURLString:KConfirmCredentials params:param successful:^(NSDictionary * result) {
             
             NSLog(@"%@",result);
            [[NSNotificationCenter defaultCenter]postNotificationName:@"buyResult" object:nil userInfo:@{@"type":@"6"}];//验证成功
            //验证成功,清除本地缓存
             [JLKJLocalCacheUserInfo removePurchasedInfo];
            
         } failure:^(NSError * error) {

         }];
        
    }
    // 交易失败
    - (void)failedTransaction:(SKPaymentTransaction *)transaction{
        if (transaction.error.code != SKErrorPaymentCancelled) {
            [self handleActionWithType:IAPPurchFailed data:nil];
        }else{
            [self handleActionWithType:IAPPurchCancel data:nil];
        }
        
        [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    }

    - (void)verifyPurchaseWithPaymentTransaction:(SKPaymentTransaction *)transaction isTestServer:(BOOL)flag{
        //交易验证
        NSURL *recepitURL = [[NSBundle mainBundle] appStoreReceiptURL];
        NSData *receipt = [NSData dataWithContentsOfURL:recepitURL];
        
        if(!receipt){
            // 交易凭证为空验证失败
            [self handleActionWithType:IAPPurchVerFailed data:nil];
            return;
        }
        // 购买成功将交易凭证发送给服务端进行再次校验
        [self handleActionWithType:IAPPurchSuccess data:receipt];
        
        NSError *error;
        NSDictionary *requestContents = @{
                                          @"receipt-data": [receipt base64EncodedStringWithOptions:0]
                                          };
        NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
                                                              options:0
                                                                error:&error];
        
        if (!requestData) { // 交易凭证为空验证失败
            [self handleActionWithType:IAPPurchVerFailed data:nil];
            return;
        }
        
        //In the test environment, use https://sandbox.itunes.apple.com/verifyReceipt
        //In the real environment, use https://buy.itunes.apple.com/verifyReceipt
        
    #ifdef DEBUG
    #define serverString @"https://sandbox.itunes.apple.com/verifyReceipt"
    #else
    #define serverString @"https://buy.itunes.apple.com/verifyReceipt"
    #endif
        
        NSURL *storeURL = [NSURL URLWithString:serverString];
        NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
        [storeRequest setHTTPMethod:@"POST"];
        [storeRequest setHTTPBody:requestData];
        
        NSURLSession *session = [NSURLSession sharedSession];
        [session dataTaskWithRequest:storeRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            if (error) {
                // 无法连接服务器,购买校验失败
                [self handleActionWithType:IAPPurchVerFailed data:nil];
            } else {
                NSError *error;
                NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
                if (!jsonResponse) {
                    // 苹果服务器校验数据返回为空校验失败
                    [self handleActionWithType:IAPPurchVerFailed data:nil];
                }
                
                // 先验证正式服务器,如果正式服务器返回21007再去苹果测试服务器验证,沙盒测试环境苹果用的是测试服务器
                NSString *status = [NSString stringWithFormat:@"%@",jsonResponse[@"status"]];
                if (status && [status isEqualToString:@"21007"]) {
                    [self verifyPurchaseWithPaymentTransaction:transaction isTestServer:YES];
                }else if(status && [status isEqualToString:@"0"]){
                    [self handleActionWithType:IAPPurchVerSuccess data:nil];
                }
                NSLog(@"----验证结果 %@",jsonResponse);
            }
        }];
        
        // 验证成功与否都注销交易,否则会出现虚假凭证信息一直验证不通过,每次进程序都得输入苹果账号
        [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    }

    #pragma mark - SKProductsRequestDelegate
    - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
        NSArray *product = response.products;
        if([product count] <= 0){
            NSLog(@"--------------没有商品------------------");
            return;
        }
        
        SKProduct *p = nil;
        for(SKProduct *pro in product){
            if([pro.productIdentifier isEqualToString:_purchID]){
                p = pro;
                break;
            }
        }
        
        NSLog(@"productID:%@", response.invalidProductIdentifiers);
        NSLog(@"产品付费数量:%lu",(unsigned long)[product count]);
        NSLog(@"%@",[p description]);
        NSLog(@"%@",[p localizedTitle]);
        NSLog(@"%@",[p localizedDescription]);
        NSLog(@"%@",[p price]);
        NSLog(@"%@",[p productIdentifier]);
        
        SKPayment *payment = [SKPayment paymentWithProduct:p];
        [[SKPaymentQueue defaultQueue] addPayment:payment];
    }

    //请求失败
    - (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
        NSLog(@"------------------错误-----------------:%@", error);
    }

    - (void)requestDidFinish:(SKRequest *)request{
        NSLog(@"------------反馈信息结束-----------------");
    }

    #pragma mark - SKPaymentTransactionObserver 监听购买结果
    - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{
        for (SKPaymentTransaction *tran in transactions) {
            switch (tran.transactionState) {
                case SKPaymentTransactionStatePurchased:
                    [self completeTransaction:tran];
                    break;
                case SKPaymentTransactionStatePurchasing:
                    NSLog(@"商品添加进列表");
                    break;
                case SKPaymentTransactionStateRestored:
                    NSLog(@"已经购买过商品");
                    // 消耗型不支持恢复购买
                    [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                    break;
                case SKPaymentTransactionStateFailed:
                    [self failedTransaction:tran];
                    break;
                default:
                    break;
            }
        }
    }

    #pragma mark -- 结束上次未完成的交易 防止串单
    -(void)removeAllUncompleteTransactionBeforeStartNewTransaction{
        NSArray* transactions = [SKPaymentQueue defaultQueue].transactions;
        if (transactions.count > 0) {
            //检测是否有未完成的交易
            SKPaymentTransaction* transaction = [transactions firstObject];
            if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                return;
            }
        }
    }

    3.2支付界面点击支付按钮


    -(void)zhifuBtnClick:(UIButton *)sender
    {
        
        if ([JailbreakDetectTool detectCurrentDeviceIsJailbroken]) {
            //越狱手机直接reture
            [JKProgressHUD showMsgWithoutView:@"请使用未越狱的手机购买"];
            return;
        }
        //添加通知告知购买结果
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(buyResult:) name:@"buyResult" object:nil];

        [JKProgressHUD showProgress:@"支付中" inView:self.view];
        NSDictionary *param = @{@"coinExchangeItemId":self.coinmodel.idNo,@"deviceType":@"2",@"payType":@"3"};
        WEAKSELF;
         [HttpsRequest requestPOSTWithURLString:KCreateCoinOrder params:param successful:^(NSDictionary * result) {

             JLKJApplePay *idStr = [JLKJApplePay shareIAPManager];
             idStr.idNo = [NSString stringWithFormat:@"%@",result[@"data"][@"id"]];
             [[JLKJApplePay shareIAPManager]addPurchWithProductID:weakSelf.coinmodel.productId completeHandle:^(IAPPurchType type, NSData * _Nonnull data) {
                 //购买成功后的操作
                 NSLog(@"%u==%@",type,data);
                 
             }];
            
         } failure:^(NSError * error) {

         }];
    }
    //通知返回购买结果
    -(void)buyResult:(NSNotification *)noti
    {

        NSDictionary  *dictFromData = [noti userInfo];
        if ([dictFromData[@"type"] isEqualToString:@"0"]) {
             [JKProgressHUD showProgress:@"等待验证" inView:self.view];
        }else if ([dictFromData[@"type"] isEqualToString:@"6"]){
            [JKProgressHUD showMsgWithoutView:@"充值成功"];
             if ([self.whichType isEqualToString:@"2"]) {
                 [self.navigationController popViewControllerAnimated:NO];
             }else if ([self.whichType isEqualToString:@"1"]){
                 if (self.PaySuccess) {
                     self.PaySuccess();
                 }
                 [self.navigationController popViewControllerAnimated:NO];
             }else{
                 [self getPurchaseInfo];
             }
            
        }else{
             [JKProgressHUD hide];
        }
        
    }

    3.3在AppDelegate检查是否有缓存,有缓存的话,去请求自己的后台服务器验证单据,验证成功后清除缓存

    //如果有内购缓存,调用自己后台验证订单
        if ([JLKJLocalCacheUserInfo isSelfVerification]) {
            [JLKJLocalCacheUserInfo verificationWithSelfServer];//自己写的类实现的
        }

  • 相关阅读:
    TSQL Beginners Challenge 1
    SQL拾遗
    简易实体生成方式
    CTE-递归[2]
    编号处理
    行列转换/横转竖
    OUTPUT、Merge语句的使用
    关于SQL IO的一些资料
    对左值(lvalue)和右值(rvalue)的两种理解方式
    跨平台判断64位和32位开发的一些宏定义
  • 原文地址:https://www.cnblogs.com/laolitou-ping/p/13653348.html
Copyright © 2020-2023  润新知