最近在做一款产品,用到了In-App Purchase,对比Apple的文档研究了两天,也找了一些文章,但是没有一篇是非常详细的,因为任何一点细节不注意,里面都会有坑。所以决定写一篇详细的技术文章,希望能帮到初次做这个功能的技术朋友。这里以【订阅】作为例子介绍。
前期准备:
iTunesConnect平台配置
1、配置银行账户信息
【协议、税务和银行业务】
这部分按照页面指示填写即可,第一步是收款的银行卡信息,第二步是Tax信息,主要填写美国报税表,这里资料很多,不做赘述,可参考:https://blog.csdn.net/joinclear/article/details/107641680
2,创建内购
3,在内购群组中创建内购项目
以上,内购产品就差不多了。
接下来,创建沙盒测试账号。
4,创建沙盒测试账号,在Debug版本上,测试内购购买。
注意,单选编辑某个账号,可以设置订阅自续费的时间周期,测试期间,一般按照分钟设置。
在真机上测试时,请一定要记住,先把iPhone上appstore的账号退出登录。
至此,我们就进入代码阶段。
In-App Purchase的代码实现细节
1,引入StoreKit.framework
注意,app上的内购逻辑,不能强制用户登录后内购,否则会被审核Reject。
2,内购界面
内购界面一定要做一些具体的说明,一定要带【恢复购买】的按钮。
以我自己的app举例:
3,逻辑代码
1)第一步:首先启动StoreKit的回调监听
我是将内购封装到了一个单实例中,而且在
- (BOOL)application:(UIApplication*)applicationdidFinishLaunchingWithOptions:(NSDictionary*)launchOptions
在这个启动函数中,第一时间进行监听,原因就是如果Apple自动扣费后,用户启动app后,会收到续费的消息通知,需要准确接收,来处理用户的到期时间 。
if ([SKPaymentQueue canMakePayments]) {
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
2)发起购买请求
产品的id,就是在iTunesConnect中创建内购时设置的id字符串。
//发起产品查询请求
NSSet*productIds = [[NSSet alloc] initWithArray:@[MONTH_PRODUCTID]];
SKProductsRequest* productReq = [[SKProductsRequest alloc] initWithProductIdentifiers:productIds];
productReq.delegate=self;
[productReq start];
//产品查询回调,因为我就加了一个产品,所以我直接取第一个了。
#pragma mark SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
[response.products enumerateObjectsUsingBlock:^(SKProduct * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
[response.invalidProductIdentifiers enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
if (response.products.count > 0) {
SKProduct *product = response.products.firstObject;
SKMutablePayment *payment = [[SKMutablePayment alloc] init];
payment.applicationUsername = @"xxxx可以是用户ID";
payment.productIdentifier = product.productIdentifier;
payment.quantity = 1;
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
else
{
[SVProgressHUD dismiss];
}
}
3)支付回调
#pragma mark **SKPaymentTransactionObserver**
// 13.监听购买结果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction {
for (SKPaymentTransaction *tran in transaction){
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:
// NSLog(@"交易完成");
// 订阅特殊处理
if (tran.originalTransaction) {
// 如果是自动续费的订单,originalTransaction会有内容
NSLog(@"自动续费的订单");
} else {
// 普通购买,以及第一次购买自动订阅
NSLog(@"普通购买,以及第一次购买自动订阅");
}
[self completeTransaction:tran];
[[SKPaymentQueue defaultQueue] finishTransaction:tran];// 销毁本次操作,由本地数据库进行记录和恢复
//[SVProgressHUD dismiss];
break;
case SKPaymentTransactionStatePurchasing:
//NSLog(@"商品添加进列表");
break;
case SKPaymentTransactionStateRestored:
// NSLog(@"已经购买过商品");
[self completeTransaction:tran];
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
//[SVProgressHUD dismiss];
break;
case SKPaymentTransactionStateFailed:
//NSLog(@"交易失败");
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
[SVProgressHUD dismiss];
break;
default:
[SVProgressHUD dismiss];
break;
}
}
}
// 交易结束,当交易结束后还要去appstore上验证支付信息是否都正确
- (void)completeTransaction:(SKPaymentTransaction *)transaction {
[self verifyFinishedTransaction:transaction];
}
4) 验证支付信息是否都正确
两种方式:
1,客户端自行校验
2,服务端校验
原理都是一样的,我这里用客户端校验来举例。
我们来看一下apple的文档怎么说:
注意Important的描述:
我们在用Debug版本测试也好,apple review团队在审核也好,都是采用sandbox环境,所以,apple的建议是,首先去product环境发送校验【正式的用户都会走这一步】,返回结果中的status如果等于21007【测试用户走这一步】,说明我们应该去sandbox去校验,再重新发送一次去sandbox url去校验。
上代码
代码中passwrod 就是上面在iTunesConnect上面管理的共享密钥,对于自续费的内购,密钥必须设置,否则会返回错误
#define ITMS_SANDBOX_VERIFY_RECEIPT_URL @"https://sandbox.itunes.apple.com/verifyReceipt"
#define ITMS_PRODUCT_VERIFY_RECEIPT_URL @"https://buy.itunes.apple.com/verifyReceipt"
#pragma mark - VerifyFinishedTransaction
-(void)verifyFinishedTransaction:(SKPaymentTransaction *)transaction{
NSData *receiptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
NSString* receipt = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
//NSLog(@"购买结果票据:%@",receipt);
[[GoGoDB sharedDBInstance] insertNewReceipt:receipt];
if(transaction.transactionState == SKPaymentTransactionStatePurchased ||
transaction.transactionState == SKPaymentTransactionStateRestored){
// 在app上做验证, 仅用于测试
//passwrod 就是在iTunesConnect上面管理的共享密钥
NSDictionary *body = @{@"receipt-data":receipt,
@"password":@""
};
NSError *error = nil;
NSData *payloadData = [NSJSONSerialization dataWithJSONObject:body
options:NSJSONWritingPrettyPrinted
error: &error];
if(payloadData)
{
//我这里用的是我自己封装的HTTP Request,请替换为你自己的HTTP request
if(verify_request == nil)
{
self.verify_request = [[WebClient alloc] initWithDelegate:self];
}
verify_request._httpMethod = @"POST";
//请求参数
NSMutableDictionary *param = [NSMutableDictionary dictionary];
[param setValue:ITMS_PRODUCT_VERIFY_RECEIPT_URL forKey:@"baseUrl"];
[param setValue:payloadData forKey:@"Body"];
verify_request._requestParam = param;
IMP_BLOCK_SELF(JCVideoPlayer);
[SVProgressHUD showInfoWithStatus:@"即将完成购买"];
[verify_request requestWithJSONBodySusessBlock:^(id lParam, id rParam) {
NSString *response = lParam;
[block_self processAppleReqRes:response payload:payloadData];
} FailBlock:^(id lParam, id rParam) {
[block_self processAppleReqRes:nil payload:nil];
}];
}
}
else
{
[SVProgressHUD dismiss];
}
}
- (void)processAppleReqRes:(NSString*)response payload:(NSData*)payload{
[SVProgressHUD dismiss];
if(response)
{
NSError *jsonParsingError = nil;
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonParsingError];
// NSLog(@"%@", dict);
int status = [[dict objectForKey:@"status"] intValue];
//NSString *environment = [dict objectForKey:@"environment"];
if(status == 0)
{
User *u = [User sharedUserData:nil];
NSArray *latest_receipt_info = [dict objectForKey:@"latest_receipt_info"];
if(latest_receipt_info && [latest_receipt_info count])
{
NSDictionary *latest = [latest_receipt_info firstObject];
long long ms = [[latest objectForKey:@"expires_date_ms"] longLongValue];
int expires_date_s = (int)(ms/1000);
NSString *product_id = [latest objectForKey:@"product_id"];
if(expires_date_s && [product_id isEqualToString:MONTH_PRODUCTID])
{
u._expiretime = [self getLocalDateFormateUTCDate:expires_date_s];
}
}
NSArray *pending_renewal_info = [dict objectForKey:@"pending_renewal_info"];
if(pending_renewal_info && [pending_renewal_info count])
{
NSDictionary *latest = [latest_receipt_info firstObject];
int auto_renew_status = [[latest objectForKey:@"auto_renew_status"] intValue];
NSString *product_id = [latest objectForKey:@"auto_renew_product_id"];
if([product_id isEqualToString:MONTH_PRODUCTID])
{
u.auto_renew_status = auto_renew_status;
}
}
[UserDefaultsKV saveUser:u];
[self refreshAfterPurchase];
}
else if(status == 21007)
{
[self verifySandboxTransaction:payload];
}
//21007
}
}
- (void) verifySandboxTransaction:(NSData*)payloadData{
if(payloadData)
{
self.verify_request = [[WebClient alloc] initWithDelegate:self];
verify_request._httpMethod = @"POST";
NSMutableDictionary *param = [NSMutableDictionary dictionary];
[param setValue:ITMS_SANDBOX_VERIFY_RECEIPT_URL forKey:@"baseUrl"];
[param setValue:payloadData forKey:@"Body"];
verify_request._requestParam = param;
// [SVProgressHUD show];
IMP_BLOCK_SELF(JCVideoPlayer);
[SVProgressHUD showInfoWithStatus:@"即将完成购买"];
[verify_request requestWithJSONBodySusessBlock:^(id lParam, id rParam) {
NSString *response = lParam;
[block_self processAppleReqRes:response payload:nil];
} FailBlock:^(id lParam, id rParam) {
[block_self processAppleReqRes:nil payload:nil];
}];
}
}
- (NSString *)getLocalDateFormateUTCDate:(int )expires_date_s {
NSDate *date = [NSDate dateWithTimeIntervalSince1970:expires_date_s];
NSDateFormatter *format = [[NSDateFormatter alloc] init];
format.dateFormat = @"yyyy-MM-dd HH:mm:ss";
format.timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
format.timeZone = [NSTimeZone localTimeZone];
NSString *dateString = [format stringFromDate:date];
return dateString;
}
对上面代码做一些说明:
因为是自动续期,所以有多个收据,
latest_receipt_info这个数组第一个就是最新的收据,
他的expires_date就是最新的过期时间,和这个判断就可以知道会员是否过期。对于验证用户是否取消订阅,时间是GMT时间,要进行转换。
pending_renewal_info , 该字段是续订状态的说明.
auto_renew_status 为0, 说明已经关闭订阅。对于退款,解析latest_receipt_info中的交易,
退款后会出现cancellation_date和cancellation_reason字段, 未退款则没有这两个字段。
至此,应该就基本能理解透彻了。