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];//自己写的类实现的
    }

原文地址:https://www.cnblogs.com/laolitou-ping/p/13653348.html