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. Fedora 19安装以后的优化

    Fedora 19安装以后的优化 转载自 http://www.zhukun.net/archives/6614 最近安装了Fedora 19 32bit,以下是一些优化配置,使之更适合国人使用. 1 ...

  2. CSS的position属性:relative和absolute

    relative:是相对于自己来定位的,例如:#demo{position:relative;top:-50px;},这时#demo会在相对于它原来的位置上移50px.如果它之前的元素也为relati ...

  3. systemctl无法停掉keepalived

    这个问题搞了好半天,记录一下,启停都是用的systemctl 起初是测试vip漂移时候发现,主备节点都开启keepalived的状况下,一切正常,主节点的vip也可以访问. 第一次停掉主节点的keep ...

  4. 《新标准C++程序设计》3.6-3.7(C++学习笔记9)

    一.成员对象和封闭类 (1)定义 一个类的成员变量如果是另一个类的对象,就称之为“成员对象”. 包含成员对象的类叫封闭类. (2)封闭类构造函数的初始化列表 在构造函数中添加初始化列表的写法: 类名: ...

  5. Docker部署Python应用程序

    Docker部署Python应用程序 1. 单个py文件部署 生成Dockerfile 文件 插件用的豆瓣的镜像,,重置时间(容器的默认时间是UTC时间与宿主机的相差8小时). 文中需要三个插件(pe ...

  6. kafka cmd with ssl

    set PATH=C:\Program Files\Java\jdk1.8.0_201\bin;@call kafka-consumer-groups.bat --bootstrap-server l ...

  7. HDU 1576:A/B

    A/B Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Submis ...

  8. hibernate.hbm.xml必须必须配置主键

    hibernate.hbm.xml必须必须配置主键 <id name="XXid" type="java.lang.long" column=" ...

  9. 汇编,寄存器,内存,mov指令

    一.代码 和 汇编 和 二进制之间的关系 二.复习一下计算机组成原理的知识 1.寄存器 计算机中有三个存储 32位cpu提供的寄存器有三种类型8位 16位 32位 64位的只是32位的扩展 并且程序大 ...

  10. StringUtils.format用法

    String.format()字符串常规类型格式化的两种重载方式 format(String format, Object… args) 新字符串使用本地语言环境,制定字符串格式和参数生成格式化的新字 ...