iOS内购 StoreKit文档

成熟的小朋友要学会自己看文档
置于为什么示例代码是swift,因为官方文档没有给OC版本的,我也懒得改。
更新-后续的内容有OC的代码示例了。

StoreKit

支持应用程序内购买以及与App Store的交互。


总览

在应用程序中使用StoreKit,可以提供以下功能和服务:

  • In-App Purchase。 提供和推广应用内购买的内容和服务。
  • Apple Music。 检查用户的Apple Music功能并提供订阅。
  • Recommendations and reviews。 提供有关第三方内容的建议,并使用户能够对您的应用进行评分和审查。

这里主要看内购。

In-App Purchase

通过在您的应用内购买商品,为用户提供其他内容和服务。


应用内购买可让您为用户提供购买应用内内容和功能的机会。 客户可以在您的应用程序中进行购买,也可以直接从App Store中进行购买。 有关在App Store中推广产品的信息,请参阅Promoting Your In-App Purchases

StoreKit框架代表您的应用程序连接到App Store,以提示并安全地处理付款。 然后,框架通知您的应用程序,该应用程序将交付购买的产品。 要验证购买,您可以通过App Store或设备在服务器上验证收据。 对于自动续订订阅,App Store还可将关键订阅事件通知您的服务器。

image.png

在App Store Connect中配置应用内购买

要使用应用内购买,必须首先在App Store Connect中配置产品。 在开发应用程序时,您可以添加或删除产品以及优化或重新配置现有产品。 有关更多信息,请参阅Workflow for configuring in-app purchases

您还可以提供在多个平台上运行的应用程序和应用程序内购买作为一次购买。 有关通用购买的更多信息,请参阅App Store Connect Help

了解产品类型

您可提供四种类型的 App 内购买项目:

  • 消耗型项目是一种使用一次之后即失效的项目。用户可以多次购买这类项目。
  • 非消耗型项目是一种用户只需购买一次的项目。这类项目不会过期。
  • 服务或内容的自动续期订阅是一种用户购买一次之后,只要用户不选择取消,就会一直自动续期的项目。
  • 服务或内容的非续期订阅有特定访问时限,不会自动续期。用户可以再次购买这类项目。

您可以使用 StoreKit 跨设备同步和恢复非消耗型项目和自动续期订阅。当用户购买自动续期订阅或非续期订阅时,您的 app 应当让用户能够在所有设备上访问这一订阅,并让用户能够恢复以前购买的项目。

官方API实例

为付款队列设置交易观察对象

通过添加观察者,使您的应用能够接收和处理交易。


总览

要在您的应用中处理交易,您必须创建一个观察者并将其添加到付款队列中。 观察者对象响应新交易并将待处理交易队列与App Store同步,并且支付队列提示用户授权支付。 您的应用应在应用启动时添加交易观察者,以确保您的应用将尽快收到付款队列通知。

创建观察者

创建并构建自定义观察者类以处理对支付队列的更改:

class StoreObserver: NSObject, SKPaymentTransactionObserver {
                ....
    //Initialize the store observer.
    override init() {
        super.init()
        //Other initialization here.
    }

    //Observe transaction updates.
    func paymentQueue(_ queue: SKPaymentQueue,updatedTransactions transactions: [SKPaymentTransaction]) {
        //Handle transaction states here.
    }
                ....
}

创建此观察者类的实例以充当付款队列更改的观察者:

let iapObserver = StoreObserver()

提示
考虑将您的观察者创建为该类的共享实例,以便在任何其他类中进行全局引用。 共享实例还可以保证对象的生存期,从而确保通过SKPaymentTransactionObserver协议进行的回调始终由同一实例处理。

创建交易观察者后,您可以将其添加到付款队列中。
总结一下就是:自定义的实现了SKPaymentTransactionObserver协议的观察对象

添加观察者

当您的应用调用时,StoreKit会将您的观察者附加到队列中:

SKPaymentQueue.default().add(iapObserver)

当付款队列的内容在恢复或运行应用程序时发生更改时,StoreKit可以自动通知您的SKPaymentTransactionObserver实例。
实现交易观察器:

import UIKit
import StoreKit

class AppDelegate: UIResponder, UIApplicationDelegate {
                ....
    // Attach an observer to the payment queue.
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        SKPaymentQueue.default().add(iapObserver)
        return true
    }

    // Called when the application is about to terminate.
    func applicationWillTerminate(_ application: UIApplication) {
        // Remove the observer.
        SKPaymentQueue.default().remove(iapObserver)
    }
                ....
}

重要的是在启动时在application:didFinishLaunchingWithOptions:中添加观察者,以确保观察者在您的应用程序所有启动期间持续存在,接收所有付款队列通知并继续进行可能在应用程序外部处理的交易,例如:

  • 推荐的应用内购买
  • 后台订阅续订
  • 采购中断

观察者必须是持久的,因此当应用程序发送到后台时,它不会被释放。 只有持久的观察者才能接收到您的应用程序在后台运行时可能发生的交易,例如自动续订的续订交易。

总结一下:在App启动的时候,把自定义的观察对象放进SKPaymentQueue队列中,然后后续一系列的任务等待监听回调交给观察对象的协议方法处理。

提供和完成 App 内购买以及恢复 App 内购买项目

在您的应用中获取,完成和还原交易。


总览

应用程序内购买允许用户在您的应用程序内或使用StoreKit框架直接从App Store购买虚拟商品。此示例代码演示了如何检索,显示和还原应用内购买。首先,您设置您的应用程序以在启动时注册并使用单事务队列观察器。交易队列观察者管理所有支付交易并处理所有交易状态。确认这是符合SKPaymentTransactionObserver协议的自定义类的共享实例。然后,在系统即将终止应用程序时删除事务观察器。有关更多信息,请参见Setting Up the Transaction Observer for the Payment Queue

此示例构建了InAppPurchases应用程序,支持iOS,macOS和tvOS平台。启动后,InAppPurchases会在App Store中查询Products.plist文件中保存的产品标识符。 InAppPurchases会使用App Store的响应来更新其UI,其中可能包括待售产品,无法识别的产品标识符或两者。该应用程序还在其UI中显示所有可用的已购买和已恢复的付款交易。

配置示例代码项目

在运行和测试此示例之前,您需要:

  1. 从一个完整的应用程序开始,该应用程序支持应用程序内购买并在App Store Connect中配置了一些应用程序内购买。有关更多信息,请参见第1步和第2步。
  2. 在App Store Connect中创建沙盒测试用户帐户。
  3. Xcode中打开此示例,选择要构建的目标,然后将其捆绑ID更改为支持应用内购买的捆绑ID。接下来,选择合适的团队,让Xcode自动管理您的配置文件。
  4. 打开示例中的ProductIds.plist文件,并使用您现有的应用内购买商品ID更新其内容。
  5. 对于iOStvOS设备,分别构建和运行InAppPurchasesInAppPurchasestvOS目标,该示例用于构建InAppPurchases。
  6. 对于macOS,在构建InAppPurchasesmacOS目标之前,请退出Mac App Store。构建目标,然后首次从Finder启动生成的应用以获取收据。有关详细信息,请参见Testing In-App Purchases with Sandbox
  7. InAppPurchases应用程序在启动时向App Store查询ProductIds.plist中包含的产品标识符。成功后,它将显示可在App Store中出售的产品列表。点按列表中的任何产品以进行购买。当系统提示您进行购买验证时,请使用在步骤2中创建的测试用户帐户。当产品请求失败时,出于各种原因,请参阅 invalidProductIdentifiers的讨论,原因是App Store可能会返回无效的产品标识符。

显示可用本地化定价出售的产品

该示例配置了应用程序,以便在展示要出售的产品之前确认用户已被授权在设备上付款。

var isAuthorizedForPayments: Bool {
    return SKPaymentQueue.canMakePayments()
}

应用确认授权后,就会向App Store发送产品请求,以从App Store获取本地化的产品信息。 查询App Store可确保该App仅向用户显示可供购买的产品。 该应用程序使用与其希望在其UI中出售的产品相关联的产品标识符列表来初始化产品请求。 确保对产品请求对象使用强引用持有,系统可能会在请求完成之前将其释放。

fileprivate func fetchProducts(matchingIdentifiers identifiers: [String]) {
    // Create a set for the product identifiers.
    let productIdentifiers = Set(identifiers)
    
    // Initialize the product request with the above identifiers.
    productRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
    productRequest.delegate = self
    
    // Send the request to the App Store.
    productRequest.start()
}

App Store使用 SKProductsResponse对象响应产品请求。 其产品属性包含有关可在App Store中实际购买的所有产品的信息。 该应用程序使用此属性来更新其UI。 响应的invalidProductIdentifiers属性包含App Store无法识别的所有产品标识符。 出于各种原因,请参阅invalidProductIdentifiers的讨论,为什么App Store可能会返回无效的产品标识符。

// products contains products whose identifiers have been recognized by the App Store. As such, they can be purchased.
if !response.products.isEmpty {
    availableProducts = response.products
}

// invalidProductIdentifiers contains all product identifiers not recognized by the App Store.
if !response.invalidProductIdentifiers.isEmpty {
    invalidProductIdentifiers = response.invalidProductIdentifiers
}

要在用户界面中显示产品的价格,请使用App Store返回的语言环境和货币。 例如,考虑一个登录法国应用程序商店并且其设备使用美国语言环境的用户。 尝试购买产品时,App Store会以欧元显示产品价格。 因此,转换和显示以美元表示的产品价格以使其与用户界面中的设备区域设置相匹配是不正确的。

extension SKProduct {
    /// - returns: The cost of the product formatted in the local currency.
    var regularPrice: String? {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        formatter.locale = self.priceLocale
        return formatter.string(from: self.price)
    }
}

与应用互动以购买产品

在用户界面中点击任何可出售的产品进行购买。 该应用程序允许用户还原非消耗性产品和自动续订的订阅。 使用还原按钮和设置>还原所有可还原的购买分别在iOStvOS版本的应用程序中实现此功能。 选择商店>恢复菜单项以在应用的macOS版本中恢复购买。 点击任何购买的商品都会显示购买信息,例如产品标识符,交易标识符和交易日期。 当购买的商品是托管产品时,购买信息还包括其内容标识符,内容版本和内容长度。 当购买恢复后,购买信息还将包含其原始交易的标识符和日期。

处理付款交易状态

当付款队列中有待处理的交易时,StoreKit会通过调用应用的 paymentQueue:updatedTransactions:方法来通知应用的交易观察者。 每个交易都有五个可能的状态,包括SKPaymentTransactionStatePurchasingSKPaymentTransactionStatePurchasedSKPaymentTransactionStateFailedSKPaymentTransactionStateRestoredSKPaymentTransactionStateDeferred; 有关更多信息,请参见SKPaymentTransactionState 应用程序应确保其观察者的 paymentQueue:updatedTransactions:可以随时响应任何一种状态。 提供Apple托管的产品时,他们应该在其观察者上实现 paymentQueue:updatedDownloads:`方法。

func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    for transaction in transactions {
        switch transaction.transactionState {
        case .purchasing: break
        // Do not block the UI. Allow the user to continue using the app.
        case .deferred: print(Messages.deferred)
        // The purchase was successful.
        case .purchased: handlePurchased(transaction)
        // The transaction failed.
        case .failed: handleFailed(transaction)
        // There're restored products.
        case .restored: handleRestored(transaction)
        @unknown default: fatalError(Messages.unknownPaymentTransaction)
        }
    }
}

当事务失败时,检查其错误属性以确定发生了什么。 仅显示代码与paymentCancelled不同的错误。

// Do not send any notifications when the user cancels the purchase.
if (transaction.error as? SKError)?.code != .paymentCancelled {
    DispatchQueue.main.async {
        self.delegate?.storeObserverDidReceiveMessage(message)
    }
}

当用户推迟交易时,应用应允许他们在等待StoreKit更新交易时继续使用UI。

恢复已完成的购买

当用户购买非消耗品,自动更新订阅或非更新订阅时,他们希望它们可以在所有设备上无限期可用。 有关更多信息,请参见In-App Purchase。 该应用程序提供了一个UI,允许用户恢复其过去的购买记录。

@IBAction func restore(_ sender: UIBarButtonItem) {
    // Calls StoreObserver to restore all restorable purchases.
    StoreObserver.shared.restore()
}

使用SKPaymentQueuerestoreCompletedTransactions还原非消耗性和自动续订的订阅。 StoreKit通过调用每个已恢复的交易的SKPaymentTransactionStateRestored交易状态的paymentQueue:updatedTransactions:通知应用程序的交易观察者。 如果还原失败,请参阅restoreCompletedTransactions的讨论以获取有关如何解决该问题的详细信息。 恢复非续订的订阅不在此示例的范围内。

提供内容并完成交易

在收到状态为SKPaymentTransactionStatePurchasedSKPaymentTransactionStateRestored的交易后,应用必须交付内容或解锁购买的功能。这些状态表明App Store已从用户处收到产品付款。当购买的产品包含来自App Store的托管内容时,请致电SKPaymentQueue的startDownloads:下载内容。

未完成的交易留在付款队列中。在每次从后台启动或从后台恢复运行之前,StoreKit都会调用该应用程序的永久观察者的paymentQueue:updatedTransactions:直到应用程序完成这些交易。结果,App Store可能会反复提示用户对他们的购买进行身份验证,或阻止他们从该应用程序购买产品。

对状态为SKPaymentTransactionStateFailedSKPaymentTransactionStatePurchasedSKPaymentTransactionStateRestored的事务调用finishTransaction:以将其从队列中删除。完成的交易无法恢复。因此,应用程序必须提供购买的内容,下载产品的所有Apple托管内容或完成购买过程,然后才能完成交易。

// Finish the successful transaction.
SKPaymentQueue.default().finishTransaction(transaction)

SKPaymentQueue 付款队列

由App Store处理的付款交易队列。

@interface SKPaymentQueue : NSObject

总览

付款队列与App Store通信并显示用户界面,以便用户可以授权付款。队列的内容在应用程序启动之间保持不变。

要处理付款,请首先将至少一个观察者对象(SKPaymentTransactionObserver)添加到队列中(请参阅addTransactionObserver:`)。然后,为用户要购买的商品添加支付对象(SKPayment)。每次添加付款对象时,队列都会创建一个交易对象(SKPaymentTransaction)以处理该付款并将其排队。付款完成后,队列更新交易对象,然后调用任何观察者对象向他们提供更新的交易。您的观察者应处理该事务,然后将其从队列中删除。

您用于处理已处理交易的确切机制取决于应用程序的设计和所购买的产品。以下是一些常见示例:

  1. 如果产品是您的应用程序中已内置的功能,则您的应用程序将启用该功能来处理交易。
  2. 如果产品包含App Store提供的可下载内容,则您的应用将从交易中检索SKDownload对象,并要求付款队列下载它们。您在创建产品信息时将实际的内容文件提供给App Store并提供给App Store Connect
  3. 如果产品代表您自己的服务器提供的可下载内容,则您的应用程序可能会打开与服务器的网络连接并从那里下载内容。

有关设计应用程序的付款处理部分的更多信息,请参阅应用程序内购买编程指南。


Product Information 产品信息

加载应用内商品的唯一标识符,以便从App Store检索商品信息。

应用内购买流程可以分为三个阶段。 在购买过程的第一阶段,您的应用程序从App Store检索有关其产品的信息,向用户显示其商店UI,并让用户选择产品。 当用户在您的应用商店中选择产品时,您的应用会请求付款,最后,您的应用会交付该产品。 图1中突出显示了应用程序和App Store在第一阶段执行的步骤。

第一阶段执行的步骤

要开始购买过程,您的应用必须知道其产品标识符,以便它可以从App Store检索有关产品的信息,并将其商店UI呈现给用户。 应用中出售的每个产品都有唯一的产品标识符。 当您创建新的应用内购买产品时,您可以在App Store Connect中提供此值(有关更多信息,请参阅 Create an In-App Purchase)。 您的应用使用这些产品标识符来获取有关在App Store中可出售的产品的信息(例如定价),并在用户购买这些产品时提交付款请求。

有几种策略可用于在应用程序中存储产品标识符列表,例如嵌入到应用程序捆绑包中或存储在服务器上。 然后,您可以通过在应用程序包中本地读取产品标识符或从服务器中获取它们来检索产品标识符。 选择最能满足您应用程序需求的方法。

注意
没有运行时机制可以获取在App Store Connect中为特定应用配置的所有产品的列表。 您负责管理应用程序的产品列表,并将该信息提供给您的应用程序。 如果您需要管理大量产品,请考虑使用App Store Connect中的批量XML上传/下载功能。

从应用程序包中检索产品ID

在以下情况下,将产品标识符嵌入到您的应用程序包中:

  1. 您的应用有固定的应用内购买产品列表。 例如,通过应用内购买来删除广告或解锁功能的应用可以将产品标识符列表嵌入应用包中。
  2. 您希望用户更新应用程序以查看新的应用程序内购买产品。
  3. 该应用程序或产品不需要服务器。

在您的应用程序捆绑包中包含一个属性列表文件,其中包含一系列产品标识符,例如:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
 "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
    <string>com.example.level1</string>
    <string>com.example.level2</string>
    <string>com.example.rocket_car</string>
</array>
</plist>

要从属性列表中获取产品标识符,请在应用程序捆绑包中找到文件并阅读。

NSURL *url = [[NSBundle mainBundle] URLForResource:@"product_ids"
                                     withExtension:@"plist"];
NSArray *productIdentifiers = [NSArray arrayWithContentsOfURL:url];

从服务器中检索产品ID

在以下情况下,存储来自服务器的产品标识符:

  1. 您可以经常更新应用程序内产品列表,而无需更新应用程序。 例如,支持其他级别或角色的游戏应从您的服务器获取产品标识符列表。
  2. 产品包括交付的内容。
  3. 您的应用或产品需要服务器。

使用产品标识符在您的服务器上托管JSON文件。 例如,以下JSON文件包含三个产品ID:

[
    "com.example.level1",
    "com.example.level2",
    "com.example.rocket_car"
]

要从服务器获取产品标识符,请获取并读取JSON文件。

- (void)fetchProductIdentifiersFromURL:(NSURL *)url delegate:(id)delegate
{
    dispatch_queue_t global_queue =
        dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(global_queue, ^{
        NSError *err;
        NSData *jsonData = [NSData dataWithContentsOfURL:url
                                                 options:NULL
                                                   error:&err];
        if (!jsonData) { /* Handle the error. */ }

        NSArray *productIdentifiers = [NSJSONSerialization
            JSONObjectWithData:jsonData options:NULL error:&err];
        if (!productIdentifiers) { /* Handle the error. */ }

        dispatch_queue_t main_queue = dispatch_get_main_queue();
        dispatch_async(main_queue, ^{
            [delegate displayProducts:productIdentifiers]; // Custom method.
        });
    });
}

考虑对JSON文件进行版本控制,以便将来的应用程序版本可以更改其结构,而不会破坏应用程序的旧版本。 例如,您可以命名使用旧结构products_v1.json的文件和使用新结构products_v2.json的文件。 如果您的JSON文件比示例中的简单数组复杂,则此功能特别有用。

为了确保您的应用程序保持响应状态,请使用后台线程下载JSON文件并提取产品标识符列表。 为了使传输的数据最少,请使用标准的HTTP缓存机制,例如Last-Modified和If-Modified-Since`标头。

加载所有应用内商品标识后,将其传递到商品信息请求中,并发送到App Store。 有

从App Store获取产品信息

在您的应用中检索有关待售产品的最新信息,以显示给用户。

总览

为了确保您的用户只能看到实际可购买的产品,请在显示应用商店的用户界面之前查询App Store。 您将需要所有应用程序产品标识符的列表; 有关获取此列表的更多信息,请参阅加载应用内商品标识。

请求产品信息

要查询App Store,请创建一个products requestSKProductsRequest),并使用您的产品标识符列表对其进行初始化。 确保对请求对象有严格的引用; 否则,系统可能会在请求完成之前取消分配该请求。

products request检索有关有效产品的信息以及无效产品标识符的列表,然后调用其委托以处理结果。 委托必须实现SKProductsRequestDelegate协议以处理来自App Store的响应。 这是这两段代码的简单实现:

// Custom method.
- (void)validateProductIdentifiers:(NSArray *)productIdentifiers
{
    SKProductsRequest *productsRequest = [[SKProductsRequest alloc]
        initWithProductIdentifiers:[NSSet setWithArray:productIdentifiers]];

    // Keep a strong reference to the request.
    self.request = productsRequest;
    productsRequest.delegate = self;
    [productsRequest start];
}

// SKProductsRequestDelegate protocol method.
- (void)productsRequest:(SKProductsRequest *)request
didReceiveResponse:(SKProductsResponse *)response
{
    self.products = response.products;

    for (NSString *invalidIdentifier in response.invalidProductIdentifiers) {
        // Handle any invalid product identifiers.
    }

    [self displayStoreUI]; // Custom method.
}

保留对返回给委托的产品对象数组(SKProduct)的引用,因为当用户购买产品时,您将需要相应的产品对象来创建付款请求。 如果在您的应用程序上出售的产品列表可能会发生变化,例如当您添加或从销售中删除产品时,请考虑创建一个自定义类,该类封装对产品对象的引用以及其他信息,例如图片或描述文字 从服务器上获取的信息。 有关付款请求的更多信息,请参阅Requesting a Payment from the App Store

解决无效的产品ID

App Store对您的产品请求的回复中,无效的产品ID通常表示您的应用的产品ID列表中存在错误。 无效的产品ID也可能意味着您在App Store Connect中配置了不正确的产品。 可行的用户界面和深入的日志记录可以帮助您更轻松地解决此类问题:

  1. 在生产版本中,通过显示应用程序其余的商店用户界面并忽略无效的产品,以使其正常运行。
  2. 在开发版本中,显示错误以引起注意该问题。
  3. 在生产和开发版本中,都使用NSLog将消息写入控制台,以便您记录无效标识符。
  4. 如果您的应用程序是从服务器上获取列表的,则还可以定义一种日志记录机制,以使您的应用程序将无效标识符的列表发送回服务器。

购买- 从App Store请求付款

用户选择要购买的产品时,向App Store提交付款请求。

总览

在显示应用程序的商店用户界面后,用户可以在应用程序内进行购买。 当用户选择产品时,您的应用会创建一个付款请求并将其提交到App Store。

实施应用内购买流程可以分为三个阶段。 在第一阶段,您的应用将检索产品信息。 然后,当用户在您的应用商店中选择产品时,您的应用就会请求付款。 最后,您的应用可以交付产品。 本节详细介绍了应用程序和App Store在第二阶段执行的步骤,如图1所示。


图1

创建付款请求

当用户选择要购买的产品时,请使用相应的SKProduct对象创建付款请求,并根据需要设置数量,如下所示。 产品对象来自您应用的产品请求所返回的一系列产品,如从Fetching Product Information from the App Store

SKProduct *product = <# Product returned by a products request. #>;
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
payment.quantity = 2;

提交付款请求

通过将您的付款请求添加到付款队列,将其提交到App Store。 如果您将付款对象多次添加到队列中,则会多次提交到App Store,向用户收费并要求您的应用每次交付产品。

[[SKPaymentQueue defaultQueue] addPayment:payment];

对于您的应用程序提交的每个付款请求,它都会收到相应的交易要处理。 有关交易和付款队列的更多信息,请参阅Processing a Transaction

对于自动续订订阅,您可以针对确定为有资格接收报价的用户提交带有订阅报价的付款请求。 有关更多信息,请参阅Implementing Promotional Offers in Your App


交易处理

注册事交易列观察器以从App Store获取和处理交易更新。

总览

实施应用内购买流程可以分为三个阶段。 您首先要检索产品信息,然后通过将其添加到付款队列中来发送付款请求。 最后,您的应用可以交付成功交易的产品。 本节详细介绍了您的应用程序和App Store在最后阶段执行的步骤,如图1所示。

在处理付款请求后,App Store会调用交易队列观察器。 然后,您的应用会记录有关购买的信息以供将来启动,下载购买的内容并将交易标记为完成。

图1通过监视交易队列以交付产品来完成购买过程


监视队列中的交易

交易队列在让您的应用通过StoreKit框架与App Store进行通信中起着核心作用。您将工作添加到需要应用商店执行的队列中,例如要处理的付款请求。当交易状态发生变化时(例如,付款请求成功时),StoreKit会调用应用的交易队列观察器。您决定哪个类充当观察员。在非常小的应用程序中,您可以处理应用程序委托中的所有StoreKit逻辑,包括观察交易队列。但是,在大多数应用中,您创建一个单独的类来处理此观察者逻辑以及应用的其余存储逻辑。观察者必须符合SKPaymentTransactionObserver协议。

通过添加观察者,您的应用无需持续轮询其活动交易的状态。您的应用将交易队列用于支付请求,下载Apple托管的内容以及确定订阅已续订。

始终在启动应用程序后立即注册事务队列观察器,如下所示。有关更多指导,请参阅Setting Up the Transaction Observer for the Payment Queue.

- (BOOL)application:(UIApplication *)application
 didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    /* ... */

    [[SKPaymentQueue defaultQueue] addTransactionObserver:observer];
}

确保观察者随时准备处理交易,不仅是在将交易添加到队列中之后。 例如,如果用户在进入隧道之前立即在您的应用程序中购买了商品,则在没有网络连接的情况下,您的应用程序可能无法传递购买的内容。 下次您的应用程序启动时,StoreKit再次调用您的交易队列观察器,并且您的应用程序应处理交易并交付购买的内容。 同样,如果您的应用程序未能将交易标记为已完成,则StoreKit会在您的应用程序每次启动时调用观察者,直到事务完成为止。

在您的交易队列观察器上实现paymentQueue:updatedTransactions:方法。 当交易状态改变时,例如在处理付款请求时,StoreKit调用此方法。 交易状态会告诉您您的应用需要执行什么操作,如表1所示。

状态 对应的处理方式
SKPaymentTransactionStatePurchasing 更新您的用户界面以反映进行中的状态,然后等待再次调用。
SKPaymentTransactionStateDeferred 更新您的用户界面以反映延迟状态,然后等待再次调用。
SKPaymentTransactionStateFailed 使用error属性的值向用户显示消息。 有关错误常量的列表,请参见SKErrorDomain
SKPaymentTransactionStatePurchased 通常通过解锁功能或提供内容来提供购买的功能。
SKPaymentTransactionStateRestored 恢复以前购买的功能。

队列中的事务可以按任何顺序更改状态。 您的应用需要随时准备就绪,可以处理任何活动交易。 根据每个交易的交易状态对其进行操作,如本例所示:

- (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;
        }
    }
}

更新应用程序的用户界面以反映交易的变化

为了使您的用户界面在等待时保持最新状态,事务队列观察器可以从SKPaymentTransactionObserver协议中实现可选方法,如下所示:

  • StoreKit从队列中删除事务时,StoreKit会调用paymentQueue:removedTransactions:方法。 在实施此方法时,请从应用的用户界面中删除相应的项。
  • StoreKit完成还原交易时,将根据其是否存在错误来调用paymentQueueRestoreCompletedTransactionsFinished:paymentQueue:restoreCompletedTransactionsFailedWithError:方法。 在实施这些方法时,请更新应用的用户界面以反映成功或失败。

对于成功处理的交易,您的应用应验证与交易关联的收据,以验证用户购买的商品并相应地解锁内容。 有关在服务器端验证收据的更多信息,请参阅Validating Receipts with the App Store


选择收据验证技术

选择适用于您的应用的收据验证类型。

总览

App Store收据提供了应用程序销售或在应用程序内进行的任何购买的记录,您可以通过向应用程序或服务器添加收据验证码来验证购买的内容。 收据验证需要了解安全编码技术,才能采用对您的应用程序安全且独特的解决方案。

选择验证技术

有两种验证收据真实性的方法:

  1. 建议在设备上进行本地设备上的收据验证,以验证带有应用内购买的应用的收据签名。
  2. 建议通过App Store进行服务器端收据验证,以持久保存应用内购买,以维护和管理购买记录。

比较方法并确定最适合您的应用程序和基础架构的方法。 您也可以选择实施两种方法。

消耗性的应用内购买将保留在收据中,直到您调用finishTransaction:为止。 如果需要,维护和管理服务器上消耗品的记录。 非消耗品,自动续订的订阅项目和非续订的订阅项目会无限期保留在收据中。 对于自动续订订阅管理,服务器端收据验证比设备上的收据验证具有关键优势。

-- 本地设备上验证 服务器端验证
验证收据的真实性 Yes Yes
包括续订交易 Yes Yes
包括其他用户订阅信息 No Yes
处理续签而无需客户依赖性 No Yes
Resistant to device clock change No Yes

注意
为了使设备上的收据验证包括续订交易,必须有互联网连接才能刷新收据。

有关为包含自动可更新订阅产品的应用程序实施收据验证的更多信息,请参见 WWDC 2018 > Engineering Subscriptions

验证收据

本地验证需要代码读取和验证PKCS#7签名,还需要代码解析和验证签名的有效负载。 通过App Store进行验证需要在您的应用程序与服务器之间建立安全连接,并在服务器上进行编码以通过App Store验证收据。 有关服务器端验证的更多信息,请参阅Validating Receipts with the App Store

尽管收据通常会在完成购买或恢复购买后立即更新,但是当应用程序未运行时,更改可能会在其他时间发生。 如有必要,请调用SKReceiptRefreshRequest以确保您正在使用的收据是最新的,例如在后台更新订阅时。 此刷新需要网络连接。


通过App Store验证收据

在安全服务器上通过App Store验证交易。

总览

App Store收据是使用Apple证书签名的二进制加密文件。 为了读取加密文件的内容,您需要将其通过verifyReceipt端点。 端点的响应包括可读的JSON正文。 与App Store的通信按照RFC 4627的定义构造为JSON字典。二进制数据按RFC 4648的定义进行Base64编码。通过安全服务器验证与App Store的收据。 有关与App Store建立安全网络连接的信息,请参阅Preventing Insecure Network Connections.

警告
不要从您的应用程序调用App Store服务器的verifyReceipt端点。 您无法直接在用户的设备与App Store之间建立信任的连接,因为您无法控制该连接的任何一端,这会使它容易受到中间人攻击。

获取收据数据

要从设备上的应用中检索收据数据,请使用NSBundleappStoreReceiptURL方法找到该应用的收据,然后在Base64中对数据进行编码。 将此Base64编码的数据发送到您的服务器。

/* Load the receipt from the app bundle. */
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];

if (!receipt) { 
    NSLog(@"no receipt");
    /* No local receipt -- handle the error. */ 
} else {
    /* Get the receipt in encoded format */
    NSString *encodedReceipt = [receipt base64EncodedStringWithOptions:0];
}

/* ... Send the receipt data to your server ... */

将收据数据发送到App Store

在您的服务器上,创建一个带有收据数据,密码(如果收据包含自动续订)的JSON对象,并在requestBody中详细说明exclude-old-transactions键。

提交此JSON对象作为HTTP POST请求的有效负载。 在沙盒中测试您的应用程序以及在审查应用程序时,请使用测试环境URL https://sandbox.itunes.apple.com/verifyReceipt。 当您的应用程序在App Store中上线时,请使用生产URL https://buy.itunes.apple.com/verifyReceipt。 有关这些端点的更多信息,请参见verifyReceipt

重要信息
首先使用生产网址验证收据; 然后使用沙盒网址进行验证,如果您收到21007状态代码。 这种方法可确保您在测试应用程序,通过App Review进行审查或在App Store中运行应用程序时,不必在URL之间切换。

解析响应

App Store的响应有效负载是一个JSON对象,其中包含responseBody中详细介绍的键和值。

in_app数组包含用户先前购买的不可消耗,不可更新的订阅以及可自动更新的订阅项。 检查响应中这些应用内购买类型的值,以根据需要验证交易。

对于自动续订的订阅项目,解析响应以获取有关当前有效订阅期的信息。 当您验证订阅的收据时,latest_receipt包含最新的编码收据,与请求中收据数据的值相同,并且latest_receipt_info包含订阅的所有交易,包括首次购买和后续续订,但不包含 包括所有还原。

您可以使用这些值来检查自动更新订阅是否已过期。 将这些值与expiration_intent订阅字段一起使用以获取到期原因。


解锁购买内容

验证购买后,将内容交付给用户。

总览

购买完成后,您有责任确保以编程方式将内容提供给用户。

识别购买的内容

对于启用了应用程序功能(例如高级订阅)的应用程序内购买产品,请设置布尔值以启用代码路径并根据需要更新用户界面。 查看应用程序中发生的交易的永久记录,以确定要解锁的功能。 每当用户完成购买并在应用启动时,您的应用都必须更新此布尔值。 有关进行永久记录的信息,请参阅Persisting a Purchase

例如,使用应用程序收据,您的代码可能如下所示:

NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];

// Custom method to work with receipts
BOOL rocketCarEnabled = [self receipt:receiptData
        includesProductID:@"com.example.rocketCar"];

或者,使用user defaults system

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
BOOL rocketCarEnabled = [defaults boolForKey:@"enable_rocket_car"];

定义布尔变量后,使用购买信息在应用程序中启用适当的代码路径:

if (rocketCarEnabled) {
    // Use the rocket car.
} else {
    // Use the regular car.
}

传送相关内容

您的应用必须将与购买的产品相关的任何内容交付给用户。例如,在音乐应用程序中购买乐器需要传送让用户演奏这些乐器所需的声音文件。您可以将该内容嵌入到应用程序的捆绑软件中,或根据需要下载。每种方法都有其优点和缺点。

在您的应用程序中嵌入较小的文件(最大几兆字节),尤其是在您希望大多数用户购买该产品的情况下。您可以在用户购买应用捆绑包后立即使其可用。但是,要添加或更新应用程序捆绑包中的内容,必须提交应用程序的更新版本。

仅在需要时下载较大的文件。将内容与应用捆绑包分开可以使应用的初始下载量小。例如,游戏可以在其应用包中包含第一级,并允许用户在购买时下载其他级。假设您的应用从服务器中获取了产品标识符列表,并且该信息未在应用捆绑中进行硬编码,则无需重新提交应用即可添加或更新应用下载的内容。

注意
您无法修补应用二进制文件或下载可执行代码。 您的应用在提交时必须包含支持其所有功能所需的所有可执行代码。 如果新产品需要更改代码,请提交应用程序的更新版本。

加载本地内容

从应用程序包加载其他资源时,请使用NSBundle类加载本地内容。

NSURL *url = [[NSBundle mainBundle] URLForResource:@"rocketCar"
                                     withExtension:@"plist"];
[self loadVehicleAtURL:url];

从Apple的服务器下载托管内容

大多数应用程序应使用Apple托管的内容来下载文件。您使用Xcode中的“应用内购买内容”目标创建了Apple托管的内容包,并将其提交到App Store ConnectApple的服务器使用支持其他大规模操作(例如App Store)的相同基础结构来存储您的应用程序内容。即使您的应用未运行,Apple托管的内容也会在后台自动下载。

如果您需要支持旧版本的iOS或在多个平台上共享服务器基础结构,则可以选择使用自己的服务器基础结构来托管自己的内容。

当用户购买具有关联的Apple托管内容的产品时,传递给您的事务队列观察器的事务还包括一个 SKDownload实例,可让您下载关联的内容。

要下载内容,请通过调用SKPaymentQueuestartDownloads:方法,将交易对象的downloads属性中的下载对象添加到交易队列中。如果downloads属性的值为nil,则说明该交易没有Apple托管的内容。与下载应用程序不同,下载内容不会自动要求Wi-Fi连接以获取大于特定大小的内容。避免在没有用户明确行动的情况下使用蜂窝网络下载大文件。

注意
另外,您可以使用按需资源(ODR)获得更大的灵活性来下载应用程序中的数据。 ODRApple托管的服务,一旦您使用应用收据确认用户的购买后,便可以使用该服务存储应用内购买数据,以供用户下载内容。 与SKDownload相比,此替代方法的优势在于ODR不需要您调用即可恢复事务并验证用户以下载Apple服务器上托管的内容。

在交易队列观察器上实现 paymentQueue:updatedDownloads:方法,以响应下载状态的更改,例如通过更新UI中的进度。如果下载失败,请使用其error属性中的信息将错误呈现给用户。

确保您的应用正常处理错误。例如,如果设备在下载期间磁盘空间不足,请给用户选择放弃部分下载或在空间可用时稍后继续下载的选项。

在下载内容时,使用 progresstimeRemaining属性的值更新用户界面。您可以从用户界面中使用SKPaymentQueue的 pauseDownloads:, resumeDownloads:,和 cancelDownloads:方法来让用户控制正在进行的下载。使用downloadState属性确定下载是否已完成。不要使用下载对象的progresstimeRemaining属性来检查其状态;这些属性用于更新您的UI。

重点
完成交易之前,请下载所有Apple托管的内容。 事务完成后,其下载对象将无法使用。

在iOS中,您的应用可以管理下载的文件。 StoreKit框架会在未设置备份标记的情况下将这些文件保存在Caches目录中。 下载完成后,您的应用程序负责将这些文件移动到适当的位置。 对于如果设备磁盘空间不足(然后由您的应用再次下载)可以删除的内容,请将文件保留在Caches目录中。 否则,将文件移动到Documents文件夹并设置标志以将其从用户备份中排除。

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

macOS中,系统管理下载的文件; 您的应用无法直接移动或删除它们。 要在下载后找到内容,请使用下载对象的contentUR属性。 要在随后的启动中找到文件,请使用SKDownloadcontentURLForProductID:类方法。 要删除文件,请使用deleteContentForProductID:类方法。 有关阅读应用收据的信息,请参阅 Validating Receipts with the App Store

从自己的服务器上下载内容

与您的应用程序和服务器之间的所有其他交互一样,从您自己的服务器下载内容的过程的细节和机制也是您的责任。 通信至少包括以下步骤:

  1. 您的应用会将收据发送到您的服务器并请求内容。
  2. 您的服务器将验证收据以确认已购买了内容,如Validating Receipts with the App Store中所述。
  3. 假设收据有效,您的服务器将使用内容响应您的应用。

确保您的应用正常处理错误。 例如,如果设备在下载期间磁盘空间不足,请给用户选择放弃部分下载或在空间可用时稍后继续下载的选项。

考虑如何托管内容以及应用程序与服务器通信的安全性。 有关更多信息,请参见Security Overview


保存购买记录

保持购买记录,以继续根据需要提供该产品。

总览

在提供产品之后,您的应用应记录购买的永久记录。您的应用使用该永久记录来继续使产品在发布时可用并恢复购买。您的应用的持久性策略取决于您销售的产品类型:

  • 对于非消耗性产品和自动续订的订阅,请使用应用收据作为您的永久记录。如果应用收据不可用,请使用用户默认系统或iCloud保留永久记录。
  • 对于非续订订阅,请使用iCloud或您自己的服务器来保留永久记录。
  • 对于消耗品,您的应用会更新其内部状态以反映购买情况。确保更新后的状态是支持状态保留的对象的一部分(在iOS中),或者在应用启动时手动保留状态(在iOSmacOS中)。

使用用户默认系统或iCloud时,您的应用程序可以存储一个值(例如数字或布尔值)或交易收据的副本。在macOS中,用户可以使用defaults命令编辑用户默认系统。存储收据需要更多的应用程序逻辑,但可以防止永久记录被篡改。

注意
通过iCloud持久保存时,您的应用程序的持久记录会在设备之间同步,但是您的应用程序负责在其他设备上下载任何关联的内容。

使用应用收据保存购买

应用收据包含由Apple加密签名的用户购买记录。 有关更多信息,请参见Choosing a Receipt Validation Technique

付款后,有关消费品的信息就会添加到收据中,并保留在收据中,直到您完成交易为止。 完成交易后,下次更新收据时(例如用户下次购物),将删除此信息。

购买产品时,有关所有其他购买信息的信息会添加到收据中,并无限期保留在收据中。

在UserDefault或者iCloud中保存一个值

要将信息存储在UserDefault值或iCloud中,请设置密钥的值。

#if USE_ICLOUD_STORAGE
NSUbiquitousKeyValueStore *storage = [NSUbiquitousKeyValueStore defaultStore];
#else
NSUserDefaults *storage = [NSUserDefaults standardUserDefaults];
#endif

[storage setBool:YES forKey:@"enable_rocket_car"];
[storage setObject:@15 forKey:@"highest_unlocked_level"];

使用自己的服务器保存购买记录

将收据的副本以及凭据或标识符发送到您的服务器,以便您可以跟踪哪些收据属于特定用户。 例如,让用户使用用户名和密码向您的服务器标识自己。 不要使用UIDeviceidentifierForVendor属性。 不同的设备对此属性具有不同的值,因此您不能使用它来识别和恢复同一用户在不同设备上进行的购买。


完成交易

完成transaction以完成购买过程。

总览

完成交易将告诉StoreKit购买所需的一切均已完成。 未完成的交易会一直保留在队列中,直到完成为止。因此,您应在每次启动应用程序时添加事务队列观察器,以使应用程序能够完成交易。 您的应用程序必须完成每笔交易,无论成功还是失败。

完成交易之前,请执行以下所有操作:

  1. 保存购买记录。
  2. 下载相关内容。
  3. 更新应用的用户界面,以便用户可以访问产品。

要完成交易,请在支付队列上调用finishTransaction:方法。

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

完成交易后,请勿对其执行任何操作或进行任何交付产品的工作。 如果还有任何工作要做,则说明您的应用尚未准备好完成交易。

重要信息
在事务实际完成之前,请勿尝试调用finishTransaction:方法,并尝试使用应用程序中的其他机制来跟踪未完成的事务。 StoreKit并非设计用于这种方式,这样做会阻止您的应用下载Apple托管的内容,并可能导致其他问题。


处理退款通知

响应在客户退款期间为消耗性,非消耗性和非更新性订阅产品创建的通知。

总览

当客户收到应用内购买的退款时,App Store Server会发送近乎实时的通知。 如果您跨多个平台提供内容(例如游戏中的宝石或硬币),并且在服务器上更新玩家帐户余额,则接收退款通知非常重要。 通过解释和处理退款信息来响应退款通知,并在应用程序中告知客户您因退款而采取的任何措施。

接收一次性购买的客户退款通知

客户以多种方式要求退款,例如:

  • 联系Apple客户支持并要求退款
  • 登录并使用Apple的自助服务工具reportaproblem.apple.com要求退款
  • 要求他们的付款方式发行人退款

App Store处理退款时,App Store服务器会通过您配置的URL向您的服务器发送REFUND通知。 您的服务器必须使用200响应码来响应该帖子。 要启用通知,请参阅Enabling App Store Server Notifications

REFUND通知仅适用于消耗性,非消耗性和非续订性订阅。 要检测自动续订订阅的退款,请参阅检测退款。

解释和处理退款通知

您的服务器负责解析和解释来自App Store Server的所有通知。 对于“退款”通知,请从响应中标识特定交易,产品ID和相关日期:

  • 通过检查purchase_date以选择最新交易,从而在unified_receipt.latest_receipt_info中找到product_id的最新交易。
  • App Store发出退款的日期在此交易的cancel_date_ms字段中。

有关响应的更多信息,请参阅App Store Server Notifications

当您收到退款通知时,您有责任为每笔退款交易存储,监控并采取适当的措施。 例如,您可以构建自己的游戏内货币平衡逻辑,该逻辑通过将通知链接到玩家帐户或会话来处理退款交易。

重要信息
当您使用包含退款交易的收据来调用verifyReceipt端点时,JSON响应中不存在退款交易,自动续订订阅除外。

通过在应用程序中显示上下文消息通知客户有关退款所采取的任何措施。

识别退款滥用

通过将“退款”通知映射到服务器上的玩家帐户,减少退款滥用并识别重复的退款购买。 监视和分析您的数据以识别可疑的退款活动。

如果您跨多个平台提供内容,请在服务器上更新用户帐户的余额。 使用App Store Server通知来获取影响客户的交易的近实时状态更新。


恢复购买的产品

为用户提供可以在您的应用中恢复购买的功能,以维持对购买内容的访问权限。

总览

用户有时需要还原购买的内容,例如当他们升级到新手机时。 在您的应用程序中包含一些机制(例如“还原购买”按钮),以使他们还原购买的内容。

重要信息
不要自动恢复购买,特别是在启动应用程序时。 恢复购买会提示用户输入App Store凭据,这会中断您的应用流程。

在大多数情况下,您只需要刷新应用收据并交付收据上列出的产品即可。刷新后的收据包含用户从该应用商店帐户登录的任何设备上在该应用中购买商品的记录。但是,在给定情况下,应用可能需要其他方法:

  • 您使用Apple托管的内容-恢复已完成的交易,为您的应用程序提供下载内容所使用的交易对象。
  • 您需要在没有收据的设备上支持您的应用程序-还原已完成的交易。
  • 您的应用使用非续订订阅-您的应用负责恢复过程。
    刷新收据不会创建新的交易;它会从App Store请求收据的最新副本。仅刷新一次收据;连续刷新多次具有相同的结果。

恢复已完成的事务会为以前完成的每个事务创建一个新事务,实质上是为您的事务队列观察者重放历史记录。您的应用程序保持其自身状态,以跟踪其为何还原已完成的交易以及如何处理它们。多次还原会为每个完成的事务创建多个还原的事务。

注意
如果用户尝试购买已经购买的产品,则App Store会创建常规交易而不是还原交易,但是不会再次向用户收费。 与原始交易一样,解锁这些交易的内容。

为用户提供对再次下载的内容的适当级别的控制。 例如,不要同时自动下载三年的日报或数百兆的游戏级别。

刷新应用收据

创建收据刷新请求,设置委托,然后启动请求。 该请求支持可选属性,用于在测试期间获取各种状态的收据,例如过期的收据。 有关详细信息,请参见SKReceiptRefreshRequestinitWithReceiptProperties:方法。

request = [[SKReceiptRefreshRequest alloc] init];
request.delegate = self;
[request start];

刷新应用收据后,请对其进行检查并交付添加到收据中的所有产品。

恢复已完成的交易

您的应用程序通过调用SKPaymentQueuerestoreCompletedTransactions方法开始恢复已完成的交易。 此调用将请求发送到App Store,以恢复您所有应用程序已完成的交易。 如果您的应用为其付款请求的applicationUsername属性设置了一个值,则在还原交易时,可以使用restoreCompletedTransactionsWithApplicationUsername:方法提供相同的信息。

App Store会生成一个新交易,以还原每个先前完成的交易。 恢复的事务引用原始事务:SKPaymentTransaction的实例具有originalTransaction属性,而收据中的条目具有original_transaction_id字段值。

对于恢复的购买,日期字段的含义略有不同。 有关详细信息,请参见responseBody.Receipt.In_app中的purchase_dateoriginal_purchase_date字段。

StoreKit为每个已还原的事务调用状态为SKPaymentTransactionStateRestored的事务队列观察器,如处理事务中所述。 此时要执行的操作取决于应用程序的设计。

如果您的应用使用应用收据并且没有Apple托管的内容,则不需要此代码,因为您的应用不会还原已完成的交易。 立即完成所有还原的事务。

如果您的应用使用应用收据并且具有Apple托管的内容,请让用户选择要还原的产品,然后再开始还原过程。 在还原过程中,请先下载用户选择的内容,然后再完成这些事务,并立即完成任何其他事务。

NSMutableArray *productIDsToRestore = <# From the user #>;
SKPaymentTransaction *transaction = <# Current transaction #>;

if ([productIDsToRestore containsObject:transaction.transactionIdentifier]) {
// Re-download the Apple-hosted content
}

[[SKPaymentQueue defaultQueue] finishTransaction:transaction];

如果您的应用不使用应用收据,则会在还原所有已完成的交易时对其进行检查。 它使用与原始购买逻辑相似的代码路径来使产品可用,然后完成交易。 具有多个产品(尤其是具有相关内容的产品)的应用程序使用户可以选择要还原的产品,而不是还原所有内容。 这些应用程序通过立即完成而不还原它们来跟踪要还原的已完成事务以及要忽略的事务。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 175,490评论 5 419
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 74,060评论 2 335
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 124,407评论 0 291
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 47,741评论 0 248
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 56,543评论 3 329
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 43,040评论 1 246
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 34,107评论 3 358
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 32,646评论 0 229
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 36,694评论 1 271
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 32,398评论 2 279
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 33,987评论 1 288
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 30,097评论 3 285
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 35,298评论 3 282
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 27,278评论 0 14
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 28,413评论 1 232
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 38,397评论 2 309
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 38,099评论 2 314

推荐阅读更多精彩内容