iOS-内购(2)

2,082 阅读7分钟

前言

咱们接着上次的内容来进行讨论和相互学习

前期的配置

appstoreContent中配置商品 --> appstoreContent中配置沙盒测试人员

这里的流程无需过多赘叙,基本都是照着文档一步一步来即可。参考<APP内购买项目>

内购流程

获取商品信息 --> 客户端发起内购请求 --> 将凭证发给服务器做二次验证 --> 服务器修改用户数据库商品信息并回调给客户端 --> 客户端刷新本地数据以及更新UI

获取商品信息

/**  
    @param productIdentifiers = 商品的productID
*/
NSSet * set = [NSSet setWithArray:productIdentifiers];
SKProductsRequest * request = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
request.delegate = self;
[request start];

执行完该操作之后可以通过代理的回调接受结果

/**
    @abstract  获取商品信息的成功回调
    @discussion 在ios13之后,该回调会在异步线程中执行,需要特别注意!!!
*/
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
    NSLog(@"有效的商品信息 %@",response.products);
    NSLog(@"无效的商品ID %@",response.invalidProductIdentifiers);
    
    SKProduct *product = response.products.firstObject;
    NSNumberFormatter *clearFormatter = [[NSNumberFormatter alloc] init];
    CGFloat price = [[clearFormatter stringFromNumber:product.price] floatValue];
    NSLog(@"商品价格 %f",price);
    NSLog(@"商品的货币国家 %@",product.priceLocale.countryCode);
}
/**
    @abstract 获取商品信息的失败回调
    @discussion 在ios13之后,该回调会在异步线程中执行,需要特别注意!!!
*/    
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
{
    NSLog(@"失败 %@",error);
}

客户端发起内购请求

/**
    @abstract 发起内购请求
    @param product:商品
*/ 
- (void)startPaymentWithProduct:(SKProduct *)product {
    // 根据SKProduct来创建一个交易请求
    SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
    //applicationUsername是一个透传的字段,当支付成功之后会原封不动的回给到我们。一般我们能够传递我们订单号过去。
    payment.applicationUsername = @"myOrderID";
    // 将交易请求放入交易队列
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}

接受内购回调,并将凭证发给服务器做二次验证

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
    for (SKPaymentTransaction *transaction in transactions) {
        SKPayment *payment = transaction.payment;
        NSString *productIdentifier = payment.productIdentifier;
        NSLog(@"transactions. ProductID:%@ --- TransactionIdentifier:%@", productIdentifier,transaction.transactionIdentifier);
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchasing:
                NSLog(@"购买中");
                break;
            case SKPaymentTransactionStatePurchased:
                NSLog("购买请求回调成功,开始处理...");
                //获取透传字段
                NSString *orderID = transation.payment.applicationUsername;
                //transactionIdentifier:相当于Apple的订单号
                NSString *transationId = transation.transactionIdentifier;
                NSLog(@"orderID = %@, 交易ID = %@", orderNo, transationId);
                //从沙盒中获取交易凭证
                NSData *reciptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
                //转化成Base64字符串(用于校验)
                NSString *reciptString = [reciptData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
                //传给后台做二次验证
                [self checkReceipt:reciptString];                
                break;
            case SKPaymentTransactionStateFailed:
                NSLog("购买请求回调失败,开始处理...");
                break;
            case SKPaymentTransactionStateRestored:
                NSLog("恢复购买请求回调成功,开始处理...");
                break;
            case SKPaymentTransactionStateDeferred:
                NSLog("恢复购买请求回调失败,开始处理...");
                break;
            case SKPaymentTransactionStateDeferred:
                NSLog(@"交易推迟, 等待外部操作");
                //交易推迟
                //官方解释是:交易已经加入队列,但是需要等待外部操作
                //主要用于儿童模式,需要询问家长同意。这种情况下不能关闭订单(完成交易),否则这类充值将无法处理。                
            break;    
            default:
                break;
        }
    }
}

服务器二次验证

文档传送门 使用App Store验证收据

  • 我们把recipt经过Base64编码之后,传给Apple的验证服务器进行验证(格式如下:)
{"receipt-data": 你编码过的recipt}
  • 服务器验证的URL

sandbox.itunes.apple.com/verifyRecei… 是沙盒环境的验证地址。 buy.itunes.apple.com/verifyRecei… 是正式环境的验证地址

  • Apple返回的完整数据如下
    响应参数含义传送门 响应参数
{
  "status": 0,  // 状态码
  "environment": "Sandbox"  // 收据环境
  "receipt": {
      "receipt_type": "ProductionSandbox",  // 收据类型。这里是沙盒的收据
      "adam_id": 0,
      "app_item_id": 0,
      "bundle_id": "com.tes.buildID", // 应用buildID
      "application_version": "1.5.0", // 应用版本号
      "download_id": 0,
      "version_external_identifier": 0,
      "receipt_creation_date": "2018-06-28 14:08:26 Etc/GMT",  // 本次收据创建时间
      "receipt_creation_date_ms": "1530194906000",
      "receipt_creation_date_pst": "2018-06-28 07:08:26 America/Los_Angeles",
      "request_date": "2018-08-05 04:50:58 Etc/GMT", // 请求时间
      "request_date_ms": "1533444658147",
      "request_date_pst": "2018-08-04 21:50:58 America/Los_Angeles",
      "original_purchase_date": "2013-08-01 07:00:00 Etc/GMT",
      "original_purchase_date_ms": "1375340400000",
      "original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles",
      "original_application_version": "1.0",
      "in_app": [
          {
              "quantity": "1",//购买商品的数量
              "product_id": "*******",
              "transaction_id": "1000000404314890", //这个苹果的交易唯一标识符,不会随着恢复购买和重现购买而改变
              "original_transaction_id": "1000000404314890", //原始交易ID
              "purchase_date": "2018-06-04 09:58:41 Etc/GMT", // 购买商品的时间,不会因为恢复购买或者重现购买而刷新时间
              "purchase_date_ms": "1528106321000", //购买时间毫秒
              "purchase_date_pst": "2018-06-04 02:58:41 America/Los_Angeles", //太平洋标准时间
              "original_purchase_date": "2018-06-04 09:58:41 Etc/GMT", //原始购买时间
              "original_purchase_date_ms": "1528106321000", //毫秒
              "original_purchase_date_pst": "2018-06-04 02:58:41 America/Los_Angeles", //购买时间,太平洋标准时间
              "is_trial_period": "false"
          },
          {
              "quantity": "1",
              "product_id": "*******",
              "transaction_id": "1000000404523773",
              "original_transaction_id": "1000000404523773",
              "purchase_date": "2018-06-05 02:21:26 Etc/GMT",
              "purchase_date_ms": "1528165286000",
              "purchase_date_pst": "2018-06-04 19:21:26 America/Los_Angeles",
              "original_purchase_date": "2018-06-05 02:21:26 Etc/GMT",
              "original_purchase_date_ms": "1528165286000",
              "original_purchase_date_pst": "2018-06-04 19:21:26 America/Los_Angeles",
              "is_trial_period": "false"
          }
        ]
      }
  }
  • 收据返回的状态码-status
    文档传送门 状态码
21000    App Store 不能读取你提供的JSON对象
21002    receipt-data 域的数据有问题
21003    receipt 无法通过验证
21004    提供的 shared secret 不匹配你账号中的 shared secret
21005    receipt 服务器当前不可用
21006    receipt 合法, 但是订阅已过期. 服务器接收到这个状态码时, receipt 数据仍然会解码并一起发送
21007    receipt 是 Sandbox receipt, 但却发送至生产系统的验证服务
21008    receipt 是生产 receipt, 但却发送至 Sandbox 环境的验证服务

项目中遇到的问题

  • 发起内购前记得确认下内购权限
    if ([SKPaymentQueue canMakePayments]) {
        // 开始内购的操作
    }else {
        NSLog(@"没有内购权限");
    }
  • 交易完成后,需要在完成本次交易事务
    这里需要注意的是,苹果有一个补单的措施。当你发起一起交易事务之后,当你每一次重现开启APP的时候,都会自动进行之前的交易请求。从而出发内购的回调:- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
    所以我们要做的就是,当交易完成之后对改次交易进行finish操作。[[SKPaymentQueue defaultQueue] finishTransaction:transation]

  • 有时候我们可能在本地获取到内购凭证,那么就需要我们进行收到刷新

- (void)refreshPurchaseCertificate
{
    FCLogInfo(@"开始刷新本地支付凭证");
    SKReceiptRefreshRequest *request = [[SKReceiptRefreshRequest alloc] initWithReceiptProperties:nil];
    request.delegate = self;
    [request start];
}

- (void)requestDidFinish:(SKRequest *)request API_AVAILABLE(ios(3.0), macos(10.7))
{
    NCLog(@"刷新成功本地支付凭证");
    NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
    NSString *base64String = [receipt base64EncodedStringWithOptions:0];
}

- (void)request:(SKRequest *)request didFailWithError:(NSError *)error API_AVAILABLE(ios(3.0), macos(10.7))
{
    NCLog(@"刷新失败本地支付凭证");
}
  • 在购买商品的页面,一定要有恢复购买的代码,不然会被拒审
- (void)restoreCompletedTransactions
{   
    // 购买成功后,也会进行内购的回调
    [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
  • 如何避免刷单
    当服务器在进行凭证的二次校验时,服务器可以在数据库中查询transaction_id是否未曾入库,或者该id的拥有者账号为此次发起请求的账号,那么我们就认为这是一次成功的内购。

  • app至少过审一次
    我们app第一次上架的时候就有内购的功能,但是在TestFlight中死活买不了上面。然后经过咨询有内购经验的同事,最后得出的结论是:app一定要过一次苹果审核,商品才能够过审核,才能进行内购。我们直接提审后,发现真的就可以了。所以大家在做内购的时候要有这个准备。

最后向大家推荐一个内购的库,大家可以参考学习,有需要的也可以直接使用该库。RMSotre

END

-------------------------------------------------------想要保护的人多了,所以想要变得更强