iOS内购一条龙------项目代码 (4)

作者: _冷忆 | 来源:发表于2017-05-05 14:40 被阅读1494次

    步骤一 iOS内购一条龙------账户信息填写(1)
    步骤二 iOS内购一条龙------配置内购产品ID (2)
    步骤三 iOS内购一条龙------内购测试账号 (3)

    首先在项目中开启In-App Purchase,项目使用对应内购项目的sku跟证书,然后开始copy代码吧

    该例子适用消耗型内购,非消耗型请自行修改
    1.首先在项目工程中加入“storekit.framework”,加入头文件#import <StoreKit/StoreKit.h>
    2.添加代理监听 <SKPaymentTransactionObserver,SKProductsRequestDelegate>

    IAPManager.h

    //
    //  IAPManager.h
    //  Purchase
    //
    //  Created by ice on 17/5/3.
    //  Copyright © 2017年 ice. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    
    typedef enum {
        kIAPPurchSuccess = 0,       // 购买成功
        kIAPPurchFailed = 1,        // 购买失败
        kIAPPurchCancle = 2,        // 取消购买
        KIAPPurchVerFailed = 3,     // 订单校验失败
        KIAPPurchVerSuccess = 4,    // 订单校验成功
        kIAPPurchNotArrow = 5,      // 不允许内购
    }IAPPurchType;
    
    typedef void (^IAPCompletionHandle)(IAPPurchType type,NSData *data);
    
    @interface IAPManager : NSObject
    
    - (void)startPurchWithID:(NSString *)purchID completeHandle:(IAPCompletionHandle)handle;
    
    @end
    
    

    IAPManager.m

    //
    //  IAPManager.m
    //  Purchase
    //
    //  Created by ice on 17/5/3.
    //  Copyright © 2017年 ice. All rights reserved.
    //
    
    #import "IAPManager.h"
    #import <StoreKit/StoreKit.h>
    
    @interface IAPManager () <SKPaymentTransactionObserver,SKProductsRequestDelegate>
    @property (nonatomic,strong) NSString *purchID;
    @property (nonnull,strong) IAPCompletionHandle handle;
    @end
    
    @implementation IAPManager
    
    #pragma mark - system lifecycle
    - (instancetype)init{
        self = [super init];
        if (self) {
            // 购买监听写在程序入口,程序挂起时移除监听,这样如果有未完成的订单将会自动执行并回调 paymentQueue:updatedTransactions:方法
            [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
        }
        return self;
    }
    
    - (void)dealloc{
        [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
    }
    
    #pragma mark - Public Method
    - (void)startPurchWithID:(NSString *)purchID completeHandle:(IAPCompletionHandle)handle{
        if (purchID) {
            if ([SKPaymentQueue canMakePayments]) {
                // 开始购买服务
                self.purchID = purchID;
                self.handle = handle;
                NSSet *nsset = [NSSet setWithArray:@[purchID]];
                SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
                request.delegate = self;
                [request start];
            }else{
                [self handleActionWithType:kIAPPurchNotArrow data:nil];
            }
        }
    }
    
    #pragma mark - Private Method
    - (void)handleActionWithType:(IAPPurchType)type data:(NSData *)data{
    #if DEBUG
        switch (type) {
            case kIAPPurchSuccess:
                NSLog(@"购买成功");
                break;
            case kIAPPurchFailed:
                NSLog(@"购买失败");
                break;
            case kIAPPurchCancle:
                NSLog(@"用户取消购买");
                break;
            case KIAPPurchVerFailed:
                NSLog(@"订单校验失败");
                break;
            case KIAPPurchVerSuccess:
                NSLog(@"订单校验成功");
                break;
            case kIAPPurchNotArrow:
                NSLog(@"不允许程序内付费");
                break;
            default:
                break;
        }
    #endif
        if(self.handle){
            self.handle(type,data);
        }
    }
    // 交易结束
    - (void)completeTransaction:(SKPaymentTransaction *)transaction{
        [self verifyPurchaseWithPaymentTransaction:transaction isTestServer:NO];
    }
    
    // 交易失败
    - (void)failedTransaction:(SKPaymentTransaction *)transaction{
        if (transaction.error.code != SKErrorPaymentCancelled) {
            [self handleActionWithType:kIAPPurchFailed data:nil];
        }else{
            [self handleActionWithType:kIAPPurchCancle 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:KIAPPurchVerFailed data:nil];
            return;
        }
        // 购买成功将交易凭证发送给服务端进行再次校验
        [self handleActionWithType:kIAPPurchSuccess data:receipt];
        
        NSError *error;
        NSDictionary *requestContents = @{
                                          @"receipt-data": [receipt base64EncodedStringWithOptions:0]
                                          };
        NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
                                                              options:0
                                                                error:&error];
        
        if (!requestData) { // 交易凭证为空验证失败
            [self handleActionWithType:KIAPPurchVerFailed 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
        
        NSString *serverString = @"https://buy.itunes.apple.com/verifyReceipt";
        if (flag) {
            serverString = @"https://sandbox.itunes.apple.com/verifyReceipt";
        }
        NSURL *storeURL = [NSURL URLWithString:serverString];
        NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
        [storeRequest setHTTPMethod:@"POST"];
        [storeRequest setHTTPBody:requestData];
        
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        [NSURLConnection sendAsynchronousRequest:storeRequest queue:queue
                               completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
                                   if (connectionError) {
                                       // 无法连接服务器,购买校验失败
                                       [self handleActionWithType:KIAPPurchVerFailed data:nil];
                                   } else {
                                       NSError *error;
                                       NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
                                       if (!jsonResponse) {
                                           // 苹果服务器校验数据返回为空校验失败
                                           [self handleActionWithType:KIAPPurchVerFailed 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:KIAPPurchVerSuccess data:nil];
                                       }
    #if DEBUG
                                       NSLog(@"----验证结果 %@",jsonResponse);
    #endif
                                   }
                               }];
        
        
        // 验证成功与否都注销交易,否则会出现虚假凭证信息一直验证不通过,每次进程序都得输入苹果账号
        [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    }
    
    #pragma mark - SKProductsRequestDelegate
    - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
        NSArray *product = response.products;
        if([product count] <= 0){
    #if DEBUG
            NSLog(@"--------------没有商品------------------");
    #endif
            return;
        }
        
        SKProduct *p = nil;
        for(SKProduct *pro in product){
            if([pro.productIdentifier isEqualToString:self.purchID]){
                p = pro;
                break;
            }
        }
        
    #if DEBUG
        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]);
        NSLog(@"发送购买请求");
    #endif
        
        SKPayment *payment = [SKPayment paymentWithProduct:p];
        [[SKPaymentQueue defaultQueue] addPayment:payment];
    }
    
    //请求失败
    - (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
    #if DEBUG
        NSLog(@"------------------错误-----------------:%@", error);
    #endif
    }
    
    - (void)requestDidFinish:(SKRequest *)request{
    #if DEBUG
        NSLog(@"------------反馈信息结束-----------------");
    #endif
    }
    
    #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:
    #if DEBUG
                    NSLog(@"商品添加进列表");
    #endif
                    break;
                case SKPaymentTransactionStateRestored:
    #if DEBUG
                    NSLog(@"已经购买过商品");
    #endif
                    // 消耗型不支持恢复购买
                    [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                    break;
                case SKPaymentTransactionStateFailed:
                    [self failedTransaction:tran];
                    break;
                default:
                    break;
            }
        }
    }
    
    
    @end
    
    

    在ViewController中使用如下

    //
    //  ViewController.m
    //  Purchase
    //
    //  Created by ice on 17/5/2.
    //  Copyright © 2017年 ice. All rights reserved.
    //
    
    #import "ViewController.h"
    #import "IAPManager.h"
    
    @interface ViewController ()
    @property (nonatomic,strong) IAPManager *iapManager;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(100, 200, 100, 60)];
        [button setTitle:@"购买" forState:UIControlStateNormal];
        button.backgroundColor = [UIColor redColor];
        [button addTarget:self action:@selector(purchaseAction) forControlEvents:UIControlEventTouchUpInside];
        [self.view addSubview:button];
    }
    
    
    - (void)purchaseAction {
     
        if (!_iapManager) {
            _iapManager = [[IAPManager alloc] init];
        }
        
        // iTunesConnect 苹果后台配置的产品ID
        [_iapManager startPurchWithID:@"com.bb.helper_advisory" completeHandle:^(IAPPurchType type,NSData *data) {
            
        }];
    }
    
    @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 订单信息是产品环境中使用,但却被发送到测试环境中验证
    

    相关文章

      网友评论

      • 3fdb581ec682:楼主在吗,请教一下- (void)verifyPurchaseWithPaymentTransaction:(SKPaymentTransaction *)transaction isTestServer:(BOOL)flag{
        //交易验证 这个步骤多会调用,作用是干嘛
      • 汤姆的杰瑞:大佬你好 能加您的qq或者wx吗。。。想详细请教下您
      • 汤姆的杰瑞:大佬你好,请教下 现在所有流程都走痛了,我在itunes connect设置都是6元=6个钻石, 在测试环境下点击购买 弹框提示 “您想以6.00的价格购买一个钻石吗?这是怎么回事
        _冷忆:@汤姆的杰瑞 只是付费的话,这样就可以了,不过一般自己的服务器要记录用户数据吧,这个后面的章节有介绍的
        汤姆的杰瑞:@_冷忆 解决了哈是本地化版本名称没有写对 , 修改了之后 就是 “您想以6.00的价格购买一个600钻石吗" 。。。。。感觉还是有点奇怪。。。。 还有个疑问哈 服务端这边要做什么吗 我怎么知道我的账户有600钻石呢
        _冷忆:应该是价格方面设置错了吧,仔细研究下,问题要是解决了,不要忘了共享下
      • biny_ios:你好,作者,你的内购产品做游客登录了吗?。。我们点击内购购买时候,判断登录,然后苹果就认为我们的产品不需要绑定用户注册信息,说我们强制登录,然后被拒了,也不太清楚具体的原因。
        _冷忆:这个应该跟你们的产品有关系吧,你们的产品是否有需要用到登录功能,或者登录系统在产品上不明显,这跟内购没关系吧
      • figure_ai:请问一下怎么添加内购商品的列表
        28bb64fffadd:@figure_ai 我这边也是一直显示没有商品,我的那个那个在协议税务中,刚申请付费应用的合同,还在审核,是不是有这个原因呢?另外,测试的时候,需要把appstore的id和设置的id都退掉吗?
        figure_ai:@_冷忆 嗯呐、已解决、原来是开发账号在升级没有填税务协议、一直拿不到商品列表信息、我以为是商品添加出错还是获取的时候id填错了
        _冷忆:@figure_ai 苹果后台自己定义,把教程再看一遍
      • figure_ai:// 开始购买服务
        self.purchID = purchID;
        self.handle = handle;
        NSSet *nsset = [NSSet setWithArray:@[purchID]];
        SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];

        请问这里的purchId是什么id,appleId吗还是bundleId?
        汤姆的杰瑞:你在itunes connect里面设置的 内购商品id
      • 1de808f22631:请问一下,应用内购扣费是在什么阶段咧?在购买成功阶段?那既然购买成功了,成功扣费了,那么二次验证岂不是没什么用途了?
        _冷忆:@余泽锋 没有相关的需求以及测试环境,有的话我会补上。
        1de808f22631:@_冷忆 楼主能补充一下漏单的处理方法么?
        _冷忆:@余泽锋 校验订单是否完整,发行版跟debug版是不一样的,不校验不知道,校验完有一些额外信息,
      • 31cbd21ec174:封装的很好,怎么没看到漏单,误充处理,需要单独处理么?
        _冷忆:@小黄人儿何处归 我做的项目要求没那么复杂,这方面的问题,你可以研究研究,得空分享下,方便后来人。
      • bc3d3e66fba3:你好,我想问下,如果订单失败或者验证失败,你是怎么处理的?
        _冷忆:@Super洁 失败了就给用户一个失败提示,给个重试按钮,用户点了,流程再跑一遍。
      • 汉子_7859:[self handleActionWithType:kIAPPurchNotArrow data:nil];
        这里的kIAPPurchNotArrow是不是应该传IAPPurchType
        _冷忆:@汉子_7859 是的
      • 骑马纵天下:楼主在吗
      • Chivalrous:为什么代码执行完request start后就崩溃了呢,提示:Reading from public effective user settings.:sweat:
        Chivalrous:已解决,manager对象被释放掉了。。。
      • LSandy:如果是非消耗型,那么恢复内购这么写呢?
        咖啡豆8888:[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
        _冷忆:@LSandy 系统自己会处理,然后会走另外一个回调,restore开头的

      本文标题:iOS内购一条龙------项目代码 (4)

      本文链接:https://www.haomeiwen.com/subject/vlddtxtx.html