iOS:苹果内购实践

iOS 苹果的内购

一、介绍

苹果规定,凡是虚拟的物品(例如:QQ音乐的乐币)进行交易时,都必须走苹果的内购通道,苹果要收取大约30%的抽成,所以不允许接入第三方的支付方式(微信、支付宝等),当然开发者可以设置后门,在审核时避开审核人员。这个是有风险的,一旦发现,app会被立即下架,还是老老实实接入内购吧。

二、注意

内购接入还是比较简单的,苹果提供了专门的框架<StoreKit/StoreKit.h>,只要按照它提供的api进行开发就行。然而,接入的过程还是有需要注意的地方,分别是:漏单处理、二次验证、移除交易、游客模式。

漏单处理: 这个是一定会存在的,因为用户的一些误操作,造成的漏单基本无法避免,针对这种情况,最终的处理方式就是人工客服。当然,这个过程是可以优化的,开发者可以进行存储订单票据,server存储订单号,本地存储票据。如果用户启动app后,检测到用户上次付了款,但是需要的商品没有给到用户,此时可以自动进行验证并处理,验证通过,就将商品补给用户。

二次验证:这个步骤必不可少,首先正式环境验证,如果验证通过,说明是线上环境,可以正常操作。如果验证不通过,说明是沙盒环境,需要在沙盒环境下再次验证,沙盒环境下的验证结果会有一个统一的弹框标识[Environment : Sandbox],只要内购没有上线,验证时都是沙盒环境弹框。 二次验证的这个过程可以避免在审核app时,因为没有验证通过直接被拒的风险。二次验证放在server端实现,更加安全。

移除交易:用户再次交易时,如果上次的交易没有被移除,那么此次的交易会一直在队列中等候,无法被提交,所以一定要在上次交易完成时移除交易。

游客模式:如果我们的app支持游客使用,那么这个内购就必须要求对游客进行开放,否则审核会被拒绝。

三、使用

1、实现代理

@interface InAppPurchaseViewController ()<SKPaymentTransactionObserver,SKProductsRequestDelegate>
@property (nonatomic, strong)NSMutableArray   *products;
@property (nonatomic, strong)Product          *currentProduct;
@property (nonatomic,   copy)NSString         *currentPayNo;
@end

2、添加观察者

-(void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    // 添加观察者
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}

3、移除观察者

-(void)viewWillDisappear:(BOOL)animated
{
    // 移除观察者
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}

4、移除没有关闭的交易并做漏单处理

#pragma mark 先检查之前是否有未关闭的交易并做漏单处理
-(void)checkNotCloseAndFinishedTransaction{

    NSArray* transactions = [SKPaymentQueue defaultQueue].transactions;
    for (SKPaymentTransaction* transaction in transactions) {
        if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
        }
    }

    NSArray *tickets = [InAppPurchaseTicketService fetchInAppPurchaseTicket];
    if (tickets.count > 0) {
        [MBProgressHUD showMessage:@"正在验证未处理的订单,请稍后"];
        self.conchChargeView.userInteractionEnabled = NO;
        for (InAppPurchaseTicket *ticket in tickets) {
            [self checkAppStorePayResultWithTikect:ticket];
        }
    }
}

5、用户使用productId进行下单

-(void)loadPayNoData{

    AppWeak(weakSelf, self);
    NSMutableDictionary *params = [NSMutableDictionary dictionary];
    params[@"productId"] = self.currentProduct.productId;
    [InAppPurchaseTicketService getIosOrderWithParams:params success:^(NSArray *items, BOOL isLocalData) {
        if (items.count >0 ) {
            InAppPurchaseInfo *inAppPurchaseInfo = items[0];
            weakSelf.currentPayNo = inAppPurchaseInfo.payNo;
            [weakSelf startPayForProduct:weakSelf.currentProduct.productId];
        }
    } failure:^(id errorInfo) {
        [self showErrorInfo:errorInfo];
    }];
}

6、开始内购

-(void)startPayForProduct:(NSString *)productID{

    if([SKPaymentQueue canMakePayments]){
        [MBProgressHUD showMessage:@"正在请求商品信息,请稍等..."];
        self.conchChargeView.userInteractionEnabled = NO;

        // productID就是你在创建购买项目时所填写的产品ID
        [self requestProductID:productID];

    }else{
        // NSLog(@"不允许程序内付费");
        UIAlertView *alertError = [[UIAlertView alloc] initWithTitle:@"温馨提示"
                                                             message:@"请先开启应用内付费购买功能。"
                                                            delegate:nil
                                                   cancelButtonTitle:@"确定"
                                                   otherButtonTitles: nil];
        [alertError show];
    }
}

7、请求所有的商品ID

-(void)requestProductID:(NSString *)productID{

    // 1.拿到所有可卖商品的ID数组
    NSMutableArray *productIDArray = [NSMutableArray array];
    for (Product *product in self.products) {
        [productIDArray addObject:product.productId];
    }
    NSSet *sets = [[NSSet alloc] initWithArray:productIDArray];

    // 2.向苹果发送请求,请求所有可买的商品
    // 2.1.创建请求对象
    SKProductsRequest *sKProductsRequest = [[SKProductsRequest alloc]initWithProductIdentifiers:sets];

    // 2.2.设置代理(在代理方法里面获取所有的可卖的商品)
    sKProductsRequest.delegate = self;

    // 2.3.开始请求
    [sKProductsRequest start];
}

8、获取苹果那边的内购监听

//请求成功
-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{

    NSArray *product = response.products;
    if([product count] == 0){
        [MBProgressHUD hideHUD];
        self.conchChargeView.userInteractionEnabled = YES;
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"提示" message:@"没有商品" delegate:nil cancelButtonTitle:@"关闭" otherButtonTitles:nil, nil];
        [alertView show];
        return;
    }

    for (SKProduct *sKProduct in product) {

        NSLog(@"SKProduct 描述信息:%@", sKProduct.description);
        NSLog(@"localizedTitle 产品标题:%@", sKProduct.localizedTitle);
        NSLog(@"localizedDescription 产品描述信息:%@",sKProduct.localizedDescription);
        NSLog(@"price 价格:%@",sKProduct.price);
        NSLog(@"productIdentifier Product id:%@",sKProduct.productIdentifier);

        if([sKProduct.productIdentifier isEqualToString:self.currentProduct.productId]){
            [self buyProduct:sKProduct];
            break;
        }
    }
}

//请求失败
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{

    [MBProgressHUD hideHUD];
    self.conchChargeView.userInteractionEnabled = YES;
    UIAlertView *alerView =  [[UIAlertView alloc] initWithTitle:@"提示" message:[error localizedDescription] delegate:nil cancelButtonTitle:@"关闭" otherButtonTitles:nil];
    [alerView show];

}

9、创建票据,在队列中等候处理

-(void)buyProduct:(SKProduct *)product{

    // 1.创建票据
    SKPayment *skpayment = [SKPayment paymentWithProduct:product];

    // 2.将票据加入到交易队列
    [[SKPaymentQueue defaultQueue] addPayment:skpayment];
}

10、内购回调

#pragma mark 4.实现观察者监听付钱的代理方法,只要交易发生变化就会走下面的方法
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{

    /*
     SKPaymentTransactionStatePurchasing,    正在购买
     SKPaymentTransactionStatePurchased,     已经购买
     SKPaymentTransactionStateFailed,        购买失败
     SKPaymentTransactionStateRestored,      回复购买中
     SKPaymentTransactionStateDeferred       交易还在队列里面,但最终状态还没有决定
     */

    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchasing:{
                [MBProgressHUD hideHUD];
                [MBProgressHUD showMessage:@"正在购买中,别走开..."];
                NSLog(@"正在购买...");
            }
                break;
            case SKPaymentTransactionStatePurchased:{
                // 购买后告诉交易队列,把这个成功的交易移除掉
                [queue finishTransaction:transaction];
                [MBProgressHUD hideHUD];
                [self SavePaymentTransactionpAfterbuyAppleStoreProductSucceed:transaction];
                NSLog(@"购买成功");
            }
                break;
            case SKPaymentTransactionStateFailed:{
                // 购买失败也要把这个交易移除掉
                [queue finishTransaction:transaction];
                [MBProgressHUD hideHUD];
                self.conchChargeView.userInteractionEnabled = YES;
                NSString *errorInfo = @"购买失败,请稍后重新购买";
                if (transaction.error) {
                    NSString *reason = transaction.error.userInfo[NSLocalizedFailureReasonErrorKey];
                    if ([StringUtility isStringNotEmptyOrNil:reason]) {
                        errorInfo = reason;
                    }
                }
                UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"提示" message:errorInfo delegate:nil cancelButtonTitle:@"关闭" otherButtonTitles:nil, nil];
                [alertView show];
                NSLog(@"购买失败");
            }
                break;
            case SKPaymentTransactionStateRestored:{
                // 回复购买中也要把这个交易移除掉
                [queue finishTransaction:transaction];
                [MBProgressHUD hideHUD];
                self.conchChargeView.userInteractionEnabled = YES;
                UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"提示" message:@"重复购买了" delegate:nil cancelButtonTitle:@"关闭" otherButtonTitles:nil, nil];
                [alertView show];
                NSLog(@"重复购买了");
            }
                break;
            case SKPaymentTransactionStateDeferred:{
                NSLog(@"交易还在队列里面,但最终状态还没有决定");
            }
                break;
            default:
                break;
        }
    }
}

11、本地存储票据

// 苹果内购支付成功
- (void)SavePaymentTransactionpAfterbuyAppleStoreProductSucceed:(SKPaymentTransaction *)paymentTransactionp {

    // 传输的是BASE64编码的字符串
    // 验证凭据,获取到苹果返回的交易凭据
    // appStoreReceiptURL iOS7.0增加的,购买交易完成后,会将凭据存放在该地址
    NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];

    // 从沙盒中获取到购买凭据
    NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];

    // 传输的是BASE64编码的字符串
    NSString *reciept = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];

    // productIdentifier
    NSString *productIdentifier = paymentTransactionp.payment.productIdentifier;
    if ([productIdentifier length]>0) {

        //本地存储票据
        InAppPurchaseTicket *ticket = [[InAppPurchaseTicket alloc] init];
        ticket.payNo = self.currentPayNo;
        ticket.productId = self.currentProduct.productId;
        ticket.reciept = reciept;
        ticket.state = 1;
        [InAppPurchaseTicketService saveLocalTransaction:ticket];

        // 去验证是否真正的支付成功了
        [MBProgressHUD showMessage:@"购买成功,正在验证订单..."];
        [self checkAppStorePayResultWithTikect:ticket];
    }
}

12、二次验证

#pragma mark 服务端验证购买凭据
- (void)checkAppStorePayResultWithTikect:(InAppPurchaseTicket *)tikect {

    /*
     生成订单参数,注意沙盒测试账号与线上正式苹果账号的验证途径不一样,要给后台标明

     注意:
     自己测试的时候使用的是沙盒购买(测试环境)
     App Store审核的时候也使用的是沙盒购买(测试环境)
     上线以后就不是用的沙盒购买了(正式环境)
     所以此时应该先验证正式环境,在验证测试环境

     正式环境验证成功,说明是线上用户在使用
     正式环境验证不成功返回21007,说明是自己测试或者审核人员在测试

     苹果AppStore线上的购买凭证地址是: https://buy.itunes.apple.com/verifyReceipt
     测试地址是:https://sandbox.itunes.apple.com/verifyReceipt

     */

    NSString *sandbox;
#ifdef TEST
    sandbox = @"0"; //沙盒测试环境
#else
    sandbox = @"1"; //线上正式环境
#endif

    if (!tikect || !tikect.payNo || tikect.payNo.length==0 || !tikect.reciept || tikect.reciept.length==0) {
        return;
    }

    AppWeak(weakSelf, self);
    NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    params[@"payno"]   = tikect.payNo;               //订单号
    params[@"sandbox"] = sandbox;                    //使用环境
    params[@"receipt"] = tikect.reciept;             //票据信息

    [InAppPurchaseTicketService doubleIosVerifyWithParams:params success:^(NSArray *items, BOOL isLocalData) {

        //隐藏loding
        [MBProgressHUD hideHUD];
        [MBProgressHUD showSuccess:@"恭喜你,购买成功" afterDelay:2.0];
        weakSelf.conchChargeView.userInteractionEnabled = YES;

        //清除本地当前对应订单票据
        InAppPurchaseInfo *info = items[0];
        [InAppPurchaseTicketService clearInAppPurchaseTicketWithPayNo:info.payNo];

        //刷新UI
        if (self.fromVCType == FromVCTypeCurrentInAppPurchaseVC) {
            [weakSelf loadConchData];
        }
        else{
            //回调并跳转页面
            if (weakSelf.chargeConchSuccsssBlock) {
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    weakSelf.chargeConchSuccsssBlock(@(YES));
                    [weakSelf.navigationController popViewControllerAnimated:YES];
                });
            }
        }

    } failure:^(id errorInfo) {
        [MBProgressHUD hideHUD];
        weakSelf.conchChargeView.userInteractionEnabled = YES;
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"验证失败" message:errorInfo delegate:nil cancelButtonTitle:@"关闭" otherButtonTitles:nil, nil];
        [alertView show];
    }];
}

13、懒加载plist中的所有约定的商品productId

-(NSMutableArray *)products{
    if (!_products) {
        _products = [NSMutableArray array];
        NSString *path = [[NSBundle mainBundle] pathForResource:@"Products" ofType:@"plist"];
        NSArray *items = [NSArray arrayWithContentsOfFile:path];
        if ([ArrayUtility isArrayNotEmptyOrNil:items]) {
            for (NSDictionary *dic in items) {
                Product *product = [[Product alloc] initWithProperties:dic];
                [_products addObject:product];
            }
        }
    }
    return _products;
}

三、结论

这就是内购的全部流程了,我把主要的流程梳理了一下,具体的细节,开发人员自己去整理。与君共勉。。。。。

原文地址:https://www.cnblogs.com/XYQ-208910/p/9237895.html

时间: 2024-10-15 21:32:46

iOS:苹果内购实践的相关文章

IOS,苹果内购和添加广告

内购——应用内购买 通过苹果应用程序商店有三种主要赚钱的方式: 直接收费(与国内大部分用户的消费习惯相悖) 广告(降低用户体验 应用程序名称带Lite可以添加广告) O2O -> Online推广 & Offline交易,闭环 不要砍功能,增加内容,而不是增加功能 内购:应用程序本身的增值产品,游戏装备,应用程序中增值功能同样可以内购 第三方支付:跟应用程序无关的 内购分成:三(苹果)七(开发商)开 提示: 要做好游戏 & 应用,一定要研究心理,要研究哲学 人人都是产品经理 内购的五

苹果内购流程详解

苹果内购主要分为两部分 itunnes 相关配置 一.在itunnes 上配置相关信息资料 首先需要配置一些前提条件,详细步骤如下 1.进入itunnes选择协议,税务银行业务 2.完善资料信息,主要有3项(联系人,银行信息,税务信息) 4.完善联系人信息,一般填申请开发者账号人员的信息,可以修改 5.完善银行信息 需要银行支付号(就是开户银行的一个标识),银行卡持有人名字 6.完善税务信息 7.以下默认选择no 8.确认界面,下图可能不太对,当时我是上下2个界面,第四项要上下一致才行 二.上面

苹果内购和 Apple Pay

作者:CC老师_MissCC链接:http://www.jianshu.com/p/e3bc47e81785來源:简书 苹果内购 1.什么是内购? 如果你购买的商品,是在本app中使用和消耗的,就一定要用内购,否则会被拒绝上线,例如:游戏币,在线书籍,app中使用的道具等虚拟产品.如果购买的就是普通的商品,例如淘宝买东西等,就不需要用内购.内购的话,苹果公司需要抽取30%佣金. 当然,打赏功能被纳入内购项目中.所以例如微信打赏功能.直播项目打赏主播都必须采用内购. 可以简单理解成,带有内购功能的

关于苹果内购(IAP)的一些问题以及那些坑

最近在研究苹果内购功能,所以,在网上找了一些资料,进行学习.但是,内购功能在实现的过程中,有很多坑,笔者算是真的遇到了好多啊,下面也是自己对内购的一些心得与体会吧! 我这里说的可能不太详尽,所以,我先把再网上看到的一些帖子贴在这里,以便大家做内购的时候,方便查找相关信息. 这里是一篇写的比较全面的帖子,但是没有写中间问题处理: <iOS开发内购全套图文教程> 在网上搜了一些相关的帖子,简单归纳总结了一下,觉得论坛里有一个叫Teng的世界的大神,写了三篇博客,写的很详细: [IAP支付之一]In

ios IAP 内购验证

参考我之前的笔记 苹果内购笔记,在客户端向苹果购买成功之后,我们需要进行二次验证. 二次验证 IOS在沙箱环境下购买成功之后,向苹果进行二次验证,确认用户是否购买成功. 当应用向Apple服务器请求购买,成功之后,Apple会返回以下四个数据给应用 四个验证数据 productIdentifier:cosmosbox.strikehero.gems60 state: Purchased receipt: ewoJInNpZ25hdHVyZSIgPSAiQXF1M3JiR1grbmJMeGVvZS

apicloud含有微信支付。支付宝支付和苹果内购的代码

<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="maximum-scale=1.0,minimum-scale=1.0,user-scalable=0,width=device-width,initial-scale=1.0"/> <meta name="

iOS应用内购常见问题及注意事项

如果您在实现和测试iOS应用内购的时候遇到问题,可以逐一对照下面所列出的条目,并逐一进行检查.相信可以排除大部分的错误. 1.您是否在iOS Dev Center中打开了对应应用AppID的In-App Purchases功能?登陆iOS Dev Center的Certificates, Identifiers & Profiles下,在Identifiers中找到正在开发的App,In-App Purchase一项应当显示Enabled(如果使用Xcode5,可以直接在Xcode的Capabi

苹果内购支付对接

public bool ValidateApplePay() { //客户端post过来的参数 string appleReceipt = Request.Form["appleReceipt"]; //苹果内购的验证收据 string orderId = PayHelper.GetOrderIDByPrefix("AP");  //订单编号 string amount = Request.Form["amount"];             

苹果内购服务器验证之receipt返回多组in_app思考

最近有部分用户反映,苹果内购充值失败,经过测试总结有几个关键点出现问题 1.app购买成功苹果没有返回票据,属于票据遗漏(取决于苹果服务器的响应状况),只能客户端进行监听刷新等处理 2.app连续购买的过程中,前几次苹果没有返回票据,几次之后,苹果返回了一个有效的票据,app提交给服务器进行验证的过程中in_app出现多组数据的情况,这种情况还是能充值成功了,只是不能全部到账 3.app连续购买,有一次正常返回票据,在提交给服务器的过程中出现意外,但实际服务端已经接受到票据,为用户成功充值,但a