iOS 苹果内购
公司性质乃是少儿英语在线教育培训。app内需要上课。因此苹果规定需要集成内部购买功能,做之前一直不了解怎么形成一个好的闭环。参阅网上多个博客以及自己的思考。记录一下,希望对大家有一个帮助,以及对自己的总结。
本文章的重点问题是:
1.iTunes Connect的配置(网上自行搜索,很详细)
2.常用的购买代码
3.漏单问题处理
4.后台交互
2.常用的购买代码
首选需要引入头文件以及添加代理。
#import <StoreKit/StoreKit.h>
<SKProductsRequestDelegate,SKPaymentTransactionObserver>
#define AppStore_BuyClassHours @"appstore_buyclasshours" //购买课时
1.在viewDidLoad内需要添加监听。
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"购买课时";
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
2.通过购买事件触发 购买。
-(void)buyHourClassClick {
if([SKPaymentQueue canMakePayments]){
[self requestProductData:self.currentProId]; //self.currentProId是当前购买产品的 产品Id(在itunes connect配置的)
} else {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"内购未开启" message:@"进入【设置】-【屏幕使用时间】-【内容和隐私访问限制】-【iTunes Store 与 App Store 购买项目】-【App内购买项目】- 选择“允许”,将该功能开启" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
[self dismissViewControllerAnimated:YES completion:nil];
}];
[alert addAction:defaultAction];
[self presentViewController:alert animated:YES completion:nil];
}
}
3.客户端向苹果发起请求,获取内购的商品
- (void)requestProductData:(NSString *)type{
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUD showLoadingAndMessage:@"开始请求商品列表" toView:self.view];
});
NSArray *product = [[NSArray alloc] initWithObjects:type,nil];
NSSet *nsset = [NSSet setWithArray:product];
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
request.delegate = self;
[request start];
}
4.客户端获取全部商品成功,根据你选择的产品id去苹果服务器发起支付请求
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
NSLog(@"--------------收到产品反馈消息---------------------");
NSArray *product = response.products;
if([product count] == 0){
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUD hideHUDForView:self.view];
});
NSLog(@"--------------没有商品------------------");
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUD showLoadingAndMessage:@"商品添加进列表" toView:self.view];
});
for (SKProduct *pro in product) {
NSLog(@"SKProduct 描述信息:%@", [pro description]);
NSLog(@"localizedTitle 产品标题:%@", [pro localizedTitle]);
NSLog(@"localizedDescription 产品描述信息:%@", [pro localizedDescription]);
NSLog(@"price 价格:%@", [pro price]);
NSLog(@"productIdentifier Product id:%@", [pro productIdentifier]);
if([pro.productIdentifier isEqualToString:_currentProId]){
// 1.创建票据
SKPayment *skpayment = [SKPayment paymentWithProduct:pro];
// 2.将票据加入到交易队列
[[SKPaymentQueue defaultQueue] addPayment:skpayment];
}
}
}
5.监听购买结果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
/*
SKPaymentTransactionStatePurchasing, 正在购买
SKPaymentTransactionStatePurchased, 已经购买
SKPaymentTransactionStateFailed, 购买失败
SKPaymentTransactionStateRestored, 回复购买中
SKPaymentTransactionStateDeferred 交易还在队列里面,但最终状态还没有决定
*/
for(SKPaymentTransaction *tran in transactions) {
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchasing: {
NSLog(@"商品正在购买...");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[MBProgressHUD hideHUDForView:self.view];
[MBProgressHUD showLoadingAndMessage:@"商品正在购买..." toView:self.view];
});
}
break;
case SKPaymentTransactionStatePurchased:{
if (!IsStrEmpty(self.orderIDStr)) {
NSLog(@"购买成功之后才保存订单id,否则有订单,没购买成功,也会在首页弹窗");
//保存凭证--自定义操作
NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];
/*1.传输的是BASE64编码的字符串
2.BASE64常用的编码方案,通常用于数据传输,以及加密算法的基础算法,传输过程中能够保证数据传输的稳定性
3.BASE64是可以编码和解码的
*/
NSString *receiptString = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
NSDictionary *buyClassHourSaveDic = @{@"orderID":self.orderIDStr,@"appleReceipt":receiptString};
[UserDefaults() setObject:buyClassHourSaveDic forKey:AppStore_BuyClassHours];
[UserDefaults() synchronize];
}
[MBProgressHUD hideHUDForView:self.view];
[self verifyPurchaseWithPaymentTransaction:tran];
}
break;
case SKPaymentTransactionStateRestored:{
NSLog(@"已经购买过商品");
[MBProgressHUD showMessage:@"已经购买过商品" toView:self.view];
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
}
break;
case SKPaymentTransactionStateFailed:{//只有自己取消购买,才会弹窗。
NSLog(@"交易失败");
[MBProgressHUD hideHUDForView:self.view];
if (!IsStrEmpty(self.orderIDStr)) {
[self buyErrorAlert:tran];
} else {
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
[MBProgressHUD showMessage:@"交易失败" toView:self.view];
}
}
break;
case SKPaymentTransactionStateDeferred://交易延迟
break;
default:
break;
}
}
}
-(void)buyErrorAlert:(SKPaymentTransaction *)transaction {
self.alert = [UIAlertController alertControllerWithTitle:@"交易提醒" message:@"支付遇到问题?别着急,一键获取支付帮助" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:@"取消支付" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
//置空。
[UserDefaults() setObject:@{} forKey:AppStore_BuyClassHours];
[UserDefaults() synchronize];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[self dismissViewControllerAnimated:YES completion:nil];
}];
[self.alert addAction:defaultAction];
UIAlertAction *successAction = [UIAlertAction actionWithTitle:@"继续支付" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
[self dismissViewControllerAnimated:YES completion:nil];
[self buyHourClassClick];
}];
[self.alert addAction:successAction];
[self presentViewController:self.alert animated:YES completion:nil];
}
//请求失败
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
[MBProgressHUD showMessage:@"支付失败" toView:self.view];
NSLog(@"------------------错误-----------------:%@", error);
}
- (void)requestDidFinish:(SKRequest *)request{
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUD hideHUDForView:self.view];
});
NSLog(@"------------反馈信息结束-----------------");
}
//交易结束
- (void)completeTransaction:(SKPaymentTransaction *)transaction{
NSLog(@"交易结束");
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
- (void)dealloc{
NSLog(@"控制器--%@--销毁了", [self class]);
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
6.购买成功后的二次验证
//MARK:2.2支付成功后,苹果返回给你一个receipt收据,收据包含你这次交易的全部信息,产品id,交易号,时间等。
-(void)verifyPurchaseWithPaymentTransaction:(SKPaymentTransaction *)transaction {
if (self.alert) {
[self dismissViewControllerAnimated:YES completion:nil];//进入之后,走了失败,会dissmiss
}
[MBProgressHUD showLoading:self.view];
NSString *orderIdStr = @""; //预下订单id
NSString *receiptString = @""; //苹果返回的凭证
NSDictionary *buyClassHourSaveDic = [UserDefaults() objectForKey:AppStore_BuyClassHours];
if ([buyClassHourSaveDic isKindOfClass:[NSDictionary class]] && [buyClassHourSaveDic count] > 0) {
orderIdStr = [NSString stringWithFormat:@"%@",[buyClassHourSaveDic objectForKey:@"orderID"]];
receiptString = [NSString stringWithFormat:@"%@",[buyClassHourSaveDic objectForKey:@"appleReceipt"]];
} else {
NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];
receiptString = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];//转化为base64字符串
orderIdStr = self.orderIDStr;
}
if (!IsStrEmpty(receiptString) && !IsStrEmpty(orderIdStr)) {
NSDictionary *postDic = @{@"appleReceipt":receiptString,@"AP":orderIdStr};
[[BaseService share] sendPostRequestWithPath:URL_ApplePurchaseNotify parameters:postDic token:YES viewController:self showMBProgress:YES success:^(id responseObject) {
[MBProgressHUD hideHUDForView:self.view];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
//购买成功之后,置空。并删除本地的凭证。 在首页对这个字段进行判断。如果不为空,证明有未完成的订单。
[UserDefaults() setObject:@{} forKey:AppStore_BuyClassHours];
[UserDefaults() synchronize];
[self getProductData];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[MBProgressHUD showMessage:responseObject[@"msg"] toView:self.view];
});
} failure:^(NSError *error) {
//其他状态置空---只要走这个接口,不管失败成功,后台都可以将这两个字段对应保存。
[UserDefaults() setObject:@{} forKey:AppStore_BuyClassHours];
[UserDefaults() synchronize];
[MBProgressHUD hideHUDForView:self.view];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[self getProductData];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[MBProgressHUD showMessage:error.userInfo[xc_returnMsg] toView:self.view];
});
}];
} else { //如果为空,取消队列,重新购买、
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[MBProgressHUD showMessage:@"支付遇到问题,请联系客服" toView:self.view];
});
}
}
3.漏单问题处理
因为苹果是先扣款再进行交易验证,因此部分情况下会造成用户付款后,没有验证,造成未发道具给用户的情况。就造成了漏单问题。
我们是后台自己先做了一个预下订单,即代码中的self.orderIDStr。如果我购买成功,我就将这个【订单号和苹果购买凭证】保存到本地。二次验证成功,就置空。
1.如果苹果扣款成功,2次验证失败,可以在本页面根据自己的需要在6中操作。
2.如果苹果扣款成功,还未做验证就退出了app。可以在首页判断本地plist中AppStore_BuyClassHours是否为空,做一个弹窗,点击跳转到购买页,自动执行代理。然后二次验证。
3.如果苹果扣款成功,还未做验证就卸载了app。然后还换了手机或者重新安装了,就只能联系客服发付款截图给课时了。
ps:我的逻辑也不是太严谨,根据自己的逻辑处理吧。
4.后台交互
app内可以加二次验证的,但是怕后续出现问题太多。(百度可以查到好多app内验证的博客)比如:
1.沙盒付款,发送到苹果正式环境
2.appstore付款,发送到了沙盒环境,等多种问题。
我就直接让后台(如果遇到问题了,后台更新也快)做了这个处理。