苹果内购流程及后台配置

第一部分:在Apple后台添加一个内购产品

1、登录appStoreConnect,如下图所示,添加一个商品

增加内购.png
IAP类型类型主要有4种:

1、Consumable products 适用于可多次购买的消耗型项目,如游戏道具、虚拟币等。

2、Non-consumable products 适用于一次购买永久有效的项目,如电子书、游戏关卡等。该类型项目支持跨设备同步和本地restore,比如说,用户在某个App中购买了一本书,可在所有相同Apple ID设备的App中免费获取这本书。

3、Auto-renewable subscriptions 适用于自动续费的订阅项目,如Apple Music的按月订阅,用户购买后会每月自动续费,直到用户手动取消或者开发者下架IAP项目。

4、Non-renewable subscriptions 适用于固定有效期的非自动续费项目,如云音乐的会员和一些视频App的会员。

由于我们是充值虚拟币学点,所以选择了Consumable类型。

2、填写商品名称、Product ID(Product ID一旦创建不可修改),选择价格,然后拉到最下面添加商品购买时的截图,最后保存

创建一个学点.jpeg
点击④,会进入价格列表,对照下图中的价格,选择商品想要卖的价格在③中进行选择
价格参考.jpg

3、记住要添加截图,不然状态会变成Miss Metadata,添加截图数据没有问题后会变成Ready to Submit状态

MissMetaData.jpeg

4、在App提交审核时,把App当前版本用到的内购商品添加到App中,不添加的话,苹果审核会被拒绝,报你有一个或多个内购商品没有提交审核的Issue

添加内购商品.png

第二部分:苹果支付流程

支付流程简单来讲就是在App中点击购买商品按钮的时候,把在Apple后台设置的Product ID通过SKProductsRequest传给Apple后台,Apple后台会把商品对象SKProduct回调给App,然后再把SKProduct加到支付队列中,这个时候就会有弹框显示支付金额让你输密码了,支付成功后苹果会把交易凭证返回,拿着凭证去公司服务器验证,验证为有效凭证发学点,交易完成

1.点击购买商品按钮时传入在Apple后台的In-App Purchases中设置的相应的Product ID
2、传入Product ID参数,通过SKProductsRequest发起请求,监听回调结果
-(void)starBuyToAppStoreWithGoodsId:(NSString *)goodsID cannotPayment:(void(^)(void))cannotPayment{
    //判断app是否允许apple支付
    if (![SKPaymentQueue canMakePayments]) {
        if (cannotPayment) {
            cannotPayment();
        }
        return;
    }
    //1.点击购买商品时传入在Apple后台的In-App Purchases中设置的相应的Product ID
    //goodsID 就是在苹果后台设置的商品ID
    self.goodsId = goodsID; //比如Product ID可以是 com.example.example_LevelA
    NSArray *product = [[NSArray alloc] initWithObjects:goodsID,nil];
    NSSet *nsset = [NSSet setWithArray:product];
    
    //2、传入Product ID参数,通过SKProductsRequest发起请求,监听回调结果
    SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
    request.delegate = self;
    [request start];
}
3、在需要监听商品请求回调的地方,实现SKProductsRequestDelegate,确保Apple后台返回的Product ID与步骤2中请求的一样
4、把SKProduct加到支付队列之前,创建一个订单持久化到本地,用户可以查看订单状态,在支付流程中会遇到各种情况导致支付失败,至少可以列举5种可能的状态:0=待充值,1=充值完成,2=充值中,3=充值取消,4=充值失败,可以根据各种状态去更新订单状态
5、把SKProduct加入到支付队列中,并给SKMutablePayment对象添加唯一标识用于交易结束后获取相应的订单改变订单状态,当被成功添加到支付队列后这个时候就会有弹框了
#pragma mark -SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    NSArray *products = response.products;
    
    if([products count] == 0){
        if (self.delegate && [self.delegate respondsToSelector:@selector(appStorePayFailed)]) {
            [self.delegate appStorePayFailed];
        }
        return;
    }
    
    //3、在需要监听商品请求回调的地方,实现SKProductsRequestDelegate,确保Apple后台返回的Product ID与步骤2中请求的一样
    SKProduct *requestProduct = nil;
    for (SKProduct *product in products) {
        if([product.productIdentifier isEqualToString:self.goodsId]){
            requestProduct = product;
            break;
        }
    }
    
    if (!requestProduct) {
        return;
    }
    
//4、把SKProduct加到支付队列之前,创建一个订单持久化到本地,用户可以查看订单状态,在支付流程中会遇到各种情况导致支付失败,至少可以列举5种可能的状态:0=待充值,1=充值完成,2=充值中,3=充值取消,4=充值失败,可以根据各种状态去更新订单状态
    NSString *startTime = [NSString stringWithFormat:@"%.0f", [NSDate date].timeIntervalSince1970];
    NSString *applicationUsername = [NSString stringWithFormat:@"%@/%@/%@",[ZGAppInfoUtil appID], startTime, requestProduct.price];
    NSDictionary *dict = @{
        @"price" : requestProduct.price,
        @"startTime" : startTime,
        @"status" : @(InAppPurchaseStatusPrepare),
        @"receiptData" : @"",
        @"transactionID" : @"",
        @"applicationUserName" : applicationUsername
    };
    [self.chargeManager makeLearnPointOrderWithInfo:dict];
    
    
    //5、把SKProduct加入到支付队列中,并给SKMutablePayment对象添加唯一标识用于交易结束后获取相应的订单改变订单状态,当被成功添加到支付队列后这个时候就会有弹框了
    SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:requestProduct];
    payment.applicationUsername = applicationUsername;
    [[SKPaymentQueue defaultQueue] addPayment:payment];//将票据加入到交易队列
}
6、监听购买结果 - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions,根据不同的交易状态去处理相应的逻辑
#pragma mark -SKPaymentTransactionObserver
//监听购买结果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
     //6、支付结果的回调,根据不同的交易状态去处理相应的逻辑
    
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchasing:
                NSLog(@"交易已经添加到服务队列中");
                break;
            case SKPaymentTransactionStatePurchased:
                NSLog(@"已经付费了,交易完成");
                [self completeTransaction:transaction];
                break;
            case SKPaymentTransactionStateFailed:
                [self failedTransaction:transaction];
                NSLog(@"交易被取消或者添加到交易队列失败");
                break;
            case SKPaymentTransactionStateRestored:
                NSLog(@"交易从购买历史列表中被恢复,客户端应该完成交易");
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                break;
            case SKPaymentTransactionStateDeferred:
                NSLog(@"在交易队列中,还没有最终的结果");
                break;
            default:
                break;
        }
    }
}
7、当交易状态为SKPaymentTransactionStatePurchased,意味着支付完成了,从沙盒中获取交易凭证
8、拿到交易凭证后,在去后台服务器后台验证之前,需要把订单状态改为充值中,在持久化的订单列表中对状态为充值中的订单可以发起补充值请求
9、用户苹果支付完成了,需要拿着苹果返回的凭证去服务器校验,校验成功发放学点,并且该条订单设置为完成状态,可能因为网络原因校验订单失败,该条订单状态保持充值中状态
//支付完成
- (void)completeTransaction:(SKPaymentTransaction *)transaction{
    if (transaction.payment.productIdentifier && transaction.transactionIdentifier) {
        // 7、从沙盒中拿到交易凭证
        NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
        NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];
        if (!receiptData) {
            return;
        }
        NSString *receiptString = [receiptData base64EncodedStringWithOptions:0];
        if (!receiptData) {
            return;
        }
        
        //8、拿到交易凭证后,在去后台服务器后台验证之前,需要把订单状态改为充值中,在持久化的订单列表中对状态为充值中的订单可以发起补充值请求
        NSMutableDictionary *orderDic = [self.chargeManager getLearnPointOrderWithApplicationUsername:transaction.payment.applicationUsername].mutableCopy;
        [orderDic setObject:@(InAppPurchaseStatusOngoing) forKey:@"status"];
        [orderDic setObject:receiptString forKey:@"receiptData"];
        [orderDic setObject:transaction.transactionIdentifier forKey:@"transactionID"];
        
        //9、用户苹果支付完成了,需要拿着苹果返回的凭证去服务器校验,校验成功发放学点,并且该条订单设置为完成状态,可能因为网络原因校验订单失败,该条订单状态保持充值中状态
        NSMutableDictionary *params = [NSMutableDictionary dictionary];
        params[@"apple_receipt"] = receiptString ?: @"";
        __weak typeof(self) weakSelf = self;
        [OrderChargeManager verifyToServerWithReceipt:params success:^(NSDictionary * _Nonnull result) {
            if ([result[@"flag"] integerValue] == 1) { //校验成功
                if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(appStoreDidPaySuccess)]) {
                    [weakSelf.delegate appStoreDidPaySuccess];
                }
                [orderDic setObject:@(InAppPurchaseStatusCompleted) forKey:@"status"];
            } else {
                if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(appStoreWillPaySuccess)]) {
                    [weakSelf.delegate appStoreWillPaySuccess];
                }
            }
            
            [self.chargeManager makeLearnPointOrderWithInfo:orderDic];
        } fail:^(NSError * _Nonnull error) {
            if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(appStoreWillPaySuccessNetError)]) {
                [weakSelf.delegate appStoreWillPaySuccessNetError];
            }
            [weakSelf.chargeManager makeLearnPointOrderWithInfo:orderDic];
        }];
    }
    
    //不管凭证验证结果,最后完成交易
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
10、如果交易失败,把订单状态更新为相应的状态
//交易失败
- (void)failedTransaction:(SKPaymentTransaction *)transaction {
   // 10、如果交易失败,把订单状态更新为相应的状态
    NSMutableDictionary *orderDic = [self.chargeManager getLearnPointOrderWithApplicationUsername:transaction.payment.applicationUsername].mutableCopy;
    if (transaction.error.code == SKErrorPaymentCancelled) {
        if (self.delegate && [self.delegate respondsToSelector:@selector(appStorePayCancel)]) {
            [self.delegate appStorePayCancel];
        }
        [orderDic setObject:@(InAppPurchaseStatusCanceled) forKey:@"status"];
    } else {//其他错误
        if (self.delegate && [self.delegate respondsToSelector:@selector(appStorePayFailed)]) {
            [self.delegate appStorePayFailed];
        }
        [orderDic setObject:@(InAppPurchaseStatusFailed) forKey:@"status"];
    }
    
    [self.chargeManager makeLearnPointOrderWithInfo:orderDic];
    
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

第三部分:订单状态记录

在发起支付的过程中会遇到各种各样的问题,比如用户中途取消、用户支付完成后拿着交易凭证去公司服务器验证的时候网络不好或者公司服务器挂了,为了防止掉单也让用户看到自己订单的支付记录,有必要在本地使用Plist持久化一个订单列表

1、在Document目录下创建一个Plist文件
- (NSString *)filePath {
    if (!_filePath) {
        NSString *document = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
        NSString *path = [document stringByAppendingPathComponent:@"PointCoinOrder.plist"];
        _filePath = path;
    }
    return _filePath;
}
2、创建订单,更新订单 (因为一条订单信息在支付流程的不同阶段,最终的状态会发生改变(创建、取消、失败、完成等),需要要把老的订单删除,更新订单状态信息之后的订单追加进去)
- (dispatch_queue_t)queue {
    if (!_queue) {
        _queue = dispatch_queue_create("com.example.xxxxx", DISPATCH_QUEUE_SERIAL);
    }
    return _queue;
}

- (void)makeLearnPointOrderWithInfo:(NSDictionary *)dict {
    dispatch_async(self.queue, ^{
        NSMutableArray *resultArray = [NSMutableArray arrayWithArray:[NSArray arrayWithContentsOfFile:self.filePath]];
        //因为一条订单信息在支付流程的不同阶段,最终的状态会发生改变(创建、取消、失败、完成等),需要要把老的订单删除,更新状态信息之后的订单追加进去
        for (NSDictionary *subDict in resultArray) {
            if ([[subDict objectForKey:@"applicationUserName"] isEqualToString:[dict objectForKey:@"applicationUserName"]]) {
                [resultArray removeObject:subDict];
                break;
            }
        }
        if (dict) {
            [resultArray addObject:dict];
        }
        [resultArray writeToFile:self.filePath atomically:YES];
    });
}

//根据订单的ID获取订单
- (NSDictionary *)getLearnPointOrderWithApplicationUsername:(NSString *)applicationUsername {
    NSMutableArray *resultArray = [NSMutableArray arrayWithArray:[NSArray arrayWithContentsOfFile:self.filePath]];
    NSMutableDictionary *resultDict;
    for (NSDictionary *subDict in resultArray) {
        if ([[subDict objectForKey:@"applicationUserName"] isEqualToString:applicationUsername]) {
            resultDict = [NSMutableDictionary dictionaryWithDictionary:subDict];
            //移除旧的字典
            [resultArray removeObject:subDict];
            break;
        }
    }
    return resultDict;

}
3、去公司服务器验证订单
+ (void)verifyToServerWithReceipt:(NSDictionary *)receiptInfo success:(void(^)(NSDictionary *result))success fail:(void(^)(NSError *error))fail {
    
}

参考资料:

iOS 支付 --苹果内购解读

iOS内购全面实战

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,468评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,620评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,427评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,160评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,197评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,334评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,775评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,444评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,628评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,459评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,508评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,210评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,767评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,850评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,076评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,627评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,196评论 2 341

推荐阅读更多精彩内容