iOS开发之内购完全笔记(您已购买此 App 内购买项目。此项目将免费恢复。)

时间:2021-04-21 08:51:44

1、内购流程

  • 1、在 AppStore 中创建相应的物品,创建内购沙盒测试账号
  • 2、客户端从后台获取相应的物品 ID (当然也可以再客户端写死,但后期扩展性就受限制了)
  • 3、依据相应的物品 ID 请求商品的相关信息
  • 4、依据商品信息创建订单请求交易
  • 5、依据返回的订单状态处理交易结果
  • 6、请求后台再次验证订单状态
  • 7、依据后台返回结果处理相关逻辑

2、创建内购物品以及沙盒测试账号

3、客户端编写相关代码

  • 再这里我把和支付相关的逻辑都抽取到了一个单例中,在最后贴上个人梳理的相关代码大家一起学习

4、做内购过程中遇到的坑


  • 1、内购沙盒测试账号在支付成功后,再次购买相同 ID 的物品,会提示如下内容的弹窗。

    您已购买此 App 内购买项目。此项目将免费恢复


    解决方法:在使用

    [[SKPaymentQueue defaultQueue] addPayment:payment];

    将支付信息添加进苹果的支付队列后,苹果会自动完成后续的购买请求,在用户购买成功或者点击取消购买的选项后回调

     - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction;

    方法返回响应的结果信息,在该方法内除了得到响应的支付信息编写自身的业务的代码外还要记得调用

    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];

    方法通知苹果的支付队列该交易已经完成,否者就会已发起相同 ID 的商品购买就会有此项目将免费恢复的提示。


  • 2、每次启动一个新的内购支付流程,刚发起的时候系统就会调用- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction;这个方法,结果扰乱一部分的支付业务逻辑


    在 SKPaymentQueue 被启动并且添加了 addObserver之后,如果其判断到有未完成的交易,会主动调用paymentQueue updatedTransactions 这个方法来继续完成相关的交易流程,所以如果在上面那种情况下得到结果后不去调用 finish 接口,下次重新开启支付流程就会检查未完成的支付并调用该接口。

    解决方法:

    1.在得到支付结果后及时调用 finish 方法

    2.添加一个是否是新发起的支付流程的条件,在条件符合的情况下才触发应用的相关逻辑的代码

    PS:在拿到苹果的支付结果凭据的时候最好在客户端做一份持久化的数据备份,等待后台验证完成后再清除掉,避免出现验证中间出现问题导致用户支付成功但后台相关的增值处理没有完成导致用户金钱损失的问题)


  • 3、如何区分购买物品的是 沙盒测试账号 还是 真实账号 


    后台再验证支付凭据的时候要区分是沙盒测试账号购买的还是用户真实账号购买的,所以在传凭据的时候还需要告诉后台当前购买的账号性质。

    解决方法:通过在配置文件中定义相关的宏定义并结合 Debug 与 Release 的编译环境确定相关的参数

    // 苹果内购是否为沙盒测试账号,打开就代表为沙盒测试账号,注意上线时注释掉#define APPSTORE_ASK_TO_BUY_IN_SANDBOX 1		// 生成订单参数,注意沙盒测试账号与线上正式苹果账号的验证途径不一样,要给后台标明     NSNumber *sandbox;#if (defined(APPSTORE_ASK_TO_BUY_IN_SANDBOX) && defined(DEBUG))    sandbox = @(0);#else    sandbox = @(1);#endif

    个人没有找到相关的方法可以打完包后动态的检测购买物品的账号性质,希望知道的朋友分享一下,感谢 ^_^


5、iOS7 客户端验证的订单状态

  • 苹果在iOS7提升了购买凭据的安全性,可以直接单独在客户端完成订单正确性的验证,但是处于金钱考虑,购买完成后,建议还是要做凭据的后台验证工作。

    #pragma mark 客户端验证购买凭据
    - (void)verifyTransactionResult
    {
    // 验证凭据,获取到苹果返回的交易凭据
    // appStoreReceiptURL iOS7.0增加的,购买交易完成后,会将凭据存放在该地址
    NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
    // 从沙盒中获取到购买凭据
    NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
    // 传输的是BASE64编码的字符串
    /**
    BASE64 常用的编码方案,通常用于数据传输,以及加密算法的基础算法,传输过程中能够保证数据传输的稳定性
    BASE64是可以编码和解码的
    */
    NSDictionary *requestContents = @{
    @"receipt-data": [receipt base64EncodedStringWithOptions:0]
    };
    NSError *error;
    // 转换为 JSON 格式
    NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
    options:0
    error:&error];
    // 不存在
    if (!requestData) { /* ... Handle error ... */ }

    // 发送网络POST请求,对购买凭据进行验证
    NSString *verifyUrlString;
    #if (defined(APPSTORE_ASK_TO_BUY_IN_SANDBOX) && defined(DEBUG))
    verifyUrlString = @"https://sandbox.itunes.apple.com/verifyReceipt";
    #else
    verifyUrlString = @"https://buy.itunes.apple.com/verifyReceipt";
    #endif
    // 国内访问苹果服务器比较慢,timeoutInterval 需要长一点
    NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:[[NSURL alloc] initWithString:verifyUrlString] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10.0f];

    [storeRequest setHTTPMethod:@"POST"];
    [storeRequest setHTTPBody:requestData];

    // 在后台对列中提交验证请求,并获得官方的验证JSON结果
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [NSURLConnection sendAsynchronousRequest:storeRequest queue:queue
    completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
    if (connectionError) {
    NSLog(@"链接失败");
    } else {
    NSError *error;
    NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
    if (!jsonResponse) {
    NSLog(@"验证失败");
    }

    // 比对 jsonResponse 中以下信息基本上可以保证数据安全
    /*
    bundle_id
    application_version
    product_id
    transaction_id
    */

    NSLog(@"验证成功");
    }
    }];

    }


6、内购验证凭据返回结果状态码说明

  • 苹果反馈的状态码:

    21000 App Store无法读取你提供的JSON数据
    21002 收据数据不符合格式
    21003 收据无法被验证
    21004 你提供的共享密钥和账户的共享密钥不一致
    21005 收据服务器当前不可用
    21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中
    21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证
    21008 收据信息是产品环境中使用,但却被发送到测试环境中验证


更为详细的信息请参考

ios 应用内支付(In-App Purchase,沙盒测试,后台验证)

【IOS一气呵成】之IAP集成:内购和内购恢复

另附:苹果官网内购 API 链接

7、如何恢复购买

  • 注:此部分内容后期再详细添加 ^_^

备注:

相关代码:

XYPayManager.h

//
// XYPayManager.h
// xingyun
//
// Created by 郑亚恒 on 15/11/2.
// Copyright © 2015年 郑亚恒. All rights reserved.
//

#import <Foundation/Foundation.h>

// 苹果内购是否为沙盒测试账号,打开就代表为沙盒测试账号,注意上线时注释掉!!
#define APPSTORE_ASK_TO_BUY_IN_SANDBOX 1

typedef void(^payCompleteBlock)(NSDictionary *resultDic, BOOL isSuccess);

@interface XYPayManager : NSObject

+ (instancetype)sharedPayManager;

/// 苹果内购
- (void)requestAppleStoreProductDataWithString:(NSString *)productIdentifier payComplete:(payCompleteBlock)payCompletionBlock;
/// 验证苹果支付订单凭证
- (void)checkAppStorePayResultWithBase64String:(NSString *)base64String;

@end


XYPayManager.m

//
// XYPayManager.m
// xingyun
//
// Created by 郑亚恒 on 15/11/2.
// Copyright © 2015年 郑亚恒. All rights reserved.
//

#import "XYPayManager.h"
#import <StoreKit/StoreKit.h>

@interface XYPayManager() <SKPaymentTransactionObserver, SKProductsRequestDelegate>

// 苹果内购
@property (nonatomic, copy) NSString *appleProductIdentifier;
@property (nonatomic, copy) payCompleteBlock payComplete;

@end

@implementation XYPayManager

+ (instancetype)sharedPayManager {
static XYPayManager *payManager;
static dispatch_once_t once = 0;
dispatch_once(&once, ^{
payManager = [[XYPayManager alloc] init];
// 注册苹果内购
[[SKPaymentQueue defaultQueue] addTransactionObserver:payManager];

});
return payManager;
}

- (void)dealloc {
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}

#pragma mark - 苹果支付充值
//请求商品
- (void)requestAppleStoreProductDataWithString:(NSString *)productIdentifier payComplete:(payCompleteBlock)payCompletionBlock {
if(![SKPaymentQueue canMakePayments]) {
NSLog(@"不允许程序内付费");
// [APPCONTEXT.hudHelper showHudOnWindow:@"不允许程序内付费" image:nil acitivity:NO autoHideTime:DEFAULTTIME];

return;
}

NSLog(@"-------------请求对应的产品信息----------------");
self.startBuyAppleProduct = YES;
self.payComplete = payCompletionBlock;
self.appleProductIdentifier = productIdentifier;

NSLog(@"生成产品信息");
NSArray *product = [[NSArray alloc] initWithObjects:productIdentifier, nil];
NSSet *nsset = [NSSet setWithArray:product];
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
request.delegate = self;
[request start];

}

//收到产品返回信息
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{

NSLog(@"--------------收到产品反馈消息---------------------");
NSArray *productArray = response.products;
if([productArray count] == 0){
NSLog(@"--------------没有商品------------------");
return;
}

NSLog(@"productID:%@", response.invalidProductIdentifiers);
NSLog(@"产品付费数量:%lu",(unsigned long)[productArray count]);

SKProduct *product = nil;
for (SKProduct *pro in productArray) {
NSLog(@"%@", [pro description]);
NSLog(@"%@", [pro localizedTitle]);
NSLog(@"%@", [pro localizedDescription]);
NSLog(@"%@", [pro price]);
NSLog(@"%@", [pro productIdentifier]);

if([pro.productIdentifier isEqualToString:self.appleProductIdentifier]){
product = pro;
}
}

SKPayment *payment = [SKPayment paymentWithProduct:product];

NSLog(@"发送购买请求");
[[SKPaymentQueue defaultQueue] addPayment:payment];
}

//请求失败
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
NSLog(@"------------------错误-----------------:%@", error);
}

- (void)requestDidFinish:(SKRequest *)request{
NSLog(@"------------反馈信息结束-----------------");
}

//监听购买结果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction {
for(SKPaymentTransaction *paymentTransactionp in transaction){

switch (paymentTransactionp.transactionState) {
case SKPaymentTransactionStatePurchased:
{
NSLog(@"交易完成-restoreCompletedTransactions");
/* your code */
[self buyAppleStoreProductSucceedWithPaymentTransactionp:paymentTransactionp];

[self completeTransaction:paymentTransactionp];
}

break;

case SKPaymentTransactionStatePurchasing:
NSLog(@"商品添加进列表");

break;

case SKPaymentTransactionStateRestored:
NSLog(@"已经购买过商品");

break;

case SKPaymentTransactionStateFailed:
{
NSLog(@"交易失败");
/* your code */
[self completeTransaction:paymentTransactionp];
}
break;
}
}
}

// 苹果内购支付成功
- (void)buyAppleStoreProductSucceedWithPaymentTransactionp:(SKPaymentTransaction *)paymentTransactionp {

/* 获取相应的凭据,并做 base64 编码处理 */
NSString *base64Str = [paymentTransactionp.transactionReceipt base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
NSLog(@"苹果内购凭据号\n\n\n\n\n\n%@\n\n\n\n\n\n",base64Str);

[self checkAppStorePayResultWithBase64String:base64Str];
}


- (void)checkAppStorePayResultWithBase64String:(NSString *)base64String {

/* 生成订单参数,注意沙盒测试账号与线上正式苹果账号的验证途径不一样,要给后台标明 */
NSNumber *sandbox;
#if (defined(APPSTORE_ASK_TO_BUY_IN_SANDBOX) && defined(DEBUG))
sandbox = @(0);
#else
sandbox = @(1);
#endif

NSMutableDictionary *prgam = [[NSMutableDictionary alloc] init];;
[prgam setValue:sandbox forKey:@"sandbox"];
[prgam setValue:base64String forKey:@"reciept"];

/*
请求后台接口,服务器处验证是否支付成功,依据返回结果做相应逻辑处理
*/

}

//交易结束
- (void)completeTransaction:(SKPaymentTransaction *)transaction{
NSLog(@"交易结束");
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

@end