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. NAND厂商哭晕:减产也阻止不了跌价

    导读 NAND闪存价格已经连跌了6个季度,这让上游NAND厂商三星.东芝.美光等损失惨重,纷纷削减NAND产能.在群联台北电脑展上,群联公司董事长潘建成也预测NAND闪存价格已经跌破了成本,未来跌幅会 ...

  2. 51nod 1206:Picture 求覆盖周长

    1206 Picture 题目来源: IOI 1998 基准时间限制:2 秒 空间限制:131072 KB 分值: 160 难度:6级算法题  收藏  关注 给出平面上的N个矩形(矩形的边平行于X轴和 ...

  3. maven的概念模型及maven坐标

     1.概念模型 项目对象模型:一个maven工程有一个pom.xml文件,通过pom.xml文件定义项目的坐标.项目依赖.项目信息.插件目标等. 依赖管理系统:通过maven的依赖管理对项目所依赖的j ...

  4. 计算机操作系统学习(一) Linux常用指令(随时更新)

    1.chmod 以下转载至https://blog.csdn.net/summer_sy/article/details/70142475 chmod u+x file.sh 就表示对当前目录下的fi ...

  5. 七、JavaScript之console.log输出和document.write输出

    一.代码如下 二.运行效果如下 三.点击之后,效果如下 四.按一下F12,在控制台中可以看到

  6. 留学生如何把控好Essay写作结构

    留学生在国内写过作文,但是对于essay写作到底了解多少呢?大家觉得essay写作太难是语言问题,但是大家要明白,老师对于内容的考察远重于对语言的考察.同学们的essay写作如果能做到言之有理,自圆其 ...

  7. POJ 1068:Parencodings

    Parencodings Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 22849   Accepted: 13394 De ...

  8. CSS font-family 各字体一览表

    windows常见内置中文字体字体中文名 字体英文名宋体                      SimSun(浏览器默认) 黑体                      SimHei 微软雅黑 ...

  9. 吴裕雄--天生自然JAVA SPRING框架开发学习笔记:Spring DI(依赖注入)的实现方式属性注入和构造注入

    依赖注入(Dependency Injection,DI)和控制反转含义相同,它们是从两个角度描述的同一个概念. 当某个 Java 实例需要另一个 Java 实例时,传统的方法是由调用者创建被调用者的 ...

  10. spring boot rest 接口集成 spring security(2) - JWT配置

    Spring Boot 集成教程 Spring Boot 介绍 Spring Boot 开发环境搭建(Eclipse) Spring Boot Hello World (restful接口)例子 sp ...