iOS内购
一.内购了解
1.内购的概述
内购分成:在订阅者使用付费服务的首年内,您的收益率为 70%。当订阅者为同一订阅群组中的订阅产品累积一年的付费服务后,您的收益率将提高至 85%。同一群组中的升级订阅、降级订阅和跨级订阅不会中断付费服务的天数。转换至不同群组的订阅将重置付费服务的天数。赚取 85% 订阅价格这一规则适用于2016年6月之后生效的订阅续期。
消耗型项目
用户可以购买各种消耗型项目 (例如游戏中的生命或宝石) 以继续 app 内进程。消耗型项目只可使用一次,使用之后即失效,必须再次购买。
非消耗型项目
用户可购买非消耗型项目以提升 app 内的功能。非消耗型项目只需购买一次,不会过期 (例如修图 app 中的其他滤镜)。
自动续期订阅
用户可购买固定时段内的服务或更新的内容 (例如云存储或每周更新的杂志)。除非用户选择取消,否则此类订阅会自动续期。
非续期订阅
用户可购买有时限性的服务或内容 (例如线上播放内容的季度订阅)。此类的订阅不会自动续期,用户需要逐次续订。
3.内购流程图
4.协议、税务和银行业务 信息填写
这一块内容主要是在你的开发者账号上查看协议,输入联系人信息,输入银行信息和提交报税表。具体可以查看一下链接
https://help.apple.com/app-store-connect/?lang=zh-cn#/devb6df5ee51
二.开发部分
1.创建内购商品
首先点管理,点击+号创建你业务逻辑需要的产品类型,如上图。
填写完信息后,就是这样子,我这边都是消耗性产品类型,如上图。这样子产品就创建好了。
2.添加沙箱测试人员
首先选择用户与访问,第二步找到测试员,第三部选择添加+,填写你的测试账号信息,最后就生成了下图4的测试账号信息了。(注意:测试账号信息是和开发者账号相关联的)
3.代码实践部分
首先说两个点,为了防止丢单,一般采用单例(整个应用都存在)和把单据信息存储本地策略(只要没有验证成功就不清除本地缓存)。好了上代码
3.1.单例类
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef enum {
IAPPurchSuccess = 0,//购买成功
IAPPurchFailed = 1, //购买失败el
IAPPurchCancel = 2, //取消购买
IAPPurchVerFailed = 3, //订单校验失败
IAPPurchVerSuccess = 4, //订单校验成功
IAPPurchNotArrow = 5, //不允许内购
}IAPPurchType;
typedef void(^IAPCompletionHandleBlock)(IAPPurchType type, NSData *data);
@interface JLKJApplePay : NSObject
@property(nonatomic,copy)NSString*idNo;
+ (instancetype)shareIAPManager;
//添加内购产品
- (void)addPurchWithProductID:(NSString *)product_id completeHandle:(IAPCompletionHandleBlock)handle;
@end
NS_ASSUME_NONNULL_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 订单信息是产品环境中使用,但却被发送到测试环境中验证
*/
#import "JLKJApplePay.h"
@interface JLKJApplePay () <SKProductsRequestDelegate,SKPaymentTransactionObserver>
{
NSString *_purchID;
IAPCompletionHandleBlock _handle;
}
@end
@implementation JLKJApplePay
+ (instancetype)shareIAPManager {
static JLKJApplePay *IAPManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
IAPManager = [[JLKJApplePay alloc] init];
});
return IAPManager;
}
- (instancetype)init {
if ([super init]) {
// 购买监听写在程序入口,程序挂起时移除监听,这样如果有未完成的订单将会自动执行并回调 paymentQueue:updatedTransactions:方法
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
return self;
}
- (void)dealloc{
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
//添加内购产品
- (void)addPurchWithProductID:(NSString *)product_id completeHandle:(IAPCompletionHandleBlock)handle {
//移除上次未完成的交易订单
[self removeAllUncompleteTransactionBeforeStartNewTransaction];
if (product_id) {
if ([SKPaymentQueue canMakePayments]) {
// 开始购买服务
_purchID = product_id;
_handle = handle;
NSSet *nsset = [NSSet setWithArray:@[product_id]];
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
request.delegate = self;
[request start];
}else{
[self handleActionWithType:IAPPurchNotArrow data:nil];
}
}
}
- (void)handleActionWithType:(IAPPurchType)type data:(NSData *)data{
switch (type) {
case IAPPurchSuccess:
[[NSNotificationCenter defaultCenter]postNotificationName:@"buyResult" object:nil userInfo:@{@"type":[NSString stringWithFormat:@"%@",@"0"]}];
// [JKProgressHUD showMsgWithoutView:@"购买成功"];
break;
case IAPPurchFailed:
[[NSNotificationCenter defaultCenter]postNotificationName:@"buyResult" object:nil userInfo:@{@"type":[NSString stringWithFormat:@"%@",@"1"]}];
[JKProgressHUD showMsgWithoutView:@"购买失败"];
break;
case IAPPurchCancel:
[[NSNotificationCenter defaultCenter]postNotificationName:@"buyResult" object:nil userInfo:@{@"type":[NSString stringWithFormat:@"%@",@"2"]}];
[JKProgressHUD showMsgWithoutView:@"支付取消"];
break;
case IAPPurchVerFailed:
[[NSNotificationCenter defaultCenter]postNotificationName:@"buyResult" object:nil userInfo:@{@"type":[NSString stringWithFormat:@"%@",@"3"]}];
[JKProgressHUD showMsgWithoutView:@"订单校验失败"];
break;
case IAPPurchVerSuccess:
[[NSNotificationCenter defaultCenter]postNotificationName:@"buyResult" object:nil userInfo:@{@"type":[NSString stringWithFormat:@"%@",@"4"]}];
[JKProgressHUD showMsgWithoutView:@"订单校验成功"];
break;
case IAPPurchNotArrow:
[[NSNotificationCenter defaultCenter]postNotificationName:@"buyResult" object:nil userInfo:@{@"type":[NSString stringWithFormat:@"%@",@"5"]}];
[JKProgressHUD showMsgWithoutView:@"不允许程序内付费"];
break;
default:
break;
}
}
#pragma mark - SKProductsRequestDelegate// 交易结束
- (void)completeTransaction:(SKPaymentTransaction *)transaction {
// Your application should implement these two methods.
NSString * productIdentifier = transaction.payment.productIdentifier;
NSData *data = [productIdentifier dataUsingEncoding:NSUTF8StringEncoding];
NSString *receipt = [data base64EncodedStringWithOptions:0];
NSLog(@"%@",receipt);
if ([productIdentifier length] > 0) {
// 向自己的服务器验证购买凭证
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
if (![[NSFileManager defaultManager] fileExistsAtPath:[receiptURL path]]) {
// 取 receipt 的时候要判空,如果文件不存在,就要从苹果服务器重新刷新下载 receipt 了
// SKReceiptRefreshRequest 刷新的时候,需要用户输入 Apple ID,同时需要网络状态良好
SKReceiptRefreshRequest *receiptRefreshRequest = [[SKReceiptRefreshRequest alloc] initWithReceiptProperties:nil];
receiptRefreshRequest.delegate = self;
[receiptRefreshRequest start];
return;
}
NSData *data = [NSData dataWithContentsOfURL:receiptURL];
/** 交易凭证*/
NSString *receipt_data = [data base64EncodedStringWithOptions:0];
/** 事务标识符(交易编号) 交易编号(必传:防止越狱下内购被破解,校验 in_app 参数)*/
NSString *transaction_id = transaction.transactionIdentifier;
NSString *goodID = transaction.payment.productIdentifier;
//这里缓存receipt_data,transaction_id 因为后端做校验的时候需要用到这两个字段
[JLKJLocalCacheUserInfo savePurchasedInfoWithReceipt_data:receipt_data transaction_id:transaction_id orderId:self.idNo];
NSLog(@"%@",receipt_data);
NSLog(@"%@",transaction_id);
[self retquestApplePay:receipt_data transaction_id:transaction_id goodsID:goodID];
}
[self verifyPurchaseWithPaymentTransaction:transaction isTestServer:NO];
}
- (void)retquestApplePay:(NSString *)receipt_data transaction_id:(NSString *)transaction_id goodsID:(NSString *)goodsId {
NSMutableDictionary *param = [NSMutableDictionary new];
param[@"transactionId"] = transaction_id;
param[@"receiptData"] = receipt_data;
param[@"orderId"] = self.idNo;
NSLog(@"%@",param);
[HttpsRequest requestPOSTWithURLString:KConfirmCredentials params:param successful:^(NSDictionary * result) {
NSLog(@"%@",result);
[[NSNotificationCenter defaultCenter]postNotificationName:@"buyResult" object:nil userInfo:@{@"type":@"6"}];//验证成功
//验证成功,清除本地缓存
[JLKJLocalCacheUserInfo removePurchasedInfo];
} failure:^(NSError * error) {
}];
}
// 交易失败
- (void)failedTransaction:(SKPaymentTransaction *)transaction{
if (transaction.error.code != SKErrorPaymentCancelled) {
[self handleActionWithType:IAPPurchFailed data:nil];
}else{
[self handleActionWithType:IAPPurchCancel 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:IAPPurchVerFailed data:nil];
return;
}
// 购买成功将交易凭证发送给服务端进行再次校验
[self handleActionWithType:IAPPurchSuccess data:receipt];
NSError *error;
NSDictionary *requestContents = @{
@"receipt-data": [receipt base64EncodedStringWithOptions:0]
};
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
options:0
error:&error];
if (!requestData) { // 交易凭证为空验证失败
[self handleActionWithType:IAPPurchVerFailed 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
#ifdef DEBUG
#define serverString @"https://sandbox.itunes.apple.com/verifyReceipt"
#else
#define serverString @"https://buy.itunes.apple.com/verifyReceipt"
#endif
NSURL *storeURL = [NSURL URLWithString:serverString];
NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
[storeRequest setHTTPMethod:@"POST"];
[storeRequest setHTTPBody:requestData];
NSURLSession *session = [NSURLSession sharedSession];
[session dataTaskWithRequest:storeRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
// 无法连接服务器,购买校验失败
[self handleActionWithType:IAPPurchVerFailed data:nil];
} else {
NSError *error;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (!jsonResponse) {
// 苹果服务器校验数据返回为空校验失败
[self handleActionWithType:IAPPurchVerFailed 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:IAPPurchVerSuccess data:nil];
}
NSLog(@"----验证结果 %@",jsonResponse);
}
}];
// 验证成功与否都注销交易,否则会出现虚假凭证信息一直验证不通过,每次进程序都得输入苹果账号
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
NSArray *product = response.products;
if([product count] <= 0){
NSLog(@"--------------没有商品------------------");
return;
}
SKProduct *p = nil;
for(SKProduct *pro in product){
if([pro.productIdentifier isEqualToString:_purchID]){
p = pro;
break;
}
}
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]);
SKPayment *payment = [SKPayment paymentWithProduct:p];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
//请求失败
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
NSLog(@"------------------错误-----------------:%@", error);
}
- (void)requestDidFinish:(SKRequest *)request{
NSLog(@"------------反馈信息结束-----------------");
}
#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:
NSLog(@"商品添加进列表");
break;
case SKPaymentTransactionStateRestored:
NSLog(@"已经购买过商品");
// 消耗型不支持恢复购买
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:tran];
break;
default:
break;
}
}
}
#pragma mark -- 结束上次未完成的交易 防止串单
-(void)removeAllUncompleteTransactionBeforeStartNewTransaction{
NSArray* transactions = [SKPaymentQueue defaultQueue].transactions;
if (transactions.count > 0) {
//检测是否有未完成的交易
SKPaymentTransaction* transaction = [transactions firstObject];
if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
return;
}
}
}
3.2支付界面点击支付按钮
-(void)zhifuBtnClick:(UIButton *)sender
{
if ([JailbreakDetectTool detectCurrentDeviceIsJailbroken]) {
//越狱手机直接reture
[JKProgressHUD showMsgWithoutView:@"请使用未越狱的手机购买"];
return;
}
//添加通知告知购买结果
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(buyResult:) name:@"buyResult" object:nil];
[JKProgressHUD showProgress:@"支付中" inView:self.view];
NSDictionary *param = @{@"coinExchangeItemId":self.coinmodel.idNo,@"deviceType":@"2",@"payType":@"3"};
WEAKSELF;
[HttpsRequest requestPOSTWithURLString:KCreateCoinOrder params:param successful:^(NSDictionary * result) {
JLKJApplePay *idStr = [JLKJApplePay shareIAPManager];
idStr.idNo = [NSString stringWithFormat:@"%@",result[@"data"][@"id"]];
[[JLKJApplePay shareIAPManager]addPurchWithProductID:weakSelf.coinmodel.productId completeHandle:^(IAPPurchType type, NSData * _Nonnull data) {
//购买成功后的操作
NSLog(@"%u==%@",type,data);
}];
} failure:^(NSError * error) {
}];
}
//通知返回购买结果
-(void)buyResult:(NSNotification *)noti
{
NSDictionary *dictFromData = [noti userInfo];
if ([dictFromData[@"type"] isEqualToString:@"0"]) {
[JKProgressHUD showProgress:@"等待验证" inView:self.view];
}else if ([dictFromData[@"type"] isEqualToString:@"6"]){
[JKProgressHUD showMsgWithoutView:@"充值成功"];
if ([self.whichType isEqualToString:@"2"]) {
[self.navigationController popViewControllerAnimated:NO];
}else if ([self.whichType isEqualToString:@"1"]){
if (self.PaySuccess) {
self.PaySuccess();
}
[self.navigationController popViewControllerAnimated:NO];
}else{
[self getPurchaseInfo];
}
}else{
[JKProgressHUD hide];
}
}
3.3在AppDelegate检查是否有缓存,有缓存的话,去请求自己的后台服务器验证单据,验证成功后清除缓存
//如果有内购缓存,调用自己后台验证订单
if ([JLKJLocalCacheUserInfo isSelfVerification]) {
[JLKJLocalCacheUserInfo verificationWithSelfServer];//自己写的类实现的
}