IAP Programming Guide

参考链接:
SwiftyStoreKit
In-App Purchase Best Practice

  1. 获取可用的产品:

从本地Bundle或者服务器获取IAP的产品列表。 然后向App Store验证产品的可用性。 注意一下5点:

  • Display a store only if the user can make payments.
  • Present products naturally in the flow of your app.
  • Organize products so that exploration is easy and enjoyable.
  • Communicate the value of your products to your users.
  • Display prices clearly, using the locale and currency returned by the App Store.
    展示正确的价格:
NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
[numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
[numberFormatter setLocale:product.priceLocale];
NSString *formattedPrice = [numberFormatter stringFromNumber:product.price];
  1. 请求支付
    用户选择了内购产品之后,作出请求
SKProduct *product = <# Product returned by a products request #>;
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
payment.quantity = 2;

向支付队列添加本次支付:
[[SKPaymentQueue defaultQueue] addPayment:payment];

帮助苹果发现欺诈行为:
在提交支付时顺便提交本地服务器的用户名,用于反欺诈行为。
方法:
populate the applicationUsername property of the payment object with a one-way hash of the user’s account name on your server, such as in the example shown in Listing 3-2.

#import <CommonCrypto/CommonCrypto.h>

// Custom method to calculate the SHA-256 hash using Common Crypto
- (NSString *)hashedValueForAccountName:(NSString*)userAccountName
{
    const int HASH_SIZE = 32;
    unsigned char hashedChars[HASH_SIZE];
    const char *accountName = [userAccountName UTF8String];
    size_t accountNameLen = strlen(accountName);

    // Confirm that the length of the user name is small enough
    // to be recast when calling the hash function.
    if (accountNameLen > UINT32_MAX) {
        NSLog(@"Account name too long to hash: %@", userAccountName);
        return nil;
    }
    CC_SHA256(accountName, (CC_LONG)accountNameLen, hashedChars);

    // Convert the array of bytes into a string showing its hex representation.
    NSMutableString *userAccountHash = [[NSMutableString alloc] init];
    for (int i = 0; i < HASH_SIZE; i++) {
        // Add a dash every four bytes, for readability.
        if (i != 0 && i%4 == 0) {
            [userAccountHash appendString:@"-"];
        }
        [userAccountHash appendFormat:@"%02x", hashedChars[i]];
    }

    return userAccountHash;
}
  1. 推销内购产品
    • 在App Store Connect 添加想推荐的内购产品,
    • 实现SKPaymentTransactionObserver中的方法来处理购买

App Store调起的购买-完成购买
用户在App Store中点击内购商品后, 会通过SKPaymentTransactionObserver中的方法来发送信息。你的App负责完成购买相关的操作和任何有关动作,
* 继续一次购买操作
判断是否推迟购买,若推迟则保存付款信息并return false
判断是否取消购买,若取消则return false 并提供提示信息给用户
将保存的付款添加到付款队列 SKPaymentQueue

func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment,
        forProduct product: SKProduct) -> Bool {

    // ... Add code here to check if your app must defer the transaction.
    let shouldDeferPayment = ...
    // If you must defer until onboarding is completed, then save the payment and return false.
    if shouldDeferPayment {
        self.savedPayment = payment
    return false
    }

    // ... Add code here to check if your app must cancel the transaction.
    let shouldCancelPayment = ...
    // If you must cancel the transaction, then return false:
    if shouldCancelPayment {
    return false
    }
}

// (If you canceled the transaction, provide feedback to the user.)

// Continuing a previously deferred payment
SKPaymentQueue.default().add(savedPayment)

)

显示/隐藏内购产品

// Updating Visibility Override of a Promoted In-App Purchase
// Fetch Product Info for "Pro Subscription"

let storePromotionController = SKProductStorePromotionController.default()
storePromotionController.update(storePromotionVisibility: .hide, forProduct: proSubscription,
    completionHandler: { (error: Error?) in
        // Complete
    })

重排序内购推荐顺序

// Updating Order Override of Promoted In-App Purchases

// Fetch Product Info for three products: Pro Subscription, Fishing Hot Spots, and Hidden Beaches

let storePromotionController = SKProductStorePromotionController.default()
let newProductsOrder = [hiddenBeaches, proSubscription, fishingHotSpots]
storePromotionController.updateStorePromotionOrder(newProductsOrder,
    completionHandler: { (error: Error?) in
        // Complete
    })

取消排序
将空列表通过updateStorePromotionOrder:completionHandler 发送,IAP推荐列表将会以默认顺序显示

读取推荐排序

通过fetchStorePromotionOrder(completionHandler:) 方法 将会收到一个重排序的商品列表 如果收到空列表,那么你还没有自定义商品推荐顺序

// Reading Order Override of Promoted In-App Purchases

let storePromotionController = SKProductStorePromotionController.default()
storePromotionController.fetchStorePromotionOrder(completionHander: {
    (products: [SKProduct], error: Error?) in
        // products == [hiddenBeaches, proSubscription, fishingHotSpots
    })

The resulting URL looks like this:

itms-services://?action=purchaseIntent&bundleId=com.example.app&productIdentifier=product_name

Send this URL to yourself in an email or iMessage and open it from your device. You will know the test is running when your app opens automatically. You can then test your promoted in-app purchase.

  1. 投放产品(最后一步)
    等待App Store处理购买
    SKPaymentQueue是StoreKit中的核心环节
    当In-App支付成功后,会调起 你的App的 SKPaymentQueue Observer 方法。
    在小型App中,可以在App Delegate中实现这些方法。包括监听 transaction queue , 在大多数App中 可以创建一个独立的类,来处理观察者逻辑,和你的App的其他应用商店相关的逻辑。 观察者必须实现 SKPaymentTransactionObserver 协议。
- (BOOL)application:(UIApplication *)application
 didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    /* ... */

    [[SKPaymentQueue defaultQueue] addTransactionObserver:observer];
}

paymentQueue:updatedTransactions: 方法 用来观察购买状态的变化
在此方法中
* 循环所有的Transactions
* 判定Transaction的状态(Purchasing, purchased, failed, restored, deferred)
* 执行各个状态相应的操作。
如果是 SKPaymentTransactionStatePurchased 则支付成功需要向用户开放相关功能或提供相应的商品,如果是SKPaymentTransactionStateFailed,则需要向用户提示相应的错误,当支付完成后 该支付需要被移出payment queue, 调起payment queue的finishTransaction(_:)方法 将 Transaction 作为参数传入。

- (void)paymentQueue:(SKPaymentQueue *)queue
 updatedTransactions:(NSArray *)transactions
{
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            // Call the appropriate custom method for the transaction state.
            case SKPaymentTransactionStatePurchasing:
                

[self showTransactionAsInProgress:transaction deferred:NO]

; break; case SKPaymentTransactionStateDeferred:

[self showTransactionAsInProgress:transaction deferred:YES]

; break; case SKPaymentTransactionStateFailed:

[self failedTransaction:transaction]

; break; case SKPaymentTransactionStatePurchased:

[self completeTransaction:transaction]

; break; case SKPaymentTransactionStateRestored:

[self restoreTransaction:transaction]

; break; default: // For debugging NSLog(@”Unexpected transaction state %@”, @(transaction.transactionState)); break; } } }

购买持久化
通过iCloud , UserDefaults 或Server , AppReceipt

Consumable 商品会保存在Receipt中直至用户完成购买。该信息将会在下次购买后删除。
其他类型商品会一直存在Receipt中。

解锁App功能
用Bool值来管理App功能的开启
这个Bool值可以从Receipt中获取,也可是NSUserDefaults中的键值

提供购买相关的下载内容
比如关卡的文件, 新的乐器音频
内容可以存在本地,也可以在需要时从App Store获取

iOS6+ App需要使用Apple提供的内容提供机制,在Xcode中创建IAP内容Bundle然后提交到App Store Connect。

提示: 这种方法不能改变App内部的代码

下载苹果主机上的内容
如果一个内购商品有关联的下载内容,则传入的transaction中亦包含一个SKDownload实例,用于下载相关内容

调起SkPaymentQueue的 startDownloads方法 传入transaction的downloads属性,如果该属性为空则没有待下载的文件,否则将开始自动下载。(避免在没有获得用户同意的情况下使用蜂窝移动网络下载)

paymentQueue:updatedDownloads: 方法用来相应下载进度的变化
::Gracefully Fail: 当磁盘空间不足时可以让用户放弃部分下载 或在磁盘空间可用时继续::
使用progress 和timeRemaining来更新UI 可以使用 pauseDownloads:, resumeDownloads:, cancelDownloads: 方法来控制下载中的transaction。
使用downloadState来确定下载已完成。

提示:完成内容的下载后再 调用transaction complete方法。 否则将无法再使用下载中的内容。

iOS中可以自行管理下载的内容,下载后StoreKit将其存储在 Caches文件夹中,如果可以在磁盘空间不足时被删除,就不用处理。否则,将其转移到Documents文件夹中 并且设置用户备份排除Flag为YES

NSError *error;
BOOL success = [URL setResourceValue:[NSNumber numberWithBool:YES]
                              forKey:NSURLIsExcludedFromBackupKey
                               error:&error];
if (!success) { /* Handle error... */ }

结束Transaction
SKPaymentQueue存储所有未完成的Transaction, 你将负责将所有Queue中的Transaction完成,无论它是成功还是失败。

完成一个Transaction之前需执行以下步骤:
* 持久化这个购买
* 下载相关的内容
* 更新UI来让用户访问购买的内容

SKPaymentTransaction *transaction = <# The current payment #>;
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];

建议的测试步骤:

测试支付请求 : 提交一个内购支付请求,观察paymentQueue:updatedTransactions:是否调起
检查观察者方法:检查方法是否正确调起
测试成功的购买:用测试者账户登录并发起购买,
测试被打断的购买:
测试购买完成:检查所有工作都完成在finishTransaction:方法之前