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. Python基础笔记:函数:调用函数、定义函数、函数的参数、递归函数

    一.定义一个求二元一次方程的根的函数 #Sublime Text import math def ee(a,b,c): delta=b*b-4*a*c if delta<0: return 'n ...

  2. SpringBoot+Jpa测试自增时报错Springboot-jpa Table 'sell.hibernate_sequence' doesn't exist

    解决办法: @GeneratedValue(strategy = GenerationType.IDENTITY) 如图所示:

  3. 八十五、SAP中的ALV创建之四,去掉主键背景色

    一.我们执行程序之后,发现表格中所有的内容都是有底色的 二.这个是因为我们在代码中规定了主键 三.我们去掉处理第一个之外的主键 四.运行程序,发现还是所有字段都有背景色 五.这是因为在每次ls_fie ...

  4. 018-PHP判断文件是否存在

    <?php print("data.txt文件是否存在?" . "<br>"); if (file_exists("data.txt ...

  5. 156-PHP strrpos和strripos函数

    <?php //定义两个字符串 $str='pasSword'; $position=strrpos($str,'s'); //不区分大小写判断 echo "字母S在{$str}中最后 ...

  6. ZOJ 3791 An easy game DP+组合数

    给定两个01序列,每次操作可以任意改变其中的m个数字 0变 1  1 变 0,正好要变化k次,问有多少种变法 dp模型为dp[i][j],表示进行到第i次变化,A,B序列有j个不同的 变法总和. 循环 ...

  7. 复选框全选、反选及根据值JS控制复选框默认选中事件

    HTML代码 <div class="col-sm-7"> <input type="checkbox" id="allAndNot ...

  8. 实验吧-web-天下武功唯快不破(Python中byte和str的转换)

    题目:看看响应头 打开网站,既然已经提示我们看响应头了,那我们就看看呗(习惯bp,也可直接F12查看) 可以看到,响应头部分有个FLAG,而且有提示:please post what you find ...

  9. Docker PHP 例子

    版权所有,未经许可,禁止转载 章节 Docker 介绍 Docker 和虚拟机的区别 Docker 安装 Docker Hub Docker 镜像(image) Docker 容器(container ...

  10. jedis哨兵模式的redis组(集群),连接池实现。(客户端分片)

    java 连接redis 我们都使用的 是jedis  ,对于redis这种频繁请求的场景我们一般需要对其池化避免重复创建,即创建一个连接池 ,打开jedis的 jar包我们发现,jedis对池已经有 ...