第一部分:在Apple后台添加一个内购产品
1、登录appStoreConnect,如下图所示,添加一个商品
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一旦创建不可修改),选择价格,然后拉到最下面添加商品购买时的截图,最后保存
点击④,会进入价格列表,对照下图中的价格,选择商品想要卖的价格在③中进行选择
3、记住要添加截图,不然状态会变成Miss Metadata,添加截图数据没有问题后会变成Ready to Submit状态
4、在App提交审核时,把App当前版本用到的内购商品添加到App中,不添加的话,苹果审核会被拒绝,报你有一个或多个内购商品没有提交审核的Issue
第二部分:苹果支付流程
支付流程简单来讲就是在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 *)transactions,根据不同的交易状态去处理相应的逻辑
#pragma mark -SKPaymentTransactionObserver
//监听购买结果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)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内购全面实战