最近在写一个SDK项目,写完觉得有很多有意思的点可以记录下来,方便后续查阅。其中特别想谈谈内购这一块,毕竟支付这部分内容如果上线不出问题可能就很少在去看了。这里主要是说客户端的支付逻辑这一部分,在开发者中心创建商品和沙箱账号这些前期工作就省去不谈了。
这里写了一个YSIAPManager单例来管理开启和关闭支付状态的监听以及发起支付请求和支付完成的回调等。
+ (instancetype)share {
static YSIAPManager *manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (!manager) {
manager = [[self alloc] init];
}
});
return manager;
}
由于存在当前的支付请求没有被移出支付队列的可能,比如网络原因或者用户在支付过程中主动杀死APP等,导致支付状态的回调没有更新,当前的支付请求还存在支付队列里,所以在程序启动的时候应该开启支付状态的监听,通过单例的获取然后调用对象的监听方法。
- (void)startObserver {
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
在调起支付之前先通过商品ID跟自己的服务器初始化订单,生成订单号,让服务器全程记录这笔订单的状态,甚至可以在这一步动态控制这笔订单的支付方式,就是不用IAP,你懂的,推荐接入H5版的😊

接下来就是通过商品ID来创建商品有效性的请求,就是确认当前的商品ID跟在开发者中心配置的是否一致,是否有效,请求的结果在SKProductsRequestDelegate的代理回调里通知。在发起商品请求前确认一下当前的设备有没有IAP权限。
if ([SKPaymentQueue canMakePayments]) {
SKProductsRequest * productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:@[order.productID]]];
productsRequest.delegate = self;
[productsRequest start];
}else {
NSError *error = [NSError errorWithDomain:@"SKError" code:SKErrorPaymentNotAllowed userInfo:@{@"NSLocalizedDescription":@"该设备不能或者不允许支付交易"}];
[error.localizedDescription showToastWithCompletionBlock:^{
// 支付状态的回调
completed(YSZFStatusFail);
}];
在SKProductsRequestDelegate代理回调里拿到商品,创建支付请求,添加到请求支付队列里,接着就是在客户端弹出支付的窗口给用户操作。
不过我们怎么把这笔订单跟我们开始在服务器初始化的订单关联起来呢,不然你拿到支付完成的凭证怎么下发商品给发起支付的这个用户?强大的苹果爸爸还是提供了一些属性给我们当作透传参数用的,我这里用applicationUsername来标记订单号,不过这个applicationUsername是有丢失的风险的,后面会说我的解决办法。你不这么设计你的支付逻辑也可以,比如支付完成拿到凭证后再去服务器创建订单和下发商品等,优劣自己权衡了。
还有在这个获取苹果服务器返回商品的代理回调里,是有可能开多线程去完成的,所以如果有涉及到UI的更新操作,记得要切换回主线程去更新。
#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
if (response.invalidProductIdentifiers.count) {
NSError *error = [NSError errorWithDomain:@"SKError" code:SKErrorPaymentInvalid userInfo:@{@"NSLocalizedDescription":[NSString stringWithFormat:@"Invalid Product Identifier:%@",response.invalidProductIdentifiers.firstObject]}];
dispatch_async(dispatch_get_main_queue(), ^{
[error.localizedDescription showToastWithCompletionBlock:^{
if (self.completed) self.completed(YSZFStatusFail);
}];
});
return;
}
SKProduct *product = response.products.firstObject;
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
payment.applicationUsername = _order.orderID;
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
当然请求有成功的情况就会有失败的情况,温馨提示千万不要用越狱的机器去测试沙箱支付,不然就会在以下的回调里提示无法连接到iTunes Store。
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
dispatch_async(dispatch_get_main_queue(), ^{
[error.localizedDescription showToastWithCompletionBlock:^{
if (self.completed) self.completed(YSZFStatusFail);
}];
});
}
用户一顿支付操作后,在SKPaymentTransactionObserver的代理方法里获取到交易状态的更新,你需要对不同交易状态做不同的处理,并把当前的这个交易请求finish掉。
#pragma mark - SKPaymentTransactionObserver
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchased:{
NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];
NSString *receiptString = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
NSString *orderID = transaction.payment.applicationUsername;
[self savePruchaseReceipt:receiptString withOrderID:orderID];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
break;
case SKPaymentTransactionStateFailed:{
self.completed(YSZFStatusFail);
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
break;
case SKPaymentTransactionStateRestored:{
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
break;
case SKPaymentTransactionStatePurchasing:{
}
break;
case SKPaymentTransactionStateDeferred:{
}
break;
default:
break;
}
}
}
在支付成功的状态下,可以获取到支付完成的凭证,也可以获取到发起请求时的透传参数订单号,接下来就是校验这个支付凭证的有效性,校验通过才给用户下发商品。跟iTunes Store校验支付凭证这一步可以放在服务端或者客户端完成都可以,推荐还是把支付凭证上传到服务器,再由服务器去请求iTunes Store校验订单的有效性,注意正式环境的校验地址跟沙箱环境的地址不一样哦。大佬说过客户端发起的请求都是不安全的,存在被篡改的可能。
到此支付流程已经结束了,但是,这样是存在漏单的风险的。
在客户端把支付凭证上传给服务器的过程中有可能会失败的是吧,所以在支付成功后应该把订单号和对应的支付凭证保存到沙盒,然后再发起上传支付凭证的请求,上传成功才把沙盒对应的这笔订单删除。如果上传失败,就要设计一下重发的机制了,一个用户可能存在多笔未下发的订单,存取的时候可以通过用户ID作为唯一的key去获取,在重新登录或者切换网络状态的时候通过用户ID获取到当前用户未下发的订单,重新上传给服务器直到收到服务器接收成功的反馈。
关于applicationUsername保存的订单ID在多次的支付状态更新的回调里可能丢失的问题,我这里设计的解决机制是在支付请求加入支付队列SKPaymentQueue的时候,同时把订单ID和加入队列的时间戳保存在本地,再控制当前这笔交易结束才能开始下一笔交易的初始化,所以在交易状态更新的回调里如果碰到applicationUsername保存的订单ID丢失的情况,可以用交易的时间transactionDate去本地匹配时间最近的一笔订单,加上交易队列的先进先出原则,很稳,相当于加了限制措施的模糊匹配吧。

说完,希望对大家有所帮助。
网友评论