iOS 内购相关

下面总结一下过往订阅和内购的项目的代码方面的实现细节和注意事项,特别是掉单方面的处理。

后台的协议、商品ID、银行卡、内购类型、沙盒账号测试人员都由运营或者产品在苹果后台中申请处理。

这里主要讲内购的代码,内购的代码主要分为两大部分:商品的查询、商品的购买。

1、首先先创建一个单例,创建单例的第一时间同时要加上对苹果订单状态变化的监听[[SKPaymentQueue defaultQueue] addTransactionObserver:self];这样所有的历史订单都会回调过来,包括已经订阅的、订阅中的、已经内购的、正在内购中的订单,回调在这个方法- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions,苹果会回调历史的订单过来,初始化之后添加监听会有一次机会处理掉单的问题。

+ (instancetype)shareManager {
static PAIAPPurchaseManager *obj = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
obj = [[PAIAPPurchaseManager alloc] init];
});
return obj;
} - (instancetype)init {
self = [super init];
if (self) {
_purchaseDic = [NSMutableDictionary dictionary];
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
return self;
}

1.1 苹果后台正式环境和沙盒环境的验单链接

//内购产品相关属性
static NSString *const IAP_SANDBOX_URL = @"https://sandbox.itunes.apple.com/verifyReceipt";
static NSString *const IAP_APPSTORE_URL = @"https://buy.itunes.apple.com/verifyReceipt";

2、在appdelegate中初始化查询商品详情,可以把对应的商品ID写在本地。

/*
@param dict 获取本地的商品字典
*/
- (void)initProductWithLocalDict:(NSDictionary *)dict {
if (dict == nil) {
return;
} _requestType = PARequestTypeProductDetail;
NSLog(@"initProductWithLocalDict------------>%@",dict);
PAIAPProductModel *yearModel = [PAIAPProductModel yy_modelWithDictionary:dict[kPAPurchaseYearProductKey]];
PAIAPProductModel *monthModel = [PAIAPProductModel yy_modelWithDictionary:dict[kPAPurchaseMonthProductKey]];
PAIAPProductModel *weekModel = [PAIAPProductModel yy_modelWithDictionary:dict[kPAPurchaseWeekProductKey]]; [self.purchaseDic setValue:yearModel forKey:kPAPurchaseYearProductKey];
[self.purchaseDic setValue:monthModel forKey:kPAPurchaseMonthProductKey];
[self.purchaseDic setValue:weekModel forKey:kPAPurchaseWeekProductKey];
NSMutableArray *productIdArr = [NSMutableArray array];
[productIdArr addObject:yearModel.product_id];
[productIdArr addObject:monthModel.product_id];
[productIdArr addObject:weekModel.product_id]; [self getPurcaseProductPriceWithProductIDs:productIdArr];
} - (void)getPurcaseProductPriceWithProductIDs:(NSArray *)productIDs {
NSLog(@"getPurcaseProductPriceWithProductIDs");
if (productIDs.count == 0) {
return;
}
if ([SKPaymentQueue canMakePayments]) {
NSArray *products = [NSArray arrayWithArray:productIDs];
NSSet *set = [NSSet setWithArray:products];
SKProductsRequest *paymentRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
paymentRequest.delegate = self; [paymentRequest start];
}
}

3、在发起商品查询的请求之后会走下面的回调方法,需要注意的是商品的购买和商品的查询都会走下面这个回调方法,所以要通过某个状态区分它是商品购买过来的回调还是商品查询过来的回调,这里不建议用枚举的形式来判断,因为如果用户手速比较快,在你初始化查询商品信息的回调还没过来,你就点击了购买之后的话,那么这两个回调你是区分不出哪个是查询商品,哪个是商品购买的。这里的解决方案是通过判读request是查询的request还是购买的request来区分。详细代码如下:

#pragma mark - ------SKProductsRequestDelegate------
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
NSLog(@"查询商品");
NSArray *products = response.products; if (self.pruductMsgRequest == request) {
[products enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
SKProduct *product = (SKProduct *)obj; for (NSInteger i=0; i<self.purchaseDic.allValues.count; i++) {
PAIAPProductModel *model = self.purchaseDic.allValues[i];
if ([model.product_id isEqualToString:product.productIdentifier]) { model.currency_code = [product.priceLocale objectForKey:NSLocaleCurrencySymbol];
model.price = [product.price floatValue];
model.product_id = product.productIdentifier;
NSLog(@"product_id:%@,currency_code:%@,price:%.2f",model.product_id,model.currency_code,model.price);
}
}
}]; //把获取产品详细信息传递给委托
if (self.productUpdateBlock) {
self.productUpdateBlock();
}
}else if (self.purchaseRequest == request) {
SKProduct *purchaseProduct; for (NSInteger i=0; i<products.count; i++) {
purchaseProduct = products[i];
NSLog(@"商品信息:productId:%@,price:%@",purchaseProduct.productIdentifier,purchaseProduct.price);
if ([purchaseProduct.productIdentifier isEqualToString:self.productId]) {
break;
}
} if (purchaseProduct == nil) {
NSLog(@"商品信息为空,找不到对应的商品");
if (self.purchaseFailureBlock) {
self.purchaseFailureBlock(kString(@"purchase_State_GoodsEmpty"));
[PAStatistics.operationCode(@"purchase_product_not_found").statisticsObject(self.productId) upload104Error];
}
return;
} SKPayment *payment = [SKPayment paymentWithProduct:purchaseProduct];
[[SKPaymentQueue defaultQueue] addPayment:payment];
} } - (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
NSLog(@"request product fail"); if (self.pruductMsgRequest == request) { }else if (self.purchaseRequest == request) {
if (self.purchaseFailureBlock) {
self.purchaseFailureBlock(error.localizedDescription);
}
} } - (void)requestDidFinish:(SKRequest *)request { }

在上面的代码中,查询到商品在不同的appleID(比如说美区的账号就显示美元)对应的价钱之后就可以通过block的形式回调刷新本地的价钱了。购买商品的回调中,查询到对应的商品之后就创建一笔订单,添加到购买的队列中,然后购买的流程就会走另外一套回调方法。

#pragma mark - ------SKPaymentTransactionObserver------
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
NSLog(@"paymentQueue updatedTransactions");
for (SKPaymentTransaction *paymentTransaction in transactions) {
SKPaymentTransactionState paymentTransactionState = paymentTransaction.transactionState; switch (paymentTransactionState) {
case SKPaymentTransactionStatePurchasing: {
NSLog(@"paymentQueue SKPaymentTransactionStatePurchasing --------->"); NSArray* transactionArr = [SKPaymentQueue defaultQueue].transactions;
if (transactionArr.count > 0) {
// 检测是否有内购未完成的订单
for (SKPaymentTransaction* transaction in transactionArr) {
if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
//保存在本地
//把交易凭证保存在本地
[self saveReceiptPurcaseAtSandboxWithTransaction:paymentTransaction];
[[SKPaymentQueue defaultQueue] finishTransaction:paymentTransaction];
}
}
} break;
}
case SKPaymentTransactionStatePurchased: {
NSLog(@"paymentQueue SKPaymentTransactionStatePurchased --------->");
//保存在本地
//把交易凭证保存在本地
[self saveReceiptPurcaseAtSandboxWithTransaction:paymentTransaction];
[[SKPaymentQueue defaultQueue] finishTransaction:paymentTransaction];
break;
}
case SKPaymentTransactionStateFailed: {
//购买失败
[self failPurcaseWithTransaction:paymentTransaction];
break;
}
case SKPaymentTransactionStateRestored: {
NSLog(@"paymentQueue SKPaymentTransactionStateRestored --------->");
[self restorePurcaseWithTransaction:paymentTransaction];
break;
}
default:
[[SKPaymentQueue defaultQueue] finishTransaction:paymentTransaction];
break;
}
}
} - (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue {
NSLog(@"paymentQueueRestoreCompletedTransactionsFinished--------->"); [self sendRestoreRequest];
} - (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error {
NSLog(@"restoreCompletedTransactionsFailedWithError--------->");
if (self.restoreFailureBlock) {
self.restoreFailureBlock(error.localizedDescription);
}
}
- (void)failPurcaseWithTransaction:(SKPaymentTransaction *)transaction {
NSLog(@"failPurcaseWithTransaction error code : %ld", transaction.error.code);
switch (transaction.error.code) {
case SKErrorUnknown: {
if (self.purchaseFailureBlock) {
self.purchaseFailureBlock(transaction.error.localizedDescription);
}
break;
}
case SKErrorPaymentCancelled: {
if (self.purchaseFailureBlock) {
self.purchaseFailureBlock([self purcaseMsgWithPurcaseState:PAPurchaseStateUserCancel]); }
break;
}
case SKErrorPaymentNotAllowed: { if (self.purchaseFailureBlock) {
self.purchaseFailureBlock([self purcaseMsgWithPurcaseState:PAPurchaseStateNoRight]);
} break;
}
default: { if (self.purchaseFailureBlock) {
self.purchaseFailureBlock([self purcaseMsgWithPurcaseState:PAPurchaseStateBuyFailed]);
}
break;
}
} [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

上面在购买中之所以还要遍历[SKPaymentQueue defaultQueue].transactions;数组,是因为在购买的时候会触发这里的回调方法,在这里又提供多了一次处理掉单的机会。而且要用[SKPaymentQueue defaultQueue].transactions;这个数组,这个数组包含所有的历史订单,回调方法回调过来的数组只是包含部分订单,不一定准确。

4、向苹果后台发起验单,这个可以在客户端上验单也可以在服务器上验单。在客户端上验单的话会增加被篡改的风险,所以如果条件允许还是服务器验单会比较好,而且客户端和服务端之间定一套加密的规则,降低订单凭证被篡改的风险。下面给的是客户端本地验单的逻辑:

//保存购买记录到沙盒
- (void)saveReceiptPurcaseAtSandboxWithTransaction:(SKPaymentTransaction *)transaction {
NSLog(@"saveReceiptPurcaseAtSandboxWithTransaction start ---->"); WS(ws);
[self checkReceiptFromAppStoreWithURL:[NSURL URLWithString:IAP_SANDBOX_URL] success:^(NSData *data) {
[ws savePurcaseDetailAtDocumentWithData:data];
} failure:^(NSError *error) {
if (error.code == 21008) {
__weak typeof(ws) wws = ws;
[ws checkReceiptFromAppStoreWithURL:[NSURL URLWithString:IAP_APPSTORE_URL] success:^(NSData *data) {
[wws savePurcaseDetailAtDocumentWithData:data];
} failure:^(NSError *error) {
if (wws.purchaseFailureBlock) {
wws.purchaseFailureBlock(error.localizedDescription);
wws.purchaseFailureBlock = nil;
}
}];
} else { if (ws.purchaseFailureBlock) {
ws.purchaseFailureBlock(error.localizedDescription);
ws.purchaseFailureBlock = nil;
}
} }];
} - (void)savePurcaseDetailAtDocumentWithData:(NSData *)data {
NSLog(@"savePurcaseDetailAtDocumentWithData start ---->"); NSDictionary *receipt = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil]; PAIAPReceiptModel *receiptModel = [[PAIAPReceiptModel alloc] init];
receiptModel.environment = receipt[@"environment"]; NSDictionary *receiptInfo = [self getMostValuableRecepitWithReceiptDict:receipt];
receiptModel.originalPurcaseDate = [NSDate UTCDateFormETCStr:receiptInfo[@"original_purchase_date"]];
receiptModel.purcaseDate = [NSDate UTCDateFormETCStr:receiptInfo[@"purchase_date"]];
receiptModel.expiresDate = [NSDate UTCDateFormETCStr:receiptInfo[@"expires_date"]];
receiptModel.productID = receiptInfo[@"product_id"];
receiptModel.webOrderID = receiptInfo[@"original_transaction_id"];
NSLog(@"<parseReceiptSavePurcaseDetailAtDocumentWithData> -------> productId : %@ expires_date : %@", receiptModel.productID, receiptModel.expiresDate); [PAFileManager savePurchaseReceiptData:receiptModel];
self.productId = nil;
NSString *remark = [PAAppUserDataManager user104Remark]; [[PAAppUserDataManager sharedInstance] refreshUserPaymentState]; if ([PAAppUserDataManager sharedInstance].userPaymentState == PAUserPaymentStatusVIP && self.requestType == PARequestTypePurcase) {
NSLog(@"上传订单追踪"); } if (self.purchaseSuccessBlock) {
self.purchaseSuccessBlock(receiptModel,remark);
}
}
- (NSDictionary *)getMostValuableRecepitWithReceiptDict:(NSDictionary *)receipt{
NSDictionary *mostValueRecepit;
for( NSDictionary *temp in receipt[@"latest_receipt_info"]) {
NSDate *expiresDate = [NSDate UTCDateFormETCStr:temp[@"expires_date"]];
// NSString *productID = temp[@"product_id"];
NSLog(@"<getMostValuableRecepitWithReceiptDict> Recepit data ------> : %@", temp);
if (mostValueRecepit == nil) {
mostValueRecepit = temp;
} else {
NSDate *recepitExpiresDate = [NSDate UTCDateFormETCStr:mostValueRecepit[@"expires_date"]];
if ([expiresDate compare:recepitExpiresDate] != NSOrderedAscending) {
mostValueRecepit = temp;
}
}
}
// NSLog(@"<getMostValuableRecepitWithReceiptDict> mostValueRecepit : %@", mostValueRecepit);
return mostValueRecepit;
}

验单成功之后保存凭证在本地,每次启动的时候通过过期日期判断用户是否为VIP用户就可以了。

5、另外还有恢复购买的逻辑,也比较简单,下面贴上代码,看看应该就明白了。

/*
* 恢复订阅
*/
- (void)restorePurchaseSuccess:(RestoreSuccessBlock)successBlock failure:(RestoreFailureBlock)failBlock{
NSLog(@"restorePurchase"); self.restoreSuccessBlock = successBlock;
self.restoreFailureBlock = failBlock; [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; } - (void)sendRestoreRequest {
NSLog(@"sendRestoreRequest");
WS(ws);
[self checkReceiptFromAppStoreWithURL:[NSURL URLWithString:IAP_SANDBOX_URL] success:^(NSData *data) {
[ws restoreCheckReceiptWithData:data];
} failure:^(NSError *error) {
if (error.code == 21008) {
__weak typeof(ws) wws = ws;
[ws checkReceiptFromAppStoreWithURL:[NSURL URLWithString:IAP_APPSTORE_URL] success:^(NSData *data) {
[wws restoreCheckReceiptWithData:data];
} failure:^(NSError *error) {
if (error.code == 99999) {
if (wws.restoreFailureBlock) {
wws.restoreFailureBlock(kString(@"payment_restore_fail"));
}
} else {
if (wws.restoreFailureBlock) {
wws.restoreFailureBlock(error.localizedDescription);
}
}
}];
} else {
if (error.code == 99999) {
if (ws.restoreFailureBlock) {
ws.restoreFailureBlock(kString(@"payment_restore_fail"));
}
} else {
if (ws.restoreFailureBlock) {
ws.restoreFailureBlock(error.localizedDescription);
}
}
}
}];
} - (void)restoreCheckReceiptWithData:(NSData *)data {
NSLog(@"restoreCheckReceiptWithData");
if (data == nil) {
NSLog(@"restoreCheckReceiptWithData data is null");
if (self.restoreFailureBlock) {
self.restoreFailureBlock(kString(@"payment_restore_fail"));
}
return;
} PAIAPReceiptModel *receiptModel = [[PAIAPReceiptModel alloc] init];
NSDictionary *receipt = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
receiptModel.environment = receipt[@"environment"];
NSDictionary *receiptInfo = [self getMostValuableRecepitWithReceiptDict:receipt];
receiptModel.expiresDate = [NSDate UTCDateFormETCStr:receiptInfo[@"expires_date"]];
receiptModel.productID = receiptInfo[@"product_id"];
receiptModel.webOrderID = receiptInfo[@"original_transaction_id"]; [PAFileManager savePurchaseReceiptData:receiptModel]; PAUserPaymentStatus status = [self getUserPaymentStateWithProductId:receiptModel.productID expiresDate:receiptModel.expiresDate];
NSString *remark = [PAAppUserDataManager user104Remark]; [[PAAppUserDataManager sharedInstance] refreshUserPaymentState]; if (status == PAUserPaymentStatusVIP) {
if (self.restoreSuccessBlock) {
self.restoreSuccessBlock(receiptModel,remark);
}
}else {
if (self.restoreFailureBlock) {
self.restoreFailureBlock(kString(@"payment_restore_fail"));
}
} }

注意:

另外倘若一个项目中继承了内购和订阅的话,验单的时候可以在

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions 方法中判断paymentTransaction.payment.productIdentifier商品ID的归属的方法来判断是内购还是订阅。

iOS 内购相关的更多相关文章

  1. iOS - 内购总结

        如果有人以后要在做内购这一块.希望可以好好的阅读这篇文章,虽然不是字字珠玑.但是也是本人亲人趟过了无数的坑,希望可以对大家有所帮助!  下面是在研究工程中遇到的问题(iOS 内购的流程如下 1 ...

  2. IOS内购支付server验证模式

    IOS 内购支付两种模式: 内置模式 server模式 内置模式的流程: app从app store 获取产品信息 用户选择须要购买的产品 app发送支付请求到app store app store ...

  3. IOS内购支付服务器验证模式

    IOS 内购支付两种模式: 内置模式 服务器模式 内置模式的流程: app从app store 获取产品信息 用户选择需要购买的产品 app发送支付请求到app store app store 处理支 ...

  4. Unity苹果(iOS)内购接入(Unity内置IAP)

    https://www.jianshu.com/p/4045ebf81a1c Unity苹果(iOS)内购接入(Unity内置IAP) Kakarottog                       ...

  5. iOS 内购遇到的坑

    一.内购沙盒测试账号在支付成功后,再次购买相同 ID 的物品,会提示如下内容的弹窗.您以购买过此APP内购项目,此项目将免费恢复 原因: 当使用内购购买过商品后没有把这个交易事件关,所以当我们再次去购 ...

  6. 苹果IOS内购二次验证返回state为21002的坑

    项目是三四年前的老项目,之前有IOS内购二次验证的接口,貌似很久都没用了,然而最近IOS的妹子说接口用不了,让我看看啥问题.接口流程时很简单的,就是前端IOS在购买成功之后,接收到receipt后进行 ...

  7. iOS 内购讲解

    一.总说内购的内容 1.协议.税务和银行业务 信息填写 2.内购商品的添加 3.添加沙盒测试账号 4.内购代码的具体实现 5.内购的注意事项 二.协议.税务和银行业务 信息填写 2.1.协议.税务和银 ...

  8. IOS内购--后台PHP认证

    参考网址:https://blog.csdn.net/que_csdn/article/details/80861408 http://www.php.cn/php-weizijiaocheng-39 ...

  9. IOS - 内购

    内购的五种产品类别 •非消耗品(Nonconsumable)买了就有,头衔,功能 –指的是在游戏中一次性购买并拥有永久访问权的物品或服务.非消耗品物品可以被用户再次下载,并且能够在用户的所有设备上使用 ...

随机推荐

  1. QObject的timerEvent使用

    其实在QT里,我们自己写的类一般通常需要继承QObject类,因为这一类里规定好的拿来即可用的方法. 比如信号,槽,以及前一个博文写的movetothread方法,这里就是介绍一个QObject的ti ...

  2. Python序列内单双引的问题——未解决

    在学习python基础的时候,遇到这样一个问题: tuple=(2,2.3,"yeah",5.6,False)list=[True,5,"smile"] 这样输 ...

  3. vuejs+thinkphp5+phpsocketIO+timer数据及时更新

    1.安装thinkphp5.0以上版本包含workerman框架2.composer安装composer require workerman/phpsocket.io3.vue中调用需要加载weapp ...

  4. centos7如何修改IP地址

    步骤1:使用vi编辑 /etc/sysconfig/network-scripts/目录下的ifcfg-ens160 配置文件 [root@model ~]# [root@model ~]# vi / ...

  5. 了解C#

    了解C C#能编写那些程序 Windows桌面应用程序 桌面应用有自己独立的进程与操作系统进行消息通讯,操作系统对事件进行检测,传递给桌面应用进程,桌面应用进程对这些消息进行解释,处理后,把处理结果u ...

  6. segger rtt 输出 log

    调试 mcu 的时候,使用 jlink 的 rtt 可以方便的输出信息. 输出有两种模式, client 和 logger. client 必须依附其他程序,比如说 mdk 调试状态,或者 logge ...

  7. Python PIP包管理器

    版权所有,未经许可,禁止转载 章节 Python 介绍 Python 开发环境搭建 Python 语法 Python 变量 Python 数值类型 Python 类型转换 Python 字符串(Str ...

  8. category添加属性

    category添加属性 面试题 Category的实现原理,以及Category为什么只能加方法不能加属性. Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗 ...

  9. c++程序—字符串

    C风格字符串:char 变量名[ ]="字符串值 " int main() { char str[] = "hello world!"; cout <&l ...

  10. 每天一点点之vue框架开发 - 引入Jquery

    1. 安装jquery npm install jquery --save-dev 2.在build/webpack.base.conf.js中添加如下内容 var webpack = require ...