内购介绍
IAP 是一套商品交易系统,而非简单的支付系统,每一个购买项目都需要在开发者后台的Itunes Connect后台为 App 创建一个对应的商品,提交给苹果审核通过后,购买项目才会生效。内购商品有四种类型:
- 消耗型项目:只可使用一次的产品,使用之后即失效,必须再次购买,如:游戏币、一次性虚拟道具等
- 非消耗型项目:只需购买一次,不会过期或随着使用而减少的产品。如:电子书
- 自动续期订阅:允许用户在固定时间段内购买动态内容的产品。除非用户选择取消,否则此类订阅会自动续期,如:Apple Music这类按月订阅的商品(有些鸡贼的开发者以此收割对IAP商品不熟悉的用户,参考App Store“流氓”软件)
- 非续期订阅:允许用户购买有时限性服务的产品,此 App 内购买项目的内容可以是静态的。此类订阅不会自动续期
物料准备:
- App Store connect后台填写银行账户信息,签署内购协议
- 配置商品信息,包括产品ID,产品价格等
- 配置用于测试IAP支付功能的沙箱账户
Xcode capablities 打开IAP开关
IAP 支付流程:
1.客户端向Appstore请求购买产品,Appstore验证产品成功后,从客户端的Apple账户中扣费。
2.Appstore向客户端返回一段receipt-data(票据),里面记录了本次交易的证书和签名信息。
3.客户端向我们可以信任的服务器(后台)提供receipt-data
4.服务器对receipt-data进行一次base64编码
5.把编码后的receipt-data发往itunes.appstore进行验证
6.itunes.appstore返回验证结果给服务器
7.服务器对商品购买状态以及商品类型,向客户端发放相应的道具与推送数据更新通知
支付结束后有两种验证方式:
IAP built-in Model(本地验证):此种方式跳过来3-7步,在第2步中拿到票据直接向itunes.appstore请求验证票据,根据票据的结果来修改数据。有一些单机游戏因为不涉及后台服务器会采取此种方式,但由此单来的不安全也很明显,比如一些越狱的手机会很容易对此进行一些数据操作
IAP Server Model(服务器验证):如果把数据放在服务器做校验(如实走完1-7的流程),就不用担心客户端出现伪造票据等问题。但如果得到票据说明苹果已经扣款成功,就在这时向服务器发送票据验证的时候出现来异常,这个时候可能网络突然断了,未把票据发送的服务器验证,导致明明已经扣了款,却没有收到相应的内购产品,出现了漏单问题。
对漏单的情况处理:
1:得到票据,立即保存本地,并向服务器验证
2:验证成功,删除本地保存数据。若未成功,再次验证重试。
3:APP重启时,如有本地票据则与服务器进行认证,若认证成功则删除票据。
4:若以上流程还未能解决漏单问题,则可在APP增加类似找回按钮,依据本地保存票据进行找回(流程三)。
注:服务器需建立表单记录票据数据,避免多次增加内购产品。
代码实现:
1.向苹果后台发起商品数据请求
// 每次发起新的购买请求之前,先处理上次是否有未完成的交易
- (void)finishLastPurchasedTransaction {
NSArray *transactions = [SKPaymentQueue defaultQueue].transactions;
if (transactions.count > 0) {
SKPaymentTransaction* transaction = [transactions firstObject];
if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
}
}
// 检查设备是否支持内购
if ([SKPaymentQueue canMakePayments]) {
self.orderId = orderId;
self.productId = productId;
self.completion = completion;
NSArray *product = @[self.productId];
NSSet *set = [NSSet setWithArray:product];
//开始请求
self.request = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
self.request.delegate = self;
[self.request start];
}
2.在SKProductsRequestDelegate中处理请求到的商品数据,发起购买并监听购买过程
// 请求失败
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
if (self.completion) {
self.completion(kInAppPurchaseStatusFailed, nil);
}
}
// 请求成功
- (void)requestDidFinish:(SKRequest *)request {
}
- (void)productsRequest:(nonnull SKProductsRequest *)request didReceiveResponse:(nonnull SKProductsResponse *)response {
NSArray *products = response.products;
if([products count] == 0) {
if (self.completion) {
self.completion(kInAppPurchaseStatusIAPProductsError, nil);
}
return;
}
//在所有商品中,找到当前请求的商品
SKProduct *targetProduct = nil;
for (SKProduct *product in products) {
if([product.productIdentifier isEqualToString:self.productId]) {
targetProduct = product;
break;
}
}
if (!targetProduct) {
if (self.completion) {
self.completion(kInAppPurchaseStatusProductIDError, nil);
}
return;
}
[self showStatus:@"发起购买请求..."];
SKPayment *payment = [SKPayment paymentWithProduct:targetProduct];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
3.实现SKPaymentTransactionObserver代理,监听商品购买状态
- (void)paymentQueue:(nonnull SKPaymentQueue *)queue updatedTransactions:(nonnull NSArray *)transactions {
for(SKPaymentTransaction *transaction in queue.transactions){
self.transaction = transaction;
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchasing:
[self showStatus:@"正在交易..."];
break;
case SKPaymentTransactionStatePurchased:
if (self.completion) {
self.completion(kInAppPurchaseStatusSuccess, transaction.transactionIdentifier);
}
break;
case SKPaymentTransactionStateRestored:
if (self.completion) {
self.completion(kInAppPurchaseStatusIAPProductsRestored, nil);
}
[self finishTransaction];
break;
case SKPaymentTransactionStateFailed:
if (transaction.error.code == SKErrorPaymentCancelled) {
if (self.completion) {
self.completion(kInAppPurchaseStatusCancel, nil);
}
} else {
if (self.completion) {
self.completion(kInAppPurchaseStatusFailed, nil);
}
}
[self finishTransaction];
break;
default:
break;
}
}
}
- (void)finishTransaction {
if (self.transaction) {
[[SKPaymentQueue defaultQueue] finishTransaction:self.transaction];
self.transaction = nil;
} else {
[self finishLastPurchasedTransaction];
}
dispatch_async(dispatch_get_main_queue(), ^{
[WLInAppPurchaseDefine dismiss];
});
}
4.漏单情况处理
// 购买成功后:SKPaymentTransactionStatePurchased 本地存储订单号和transactionIdentifier
- (void)cacheCurrentOrderId:(NSString *)orderId andTransactionId:(NSString *)transactionId {
if (orderId <= 0 || transactionId.length <= 0) {
return;
}
NSDictionary *cacheDict = [[NSUserDefaults standardUserDefaults] objectForKey:kWLInAppPurchaseOrderCacheKey];
NSMutableDictionary *tempDict = [NSMutableDictionary dictionaryWithDictionary:cacheDict];
[tempDict setObject:transactionId forKey:orderId];
[[NSUserDefaults standardUserDefaults] setObject:tempDict.copy forKey:kWLInAppPurchaseOrderCacheKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
// 每次APP启动检查本地是否有未校验的订单号
- (void)checkCacheOrder:(NSString *)transactionId {
NSDictionary *cacheDict = [[NSUserDefaults standardUserDefaults] objectForKey:kWLInAppPurchaseOrderCacheKey];
if (cacheDict.allKeys.count > 0) {
__weak typeof(self) weakSelf = self;
[cacheDict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
__strong typeof(self) strongSelf = weakSelf;
if ([obj isEqualToString:transactionId]) {
strongSelf.orderId = key;
strongSelf.transactionId = transactionId;
[strongSelf verifyTransactionId:obj];
*stop = YES;
}
}];
} else {
[self.IAPRequest finishTransaction];
}
}
// 漏单校验
- (void)verifyTransactionId:(NSString *)transactionId {
// 服务器校验凭据
NSData *receiptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
NSString *receipt = [receiptData base64EncodedStringWithOptions:0];
if ([self.delegate respondsToSelector:@selector(verifyReceiptWithOrderId:transactionId:receipt:completion:)]) {
__weak typeof(self) weakSelf = self;
[self.delegate verifyReceiptWithOrderId:self.orderId transactionId:transactionId receipt:receipt completion:^(NSError * _Nonnull error, id _Nonnull data) {
__strong typeof(self) strongSelf = weakSelf;
if (!strongSelf) return;
if (!error) {
// NSLog(@"校验未完成的订单成功");
[strongSelf removeCacheOrder:strongSelf.orderId];
} else {
[strongSelf showToast:error.localizedDescription];
[self retryVerifyCurrentOrder];
}
[strongSelf.IAPRequest finishTransaction];
strongSelf.isProcessing = NO;
}];
}
}
// 服务器校验成功后删除存储数据
- (void)removeCacheOrder:(NSString *)orderId {
if (orderId <= 0) {
return;
}
NSDictionary *cacheDict = [[NSUserDefaults standardUserDefaults] objectForKey:kWLInAppPurchaseOrderCacheKey];
NSMutableDictionary *tempDict = [NSMutableDictionary dictionaryWithDictionary:cacheDict];
if ([tempDict.allKeys containsObject:orderId]) {
[tempDict removeObjectForKey:orderId];
[[NSUserDefaults standardUserDefaults] setObject:tempDict.copy forKey:kWLInAppPurchaseOrderCacheKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
}