一、内购支购买项目类型:
类型说明:
-
1.消耗型商品 :只可使用一次的产品,使用之后即失效,必须再次购买。
示例:抖音的打赏。 -
2.非消耗型商品:只需购买一次,不会过期或随着使用而减少的产品。
示例:游戏中的 角色。 -
3.非续期订阅:允许用户购买有时限性服务的产品。此 App 内购买项目的内容可以是静态的。此类订阅不会自动续期。
示例:为期一年的已归档文章目录订阅。 -
4.自动续期订阅:允许用户在固定时间段内购买动态内容的产品。除非用户选择取消,否则此类订阅会自动续期。
示例:每月订阅提供流媒体服务的 App,腾讯视频自动续订月会员。
类型注意事项:
自动续期订阅类型最麻烦,是有连续性的,其中还有免费试用期、促销期的概念,用户还可以取消续订,恢复续订等。
收益相关:
1、2、3三种类型,您的收益率为70%。
自动续期订阅类型:
我的订阅设置如何影响收益率?
在订阅者使用付费服务的首年内,您的收益率为 70%。当订阅者为同一订阅群组中的订阅产品累积一年的付费服务后,您的收益率将提高至 85%。同一群组中的升级订阅、降级订阅和跨级订阅不会中断付费服务的天数。转换至不同群组的订阅将重置付费服务的天数。赚取 85% 订阅价格这一规则适用于2016年6月之后生效的订阅续期。
当订阅被取消时,我的收益率将如何变化?
当订阅被取消时,付费服务天数将停止累积,并开始为期 60 天的宽限期。如果订阅由于账单问题而未续期,Apple 将尝试续期该订阅,且该订阅将处于“Billing Retry(计费重试)”状态。在此期间,付费服务天数不计入其中。如果用户在 60 天的宽限期内重新订阅或在宽限期内的计费重试期间恢复订阅,付费服务天数将继续累积至 85% 的收益率。如果用户在 60 天宽限期结束后重新订阅,付费服务天数将被重置,且您的收益率为 70%。每一次订阅过期,都会开始一个新的 60 天宽限期。
苹果官方网址:
https://help.apple.com/app-store-connect/#/dev3cd978dbd
https://developer.apple.com/in-app-purchase/
二、内购支付流程:
1.客户端根据产品的productIdentifiers,向Appstore请求购买产品的详细信息,确保产品信息在苹果服务器是否存在;
2.APP验证产品成功后,发送购买请求,添加支付状态的监听;
3.先相应商品添加进列表(SKPaymentTransactionStatePurchasing)方法,然后是交易完成(SKPaymentTransactionStatePurchased),Appstore向客户端返回一段receipt-data,里面记录了本次交易的证书和签名信息。
4.客户端 或 服务器 把编码后的receipt-data发往itunes.appstore进行验证(区分沙盒环境、正式环境)
//沙盒测试环境验证
#define SANDBOX @"https://sandbox.itunes.apple.com/verifyReceipt"
//正式环境验证
#define AppStore @"https://buy.itunes.apple.com/verifyReceipt"
5.服务器验证凭证是否合法,对用户业务操作(成功增加一个月会员),并返回客户端进行后续业务逻辑处理刷新UI;
三、itunes connect申请内购
3.1、协议证书相关:
1.进itunes connnet最外层开发者平台,点击协议:
2.查看付费同意条款:
3.设置付费协议,添加银行卡账号,税务表和联系信息
4.填写完成后,等待苹果审核一段时间,这里变成有效后,才能进行沙盒测试,否则找不到产品信息
3.2、添加内购项目:
1.在你上线的APP中,APP-功能-APP内购买项目
2.选择添加的类型
3.设置产品价格和名称
4.设置显示信息
5.审核信息,可以先不填,后期测试时截图补全
3.3、创建沙盒技术测试号:
APP store Connect -> 用户和访问 -> 沙箱技术 -> 测试员
不能是已经使用的APPLE ID账号,容易后期混乱。
可以新申请几个QQ,然后激活邮箱;
然后填写 新测试员信息,密码必有大小写字母;
邮箱收到邮件,激活后添加沙盒测试账号成功。
四、具体代码和实现:
#import <StoreKit/StoreKit.h>
//沙盒测试环境验证
#define SANDBOX @"https://sandbox.itunes.apple.com/verifyReceipt"
//正式环境验证
#define AppStore @"https://buy.itunes.apple.com/verifyReceipt"
@interface SubmitOrderVC ()<UITableViewDelegate,UITableViewDataSource,SKPaymentTransactionObserver,SKProductsRequestDelegate>
@property (nonatomic, strong) BaseTableView *centerTableView;
@end
static NSString *IdentifySubmitOrderCell = @"SubmitOrderCell";
static NSString *IdentifySubmitOrderAmountCell = @"SubmitOrderAmountCell";
#define KTitleListArray @[@"商品",@"会员服务时间",@"优惠说明",@"初始价格",@"新用户红包优惠"]
@implementation SubmitOrderVC
//沙盒测试环境验证
#define SANDBOX @"https://sandbox.itunes.apple.com/verifyReceipt"
//正式环境验证
#define AppStore @"https://buy.itunes.apple.com/verifyReceipt"
- (void)viewDidLoad {
[super viewDidLoad];
[self checkOrderStatus];
}
/** 检测客户端与服务器漏单情况处理*/
- (void)checkOrderStatus
{
NSDictionary *orderInfo = [self getReceiptData];
if (orderInfo != nil) {
NSString *orderId = orderInfo[@"userID"];
NSString *receipt = orderInfo[@"receipt"];
if ([[UserModel currentUser].display_name isEqualToString:orderId]) {
[self appVerificationReceipt:receipt];
}
}
}
#pragma mark - 内购
- (void)intoChooseBuyWayVC:(UIButton *)btn {
/// [ANCustomHUD showLoadingText:@"正在拉起苹果内购" View:nil];
if ([SKPaymentQueue canMakePayments]) {
// 如果允许应用内付费购买
[ANCustomHUD showLoadingText:@"正在拉起苹果内购" View:nil];
// 把商品ID信息放入一个集合中
NSSet * set = [NSSet setWithArray:@[@"caibaoshuo_lv1"]];
// 请求内购商品信息,只返回你请求的产品(主要用于验证商品的有效性)
SKProductsRequest * request = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
request.delegate = self;
[request start];
} else {
// 如果用户手机禁止应用内付费购买.
// 则弹出开启购买权限开关的提示等...
}
}
//获取请求完成和失败的结果
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
NSLog(@"------------------错误-----------------:%@", error);
}
- (void)requestDidFinish:(SKRequest *)request{
NSLog(@"------------反馈信息结束-----------------");
}
/**
获取商品的查询结果
SKProductsRequest是苹果封装好的一个对象,该对象有两个属性。
products是一个数组,代表的是你获取到的所有商品信息,每个商品 都是一个数组元素。
invalidProductIdentifiers是无效的商品id的数组,此id对应的是你在苹果后台构建的商品id。
*/
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response NS_AVAILABLE(10_7, 3_0); {
[ANCustomHUD hiddenLoadingView:nil];
NSLog(@"--------------收到产品反馈消息---------------------");
NSArray *product = response.products;
if([product count] == 0){
NSLog(@"--------------没有商品------------------");
return;
}
NSLog(@"productID:%@", response.invalidProductIdentifiers);
NSLog(@"产品付费数量:%lu",(unsigned long)[product count]);
SKProduct *p = product.lastObject;
// for (SKProduct *pro in product) {
// NSLog(@"%@", [pro description]);
// NSLog(@"%@", [pro localizedTitle]);
// NSLog(@"%@", [pro localizedDescription]);
// NSLog(@"%@", [pro price]);
// NSLog(@"%@", [pro productIdentifier]);
// p = pro;
// // 如果后台消费条目的ID与我这里需要请求的一样(用于确保订单的正确性)
// if([pro.productIdentifier isEqualToString:_currentProId]){
// p = pro;
// }
// }
///发送购买请求,创建支付
SKPayment *payment = [SKPayment paymentWithProduct:p];
//提交付款申请
[[SKPaymentQueue defaultQueue] addPayment:payment];
//支付运行时,一定要添加监听
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
//监听购买结果,交易状态发生改变时,包括状态的改变,交易的结束 SKPaymentTransactionObserver,SKPaymentTransactionObserver 是交易观察者,
//https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/DeliverProduct.html
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions NS_AVAILABLE(10_7, 3_0);
{
for(SKPaymentTransaction *tran in transactions){
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:
{ ///交易完成。事务在队列中,用户已被收费。更新UI以反映正在进行的状态,并等待再次调用。
NSLog(@"交易完成");
[self verifyPurchaseWithPaymentTransaction:tran ];
[self finishTransactionTask:tran];
}
break;
case SKPaymentTransactionStatePurchasing:
{ //商品添加进列表,事物被添加到服务器队列中。提供购买的功能
NSLog(@"商品添加进列表");
}
break;
case SKPaymentTransactionStateRestored:
{//事务从用户的购买历史记录中恢复。恢复以前购买的功能。
NSLog(@"已经购买过商品");
[self finishTransactionTask:tran];
}
break;
case SKPaymentTransactionStateFailed:
{ //交易失败,购买失败或者用户取消。使用error属性的值向用户显示消息。
[ANCustomHUD showError:@"购买失败" toView:nil];
[self finishTransactionTask:tran];
}
break;
case SKPaymentTransactionStateDeferred: {//未知状态。更新UI以反映延迟状态,并等待再次调用。
[ANCustomHUD showError:@"最终状态未确定" toView:nil];
[self finishTransactionTask:tran];
}
break;
default:
break;
}
}
}
/**
* 验证购买,避免越狱软件模拟苹果请求达到非法购买问题
票据的校验是保证内购安全完成的非常关键的一步,一般有三种方式:
1、服务器验证,获取票据信息后上传至信任的服务器,由服务器完成与App Store的验证(提倡使用此方法,比较安全)
2、本地票据校验
3、本地App Store请求验证
*
*/
-(void)verifyPurchaseWithPaymentTransaction:(SKPaymentTransaction *)transaction{
///解除监听
//[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
//从沙盒中获取交易凭证并且拼接成请求体数据
NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData=[NSData dataWithContentsOfURL:receiptUrl];
if (!receiptData) {
return ;
}
NSString *receiptString=[receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];//转化为base64字符串
NSLog(@"内购完成的收据为:%@",receiptString);
[self saveReceiptData:@{@"receipt":receiptString,
@"userID":[UserModel currentUser].display_name}];
[self appVerificationReceipt:receiptString];
}
///APP直接去苹果服务器验证
- (void)appVerificationReceipt:(NSString *)receiptString {
NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", receiptString];//拼接请求数据
NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:SANDBOX];
NSMutableURLRequest *requestM = [NSMutableURLRequest requestWithURL:url];
requestM.HTTPBody = bodyData;
requestM.HTTPMethod = @"POST";
// 创建连接并发送同步请求
NSError *error = nil;
NSData *responseData = [NSURLConnection sendSynchronousRequest:requestM returningResponse:nil error:&error];
if (error) {
NSLog(@"验证购买过程中发生错误,错误信息:%@",error.localizedDescription);
return;
}
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:nil];
NSLog(@"%@",dic);
if ([dic[@"status"] intValue]==0) {
NSLog(@"购买成功!");
///解除监听
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
[self removeLocReceiptData];
} else {
}
}
//交易结束
- (void)completeTransaction:(SKPaymentTransaction *)transaction {
NSLog(@"交易结束");
[self finishTransactionTask:transaction];
}
///完成交易:完成交易前需要完成:坚持购买 或 下载相关内容 或 更新应用的UI以允许用户访问产品
- (void)finishTransactionTask:(SKPaymentTransaction *)transaction {
///从队列中删除已完成(即失败或已完成)的事务。试图完成购买事务将引发异常。
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
- (void)dealloc{
///解除监听
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
#pragma mark -- 本地保存一次支付凭证
static NSString *const kSaveReceiptData = @"kSaveReceiptData";
- (void)saveReceiptData:(NSDictionary *)receiptData
{
[[NSUserDefaults standardUserDefaults] setValue:receiptData forKey:kSaveReceiptData];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (NSDictionary *)getReceiptData
{
return [[NSUserDefaults standardUserDefaults] valueForKey:kSaveReceiptData];
}
- (void)removeLocReceiptData
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:kSaveReceiptData];
[[NSUserDefaults standardUserDefaults] synchronize];
}
五、注意事项:
申请相关:
需要注意的是如果应用是第一次进行IAP开发, 首先要完善苹果商店内的个人信息 (银行卡信息、 税务相关信息)才能创建相关商品, 而且需要在下一个发布版本中审核商品, 如果曾经审核过IAP开发, 可直接在后台进行新增商品审核。
测试相关:
1.内购必须用真机测试;
2.测试时必须退出App Store自己的Apple ID,登录沙盒的测试Apple ID.
3.本身请求美国服务器就慢,为防止审核人员误解,我们需要在购买时加载动画;
4.自动续期测试:https://help.apple.com/app-store-connect/#/dev7e89e149d
不允许强制用户必须登录才能购买:
因为苹果规定所有内购绑定的账号都应该是apple账号,所以不登陆你app自己的账号也应该可以购买,也就是游客状态下也要能购买,不然就耽误苹果赚钱了。
关于这个问题有两个解决办法:
(1)做游客模式可购买(未登录是绑定设备,下一个账号登录以后绑定账号)
(2)必须登录才可以使用app。
(3)绑定至当前设备,跟着设备走。例如腾讯会员
当然也可以做一个审核接口来应对。
自动续订订阅的说明一定要有:
自动续订订阅,一定要在app中有详细的说明。
除了在app里要写,在iTunes Connect的应用描述里也要写,以喜马拉雅为例,
丢单处理
由于IAP服务器无法保证质量, 或者自己服务器验证凭证出现问题时, 可能会出现丢单(用户付费成功, 但是凭证无法成功向自己服务器验证)的情况, 对于这种情况, 我们可以这样处理。
在
用户成功下单后,储存订单&uid&凭证。
存储 订单&uid&凭证
@param orderID 订单
@param uid 用户uid
@param receipt 凭证
@param saveKey 储存key
*/
- (void)saveOrderReceiptWithOrderID:(long long)orderID
uid:(NSString *)uid
receipt:(NSString *)receipt
saveKey:(NSString *)saveKey;
在用户向服务器验证成功后或者非网络原因造成的失败后, 删除此条记录,
删除 订单&凭证
@param orderID 订单
@param receipt 凭证
@param saveKey 储存key
*/
- (void)removeOrderReceiptWithOrderID:(long long)orderID
receipt:(NSString *)receipt
saveKey:(NSString *)saveKey;
这样如果由于网络问题或者服务器出现问题造成丢单, 我们可以在下一次用户启动APP再次去进行验证这笔订单, 重复上面流程
核对支付成功但是验证失败的订单
*/
- (void)checkLocalLostVipOrder;