ios 所有虚拟商品的支付都不允许接第三方支付 实在是太烦了 只能自己接IAP 也是第一次接触 将过程记录下来 有任何问题 欢迎指教
官方文档:https://developer.apple.com/documentation/storekit/in-app_purchase?language=objc
https://help.apple.com/app-store-connect/#/devb57be10e7
相关参考:https://www.jianshu.com/p/7ae9654b85ee
具体操作
前期准备
在iTunes connect中完善你的银行,税务,联系人信息
在证书和project中开启in-app purchase
准备一个可以支付的沙盒测试账号
这个账号必须是一个没有注册过apple id的账号 这样在你测试的时候 你只要输入这个账号的用户名密码就可以了
创建应用内购买项目
在iTunes Connect中,我的app->功能,点击App内购买项目,加号就可以添加了 需要根据不同需要添加不同类型的商品
代码实现
获取Product Identifier 当用户点击对应商品时 将刚才创建的product id传进来
1
2
3SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:[NSArray arrayWithObjects:@"productID", nil]]];
request.delegate = self;
[request start];发起购买
1
2
3
4- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:response.products.firstObject];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}验证购买凭证(包括 ios 跟调用服务端接口)
发起了购买后,我们要对返回进行后续的处理,因为苹果只负责支付这一个部分,他并不负责这个购买完成后的服务怎么提供,也就说我们需要与后台交互,完成我们自己的业务逻辑
这个Observer的代码需要写在Appdelegate.m中,这是因为我们要处理app意外推出的情况,如果写在了购买页面,那么就没有办法去处理意外退出的情况对于交易成功的商品,我们需要校验transaction的receipt是否是真实的,这里一个可行的做法就是我们把这个receipt(自动续约还需要共享密钥)交给后台,由后台负责与苹果校验信息是否有效。这步OK了之后,就可以和用户说购买成功,然后提供你这个增值服务了。
由于我们与后台校验的逻辑不能写在购买页面,我们只传个receipt给后台,后台同事可能会问,我怎么知道用户买的是什么,看苹果的文档,我们发现校验成功后,返回的json数据有所有我们需要的信息
这里要注意的是在处理交易完成的各种情况时,要记得调用下面的方法,告诉苹果,我这个购买完成了,否则每次进app都会收到updatedTransactions的回调。
defaultQueue] finishTransaction: transaction];//购买结束``` 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
```objective-c
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
[SVProgressHUD showErrorWithStatus:@"发起支付失败"];
}
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
for (SKPaymentTransaction *transaction in transactions)
{
switch (transaction.transactionState)
{
case SKPaymentTransactionStatePurchased://购买成功
[self dl_completeTransaction:transaction];
break;
case SKPaymentTransactionStateFailed://购买失败
[self dl_failedTransaction:transaction];
break;
case SKPaymentTransactionStateRestored://恢复购买
[self dl_restoreTransaction:transaction];
break;
case SKPaymentTransactionStatePurchasing://正在处理
break;
default:
break;
}
}
}
#pragma mark - PrivateMethod
- (void)dl_completeTransaction:(SKPaymentTransaction *)transaction {
NSString *productIdentifier = transaction.payment.productIdentifier;
NSData *receiptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
NSString *receipt = [receiptData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
if ([receipt length] > 0 && [productIdentifier length] > 0) {
[SVProgressHUD showSuccessWithStatus:@"支付成功"];
/**
可以将receipt发给服务器进行购买验证
*/
[self dl_validateReceiptWiththeAppStore:receipt];
}
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];//购买结束
}
- (void)dl_failedTransaction:(SKPaymentTransaction *)transaction {
if(transaction.error.code != SKErrorPaymentCancelled) {
[SVProgressHUD showErrorWithStatus:@"用户取消支付"];
} else {
[SVProgressHUD showErrorWithStatus:@"支付失败"];
}
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
- (void)dl_restoreTransaction:(SKPaymentTransaction *)transaction {
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
-(void)dl_validateReceiptWiththeAppStore:(NSString *)receipt
{
NSError *error;
NSDictionary *requestContents = @{@"receipt-data": receipt};
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents options:0 error:&error];
if (!requestData) {
}else{
}
NSLog(@"%@", requestData);
NSURL *storeURL;
#ifdef DEBUG
storeURL = [NSURL URLWithString:@"https://sandbox.itunes.apple.com/verifyReceipt"];
#else
storeURL = [NSURL URLWithString:@"https://buy.itunes.apple.com/verifyReceipt"];
#endif
NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
[storeRequest setHTTPMethod:@"POST"];
[storeRequest setHTTPBody:requestData];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:storeRequest queue:queue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (connectionError) {
[SVProgressHUD showErrorWithStatus:@"网络繁忙, 请稍后再试"];
} else {
NSError *error;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (!jsonResponse) {
/* 处理error */
NSLog(@"%@", error);
}else{
/*服务端处理支付请求*/
NSString *paidUrl = [API_SERVER stringByAppendingString:@"paid_url"];
NSMutableDictionary *parameter = [NSMutableDictionary dictionary];
parameter[@"token"] = @"xxx";//订单唯一标识
parameter[@"pay_method"] = @"xxx";
parameter[@"receipt"] = receipt;
AFHTTPSessionManager * manager = [AFHTTPSessionManager manager];
manager.requestSerializer = [AFJSONRequestSerializer serializer];
manager.responseSerializer = [AFJSONResponseSerializer serializer];
[manager POST:paidUrl parameters:parameter constructingBodyWithBlock:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSNumber*code= [responseObject objectForKey:@"code"];
if([code isEqualToNumber:[NSNumber numberWithInt:1000]]){
NSLog(@"update order status success");
}else{
NSLog(@"update order status error");
}
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"error==========%@", error);
[SVProgressHUD showErrorWithStatus:@"网络繁忙, 请稍后再试"];
}];
}
}
}];
}
ps: 在支付文件头部需要引入StoreKit
1 | #import <StoreKit/StoreKit.h> |
服务器端之后的验证
如果是会自动续费的产品 我们就需要做自己的验证了
在APP启动时候要增加侦听:[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
在订阅url 中填入服务器地址 每次需要续费的时候 ios 会来请求
对于自动订阅的订单 每次支付的时候 我都会开一个30days之后的定时任务 去检查到期时扣款是否成功
ps:注意上线跟测试时的地址是不同的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31class IosRenewMemberWorker
include Sidekiq::Worker
sidekiq_options queue: :api_default
def perform(order_id)
order = ImprovedOrder.find order_id
res = RestClient::Request.execute(
method: :post,
url: "https://sandbox.itunes.apple.com/verifyReceipt",
# url: "https://buy.itunes.apple.com/verifyReceipt",
timeout: 5,
headers: {"content-type" => 'application/json'},
payload: {"receipt-data": order.ios_newest_receipt.gsub("\r\n", ""),"password": "xxx"}.to_json #passtword 是App 专用共享密钥
)
r = JSON.parse res
#订阅重试 r["pending_renewal_info"][0]["is_in_billing_retry_period"]
#“1”- App Store 仍然尝试续期订阅,续期失败 “0”- App Store 已停止尝试续期订阅。
if r['status'] == 0 && order.aasm_state == 'paid' && r["pending_renewal_info"][0]["is_in_billing_retry_period"] != "1"
order.update(ios_newest_receipt: r["latest_receipt"])
order.user.upgrade_membership!(30.days,"auto")
#自动续期状态 r["pending_renewal_info"][0]["auto_renew_status"]
#“1”- 订阅将在当前订阅时间段结束时续期。 “0”- 顾客已关闭自动续期订阅。
IosRenewMemberWorker.perform_at(30.days, order.id) if r["pending_renewal_info"][0]["auto_renew_status"] == "1"
#取消订阅 cancellation_date
#注意: 取消的App内购买项目将无限期保留在收据中。仅在为非消耗型产品、自动续期订阅、 非续期订阅或免费订阅退款时适用。
#续费产品 r["pending_renewal_info"][0]["product_id"]
elsif r["pending_renewal_info"][0]["is_in_billing_retry_period"] == "1"
order.ios_pay_fail!
end
end
end每次需要续费的时候 ios 会来请求url 更待订单状态 并支付虚拟商品
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23desc 'iOS IAP listen url'
post 'IAP_listen' do
order = ImprovedOrder.find_by(ios_newest_receipt: params[:unified_receipt])
#CANCEL Apple客户支持取消了订阅。检查Cancellation Date以了解订阅取消的日期和时间。
#RENEWAL 已过期订阅的自动续订成功。检查Subscription Expiration Date以确定下一个续订日期和时间。
#INTERACTIVE_RENEWAL 客户通过使用应用程序界面或在App Store中的App Store中以交互方式续订订阅。服务立即可用。
#DID_CHANGE_RENEWAL_PREF 客户更改了在下次续订时生效的计划。当前的有效计划不受影响。
if params[:notification_type] == "CANCEL"
order.cancel_auto_renew!
elsif params[:notification_type] == "RENEWAL"
order.restore_pay!
IosRenewMemberWorker.perform_at(30.days, order.id) if order.plan == 'ios_wonder_cv_vip'
elsif params[:notification_type] == "INTERACTIVE_RENEWAL"
order.auto_restore_pay!
IosRenewMemberWorker.perform_at(30.days, order.id) if order.plan == 'ios_wonder_cv_vip'
#elsif params[:notification_type] == "DID_CHANGE_RENEWAL_PREF"
end
{
status: 0,
code: 1000,
message: '订单状态更改成功'
}
end
测试
- 测试拉取有效的IAP项目,和相关页面
- 测试一个成功的交易
购买你的IAP项目,然后登陆第一步中申请的sandbox测试员的账号,然后在paymentQueue:updatedTransactions:看这个transaction.status是不是SKPaymentTransactionStatePurchased - 测试校验收据
- 测试一个被打断的交易
打断点,在完成交易前终止程序重新run项目,看看能不能继续完成购买
,这个情况是模拟用户已经付款成功,但是App由于某种原因崩溃。
特殊点
1.sandbox怎么测试取消订阅?
答:自己模拟啊,模拟苹果的post请求,先看苹果的server-to-server(https://developer.apple.com/documentation/storekit/in-app_purchase/enabling_server-to-server_notifications?language=objc),其中notification_type 对应值为 CANCEL ,其它字段按需要改成之前订阅的数据,向你们服务器发送请求啊,就点拨到这了。
2.自己的服务器怎么处理苹果的续订?
答:
- 首先用户第一次购买订阅,server需要把票据存储(最好把过期时间也记录一下,字段record_expires_date),苹果会通知我们的server的,其中notification_type 对应值为 INITIAL_BUY。
- 服务器需要做个定期(每天)检测,检测目前已有的所有订阅订单是否过期,如果发现过期了,就去苹果服务器验证receipt,其中苹果返回的latest_receipt_info 字段,会告诉最新的订阅订单情况,你可以校验expires-date与当前时间比较,判断该订阅有没有续订成功,并同时更新上述让记录的record_expires_date字段.
- 我们为什么做上述的处理?大家都知道苹果服务器会在订阅过期的前一天,对用户进行自动扣费,如果扣费成功了,苹果服务器并不会通知我们的服务器,这是重点。不过有个特例,如果苹果订阅过期前一天扣费失败了,苹果服务器后面几天还会尝试对用户自动扣费,如果后面扣费成功了(这时候用户实际状态是没有续订成功),苹果会通知我们的server的,其中notification_type 对应值为 RENEWAL,对于RENEWAL我们还是需要给用户更新为正在订阅的状态。
- 正式环境下,用户主动取消订阅,苹果会通知我们的server的,其中notification_type 对应值为 CANCEL,我们需要更新用户订阅的状态为取消。
- 总结,对于自动续订订阅,我们自己的服务器完全可以与apple server的交互应对用户的订阅状态,只需要确定客户端传来的用户第一次购买, user id 对应 original-transaction-id的关系。后面的续订,取消,变更套餐,完全不依赖于客户端传来的信息。