官方文档In-App Purchase
内购的前期准备等工作本文不讲述,有需要的可以查看网上其他文章,不少讲的挺详细的。
首先Xcode里的Capabilities中的In-App Purchase的能力打开, 如下图
关于内支付的文章网上很多,解决掉单问题的文章及方案也是一搜一大堆,但是文章中讲的掉单解决方案的实施难易程度以及可行性或者说是否适合你的产品就需要你自己做好判断了。我写这篇文章一方面总结下之前自身解决内购掉单的经验另外希望能帮得到需要的人就更好了。
上代码前先讲一下我们产品的充值流程:
app端根据用户选择的商品ID发起请求(请求苹果后台商品) -> 请求回调中找到与刚商品的ID一致的产品然后发送购买请求 -> 监听购买结果回调中状态为SKPaymentTransactionStatePurchased(即交易完成)时,调用自己服务接口将苹果回调给的凭据传给服务端 -> 服务端验证凭据成功后将用户充值的商品分发给该账户下
对于内购,我写了一个单例(建议单例,保证全局只有一个内购监听)
单例.h文件:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface IWRechargeTool : NSObject
+ (instancetype)sharedInstance;
/**
掉单处理
*/
- (void)checkIAPHandle;
/**
内购
@param goodsId 商品ID
*/
- (void)iapHandleWithGoodsId:(NSString *)goodsId;
@end
NS_ASSUME_NONNULL_END
.m文件
内购StoreKit肯定是要添加的
#import <StoreKit/StoreKit.h>
添加内购监听与代理:
SKPaymentTransactionObserver, SKProductsRequestDelegate
- 这里添加observerExist只是为了确保监听始终只有一个,loading为了确保loading的显示
static IWRechargeTool *tool = nil;
@interface IWRechargeTool ()<SKPaymentTransactionObserver, SKProductsRequestDelegate>
@property (nonatomic, copy) NSString *goodsId;
@property (nonatomic, assign) BOOL observerExist;//观察是否存在 YES(存在) NO(不存在 默认)
@property (nonatomic, assign) BOOL loading;//loading是否存在 YES(存在) NO(不存在 默认)
@end
+ (instancetype)sharedInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
tool = [[IWRechargeTool alloc] init];
});
return tool;
}
- (instancetype)init
{
if (self = [super init]) {
// 3.设置支付服务 监听
NSLog(@"==tool init");
self.observerExist = NO;
self.loading = NO;
[self addIAPObserverHandle];
}
return self;
}
请不要吐槽单例写法_
这个方法可以理解为用户选择了某一商品并且点击了购买按钮:
- (void)iapHandleWithGoodsId:(NSString *)goodsId
{
NSLog(@"内支付开始: goodsId: %@", goodsId);
if (StrEmpty(goodsId)) {
return;
}
self.goodsId = goodsId;
[self addLoadingHandle];
[self addIAPObserverHandle];
// 5.点击按钮的时候判断app是否允许apple支付
//如果app允许applepay
if ([SKPaymentQueue canMakePayments]) {
NSLog(@"==canMakePayments");
NSLog(@"==goodsId: %@", self.goodsId);
// 6.请求苹果后台商品
[self getRequestAppleProduct];
} else {
NSLog(@"not canMakePayments");
[self removeLoadingHandle];
[self removeIAPObserverHandle];
[MBProgressHUD showToast:@"请打开Apple支付"];
}
}
//请求苹果商品
- (void)getRequestAppleProduct
{
NSLog(@"====请求苹果商品");
// 7.这里的goodId就对应着苹果后台的商品ID,他们是通过这个ID进行联系的。
NSArray *product = [[NSArray alloc] initWithObjects:self.goodsId, nil];
NSSet *nsset = [NSSet setWithArray:product];
// 8.初始化请求
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
request.delegate = self;
// 9.开始请求
[request start];
}
// 10.接收到产品的返回信息,然后用返回的商品信息进行发起购买请求
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
NSLog(@"无效商品列表: %@", response.invalidProductIdentifiers);
NSArray *product = response.products;
//如果服务器没有产品
if([product count] == 0){
NSLog(@"nothing");
[self removeLoadingHandle];
[self removeIAPObserverHandle];
[MBProgressHUD showToast:@"没有有效商品"];
return;
}
NSLog(@"====product count: %lu", (unsigned long)product.count);
SKProduct *requestProduct = nil;
for (SKProduct *pro in product) {
NSLog(@"%@", [pro description]);
NSLog(@"%@", [pro localizedTitle]);
NSLog(@"%@", [pro localizedDescription]);
NSLog(@"%@", [pro price]);
NSLog(@"%@", [pro productIdentifier]);
// 11.如果后台消费条目的ID与我这里需要请求的一样(用于确保订单的正确性)
if([pro.productIdentifier isEqualToString:self.goodsId]){
[self addLoadingHandle];
[self addIAPObserverHandle];
requestProduct = pro;
// 12.发送购买请求
SKPayment *payment = [SKPayment paymentWithProduct:requestProduct];
[[SKPaymentQueue defaultQueue] addPayment:payment];
NSLog(@"goodsId: %@", self.goodsId);
NSLog(@"======addPayment");
break;
}
}
}
// 13.监听购买结果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
NSLog(@"==监听购买结果==");
[self addLoadingHandle];
[self addIAPObserverHandle];
for(SKPaymentTransaction *tran in transactions){
NSLog(@"==%@", tran.payment.productIdentifier);
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:
NSLog(@"交易完成");
[self completeTransaction:tran];
break;
case SKPaymentTransactionStatePurchasing:
NSLog(@"商品添加进列表=正在购买");
[MBProgressHUD showToast:@"正在购买"];
break;
case SKPaymentTransactionStateRestored:
NSLog(@"已经购买过商品");
[self removeLoadingHandle];
[self removeIAPObserverHandle];
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
break;
case SKPaymentTransactionStateFailed:
NSLog(@"交易失败");
[self removeLoadingHandle];
[self removeIAPObserverHandle];
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
[MBProgressHUD showToast:@"交易失败"];
break;
case SKPaymentTransactionStateDeferred:
NSLog(@"==还在队列里 状态还未决定");
[MBProgressHUD showToast:@"正在购买..."];
break;
default:
NSLog(@"==updatedTransactions default");
break;
}
}
}
- 无论前端是否验证返回的凭据,后台均需要验证,因此前端我选择不验证凭据
// 14.交易结束,当交易结束后还要去appstore上验证支付信息是否都正确,只有所有都正确后,我们就可以给用户方法我们的虚拟物品了。
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
// 验证凭据,获取到苹果返回的交易凭据
// appStoreReceiptURL iOS7.0增加的,购买交易完成后,会将凭据存放在该地址
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
// 从沙盒中获取到购买凭据
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
/**
20 BASE64 常用的编码方案,通常用于数据传输,以及加密算法的基础算法,传输过程中能够保证数据传输的稳定性
21 BASE64是可以编码和解码的
22 */
NSString *encodeStr = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
[self getApplePayDataToServerRequsetWith:transaction sendString:encodeStr];
}
注意如下的[[SKPaymentQueue defaultQueue] finishTransaction:transaction] 与发送购买请求的[[SKPaymentQueue defaultQueue] addPayment:payment]必须确保成对出现,若有未结束掉的,每次添加内购监听,回调结果中均会将未结束掉的事务回调到app端;
解决掉单也是依据该机制来处理的
- (void)getApplePayDataToServerRequsetWith:(SKPaymentTransaction *)transaction sendString:(NSString *)sendString{
NSLog(@"==========getApplePayDataToServerRequsetWith=========");
NSLog(@"==凭据: %@", sendString);
NSMutableDictionary *parms = [NSMutableDictionary dictionary];
/*
receipt String 是 苹果支付后返回的签名字符串 -
*/
parms[@"receipt"] = sendString;
WEAKSELF
[IWNetworkManager postDataWithUrl:kPayApplePay_update parameters:parms type:IWLoadingTypeAll activityInView:nil alertMessage:nil success:^(id response, NSInteger resultCode, NSString *resultMessage) {
if (resultCode == k200) {
[[NSNotificationCenter defaultCenter] postNotificationName:kUpdateUserInfoNotification object:nil];
[[NSNotificationCenter defaultCenter] postNotificationName:kMissingOrderHandleNotification object:nil];
[weakSelf removeLoadingHandle];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[weakSelf removeIAPObserverHandle];
[MBProgressHUD showToast:@"购买成功"];
} else {
[weakSelf removeLoadingHandle];
[MBProgressHUD showToast:@"购买失败"];
}
} failure:^(NSError *error) {
[weakSelf removeLoadingHandle];
[MBProgressHUD showToast:@"购买失败, 请重启App等待数秒或联系客服"];
}];
}
//请求失败
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
[self removeLoadingHandle];
[self removeIAPObserverHandle];
NSLog(@"error: %@", error);
[MBProgressHUD showToast:@"支付请求失败"];
}
//反馈请求的产品信息结束后
- (void)requestDidFinish:(SKRequest *)request{
NSLog(@"信息反馈结束");
}
- (void)removeIAPObserverHandle
{
NSLog(@"removeIAPObserverHandle");
if (self.observerExist) {
NSLog(@"==removeIAPObserverHandle");
self.observerExist = NO;
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
}
- (void)addIAPObserverHandle
{
NSLog(@"addIAPObserverHandle");
if (!self.observerExist) {
NSLog(@"==addIAPObserverHandle");
self.observerExist = YES;
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
}
- (void)removeLoadingHandle
{
NSLog(@"removeLoadingHandle");
if (self.loading) {
NSLog(@"==removeLoadingHandle");
self.loading = NO;
[MBProgressHUD hideHUDForView:[UIApplication sharedApplication].keyWindow animated:YES];
}
}
- (void)addLoadingHandle
{
NSLog(@"addLoadingHandle");
if (!self.loading) {
NSLog(@"==addLoadingHandle");
self.loading = YES;
[MBProgressHUD showHUDWithMessage:nil];
}
}
掉单处理
其实只是添加了内购监听,若有未结束掉的事务,则添加了内购监听后,苹果会将未结束掉的事务通过- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions回调到app,再进行相应处理
/**
掉单处理
*/
- (void)checkIAPHandle
{
[self addIAPObserverHandle];
}
在你认为合适的地方添加内购监听,比如App启动时,或者某些页面,又或者你可以自行添加个定时器等进行相应的修改:
///掉单处理
[[IWRechargeTool sharedInstance] checkIAPHandle];
PS: 如果你们的内购流程需要app端将苹果返回的购买凭据和与其对应的一个唯一值(比如orderId,接下来我暂且称为orderId)传输给后台,建议和后台重新设计一下内购流程。
因为如果是需要你将orderId和与其对应的transaction传给后台的话,因为可能存在多个掉单情况,那么你可能需要将所有你没成功的orderId与其对应的transaction都保存下来,在某些时机尝试将其再发送给后台;但这样做 不仅前端的工作很累,并且不能够处理所有的掉单,比如你FaceID/TouchID通过后,在监听支付回调还未回调到app端时,杀掉了app,此时虽然app进程结束了,但用户付款申请已经发出,有可能用户被扣款,但app端并未将该orderId对应的transaction保存下来,也未将此次购买行为告诉自己后台,用户购买的商品也就不会到账,因为未保存该transaction你也无法对其发起重试。
IAP机制,只要你加了监听,检测到有未结束的事务,在- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions方法中就会将未结束掉的再次返回给你
设计的内购流程,应该做到你只需要将凭据传输给自己服务,后台就可以通过验证得知该凭据是否有效以及用户购买的哪个商品(用户信息是请求头里通过token获取的)