iOS应用内付费(In-App Purchase,IAP,内购)实现要点总结

在iOS实现内购,需要接入StoreKit framework以完成玩家支付。为了保证支付的安全性,在玩家支付后,游戏并不是立即下发虚拟物品,而是拿着StoreKit从App Store收到的receipt(票据,可以理解为买东西付钱后开的发票),通过本地或者服务器向App Store发起验证,确认交易的合法性后,再发放游戏商品或者解锁游戏特性。

接入StoreKit之前的准备工作

在App Store Connect配置内购物品的Product ID

共有4种内购类型可供选择,其中常用的有两种:

  1. Consumables ,可消耗的,也就是玩家在游戏里可以装备或者花掉的,比如金币,装备,皮肤等。它是可以被玩家重复购买的,且只针对当前购买的设备有效。换句话说我在A设备上买的金币是不能再B设备上花的。
  2. Non-consumables,不可消耗的,也就是玩家只要买一次就能永久有效的,比如开启关卡,移除广告等。这种购买是在玩家所有设备上都有效的。总不能我再A设备上移除广告,换了B设备,就又要再次移除广告。
    注意:如果是按时间收费的内购,应该设置为Non-Renewing Subscription(不可更新的订阅),否则审核会被拒

在App Store Connect申请测试账号

  1. 在测试的iOS设备上退出普通账号,然后启动游戏,在游戏提交支付时StoreKit会提示你登录,这时用测试账号登录并支付,交易完成,但不会真的扣款。这里要注意:测试账号不能直接在设置里登录,否则会失效。
  2. 沙盒测试时验证receipt用的url和生产环境用的url是不一样的。它们分别是
  • 沙盒环境,“https://sandbox.itunes.apple.com/verifyReceipt”
  • 生产环境,“https://buy.itunes.apple.com/verifyReceipt”
    所以在实际验证receipt的时候,我们一般先用生产url验证票据,如果收到21007状态码,则表明这是个沙盒票据,那么接着就切换到沙盒url验证,从而避免手动在这两种环境之间切换。

在正确配置好Product ID和测试账号后,通常需要等待一段时间,最长24小时,否则可能遇到沙盒测试购买失败的情况。这是因为App Store准备沙盒测试环境需要一段时间。

接入StoreKit

获取Product信息

有了Product ID,我们就可以建立一个Product请求(SKProductsRequest,以一组Product ID初始化,指定一个实现协议 SKProductsRequestDelegate的代理以处理请求结果SKProductsResponse),向App Store获取合法的Product的本地化信息(保存在SKProduct中),填充到制作好的Store界面上,同时App Store还会返回请求里不合法的Product ID列表。不可识别的原因通常是ID拼写错误、标记为不可销售、Connect的信息还未同步到其他App Store服务器等。下面是上述的简单实现样例:

// 你定义的异步验证Product ID和获取Product信息的方法
- (void)validateProductIdentifiers:(NSArray *)productIdentifiers
{
    SKProductsRequest *productsRequest = [[SKProductsRequest alloc]
        initWithProductIdentifiers:[NSSet setWithArray:productIdentifiers]];

    // 保存request引用,避免被游戏回收
    self.request = productsRequest;
    productsRequest.delegate = self; // 当前所在的类实现了SKProductsRequestDelegate协议
    [productsRequest start];
}

// SKProductsRequestDelegate 协议接口的实现
- (void)productsRequest:(SKProductsRequest *)request
didReceiveResponse:(SKProductsResponse *)response
{
	// 包含product的本地化信息,保存product列表,后面请求交易时需要用到
    self.products = response.products; 

    for (NSString *invalidIdentifier in response.invalidProductIdentifiers) {
        // 处理不合法的Product ID,可以简单打印或者弹个Alert
    }

    [self displayStoreUI]; // 你定义的显示商店界面的方法
}

向App Store请求支付

  1. 首先要查询玩家是否允许支付,因为iOS可以在设置里禁止IAP支付。如下:
if ([SKPaymentQueue canMakePayments]) {
    // 请求支付
} else {
    NSLog(@"失败,用户禁止应用内付费购买.");
}
  1. 创建一个支付请求SKPayment,并设置想要购买的数量quantity。然后追加请求到支付队列SKPaymentQueue中, 通过StoreKit将支付请求发送到App Store中。如果这个请求被添加多次,那么它也会提交到App Store多次,每次都会要求玩家付费并让游戏下发内购物品。代码如下:
SKProduct *product = <# 之前获得的产品信息 #>;
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
payment.quantity = 3;
[[SKPaymentQueue defaultQueue] addPayment:payment];

处理交易对象Transaction

每次向支付队列添加支付请求,App Store都会生成对应的交易对象Transaction并添加到交易队列里。这是个持久化的对象,即使玩家退出或者重启游戏,下一次都能继续交易。App Store通过StoreKit来同步交易对象,游戏本身要实现自己的交易队列观察者对象(SKPaymentTransactionObserver)来监听交易对象状态的变化并在处理完交易后把交易对象从交易队列中移除。

  1. 为了立即响应交易对象的变化,游戏在刚启动的时候就要注册观察者对象
- (BOOL)application:(UIApplication *)application
 didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    /* ... */

    [[SKPaymentQueue defaultQueue] addTransactionObserver:observer];
}

- (void)applicationWillTerminate:(UIApplication *)application
{
	/* ... */

	[[SKPaymentQueue defaultQueue] removeTransactionObserver:observer];
}

  1. 在自己的观察者对象类里实现接口paymentQueue:updatedTransactions:。每当交易状态变化时,StoreKit通过这个方法通知游戏执行对应的动作。示例如下:
- (void)paymentQueue:(SKPaymentQueue *)queue
 updatedTransactions:(NSArray *)transactions
{
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchasing:
            	// 你定义的响应正在购买中的方法,等待再次调用
                [self showTransactionAsInProgress:transaction deferred:NO];
                break;
            case SKPaymentTransactionStateDeferred:
            	// 你定义的响应玩家取消购买的方法,等待再次调用
                [self showTransactionAsInProgress:transaction deferred:YES];
                break;
            case SKPaymentTransactionStateFailed:
            	// 你定义的响应购买失败的方法,获取transaction的error属性,显示购买失败原因
                [self failedTransaction:transaction];
                break;
            case SKPaymentTransactionStatePurchased:
            	// 你定义的相关购买成功的方法,通常是下发内购物品或者解锁特性,
            	// 或者为了进一步保证交易安全性,获取transaction的receipt属性,向服务器验证票据,通过后在下发物品
                [self completeTransaction:transaction];
                break;
            case SKPaymentTransactionStateRestored:
            	// 你定义的还原之前购买的功能的方法
                [self restoreTransaction:transaction];
                break;
            default:
                // For debugging
                NSLog(@"Unexpected transaction state %@", @(transaction.transactionState));
                break;
        }
    }
}
  1. 在处理完成功或者失败的交易对象后,调用finishTransaction:,把交易对象从交易队列移除。移除前确保你已经下发商品或者验证票据。在极少数的情况下,这个方法调用会失败,从而再次执行购买,所以你可以记录交易,从而避免重复购买

验证receipt

这步主要是为了防破解。要注意两点:

  1. 发送的票据要持久化,如果游戏崩溃、网络异常、强制关闭后仍然可以恢复重试。
  2. 服务器则要记录发送过来的票据是否已经在验证中,或者验证过,避免多发物品和作弊。
    代码如下:
/* 从app bundle中加载receipt. */
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];

if (!receipt) { 
    NSLog(@"no receipt");
    /* 没有本地 receipt -- 处理异常. */ 
} else {
    /* 获取编码后的 receipt */
    NSString *encodedReceipt = [receipt base64EncodedStringWithOptions:0];
}

/* ... 发送 receipt 数据给你的服务器,
	然后服务器建立一个JSON对象,把receipt数据放到receipt-data属性里,
	建立一个HTTP POST请求,将JSON对象放入Payload,发送给App Store验证
... */

你可能感兴趣的:(iOS,ios,objective-c,swift)