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. 活动目录对象属性批量修改工具------ADModify

    最近下载了一个可以修改活动目录用户.组.联系人等活动目录对象属性的工具,试用了一下,发现还是蛮好用的,并且还可以修改对象的扩展属性,如:在活动目录中安装了Exchange 2003,Exchange ...

  2. liunx笔记

    Zolertia IPv6/6LoWPAN Ubidots client Son Han Border Router with Raspberry Pi for LLN with TelosBs Co ...

  3. excel提取数字

    部分提取,那么就用=-LOOKUP(,-MID(A1,MIN(FIND({0;1;2;3;4;5;6;7;8;9},A1&1234567890)),ROW($1:$1024))) ------ ...

  4. s曲线

    一. 原型 sigmoid 函数原型: 在 [-5, 5] 上的曲线是这个样子的: 二.X轴变形 如果我们希望加速更快一点,那么就需要对原型中的指数 -X 的系数进行改变.原型可以认为是 -(1 * ...

  5. C# winform中ListView用法

    this.listView1.GridLines = true; //显示表格线 this.listView1.View = View.Details;//显示表格细节 this.listView1. ...

  6. JS确认取消按钮使用

    前几天写程序用到了点击提交之后弹出一个信息框确认提交有取消和确定按钮查阅了资料记录一手 if(window.confirm('你确定要提交吗?提交后将无法更改!')){ //这里填写提交代码 retu ...

  7. Bean XML 配置(2)- Bean作用域与生命周期回调方法配置

    系列教程 Spring 框架介绍 Spring 框架模块 Spring开发环境搭建(Eclipse) 创建一个简单的Spring应用 Spring 控制反转容器(Inversion of Contro ...

  8. springboot - 映射 /error 到自定义且实现了ErrorController的Controller

    1.总览 2.代码 1).pom.xml <dependencies> <dependency> <groupId>org.springframework.boot ...

  9. HDU 5280 BestCoder Round #47 1001:Senior's Array

    Senior's Array  Accepts: 199  Submissions: 944  Time Limit: 2000/1000 MS (Java/Others)  Memory Limit ...

  10. windows driver 分配内存

    UNICODE_STRING str = {0}; wchar_t strInfo[] = {L"马上就是光棍节了"}; str.Buffer = (PWCHAR)ExAlloca ...