还原过去的交易
如果应用程序支持可还原的产品类型,则必须添加一些用户界面元素,以允许用户还原这些购买。 借助此功能,客户可以产品添加到其他设备,或者在擦除设备或移除并重新安装应用后将产品还原到同一设备。 以下产品类型是可还原的:
- 非易耗型产品
- 自动续订订阅
- 免费订阅
还原过程应更新设备上保存的记录以提供产品。 客户可以选择随时在其任何设备上还原。 还原过程会重新发送该用户之前的所有交易;然后,应用程序代码必须决定对该信息采取何种操作(例如,检查设备上是否已经有该购买的记录,如果没有,则创建购买记录并为用户启用产品)。
实现还原
用户界面的“还原”按钮会调用以下方法,触发 SKPaymentQueue 上的 RestoreCompletedTransactions。
public void Restore()
{
   // theObserver will be notified of when the restored transactions start arriving <- AppStore
   SKPaymentQueue.DefaultQueue.RestoreCompletedTransactions();
}
StoreKit 将以异步方式将还原请求发送到 Apple 的服务器。
由于 CustomPaymentObserver 注册为交易观察程序,因此当 Apple 服务器响应时,它将收到消息。 响应将包含此用户在(所有设备的)此应用程序中执行的所有交易。 代码循环访问每个交易,检测还原状态并调用 UpdatedTransactions 方法来处理交易,如下所示:
// called when the transaction status is updated
public override void UpdatedTransactions (SKPaymentQueue queue, SKPaymentTransaction[] transactions)
{
   foreach (SKPaymentTransaction transaction in transactions)
   {
       switch (transaction.TransactionState)
       {
       case SKPaymentTransactionState.Purchased:
          theManager.CompleteTransaction(transaction);
           break;
       case SKPaymentTransactionState.Failed:
          theManager.FailedTransaction(transaction);
           break;
       case SKPaymentTransactionState.Restored:
           theManager.RestoreTransaction(transaction);
           break;
default:
           break;
       }
   }
}
如果用户没有可还原的产品,则不会调用 UpdatedTransactions。
在示例中还原给定交易的最简单代码执行的操作与购买时的操作相同,只不过 OriginalTransaction 属性用于访问产品 ID:
public void RestoreTransaction (SKPaymentTransaction transaction)
{
   // Restored Transactions always have an 'original transaction' attached
   var productId = transaction.OriginalTransaction.Payment.ProductIdentifier;
   // Register the purchase, so it is remembered for next time
   PhotoFilterManager.Purchase(productId); // it's as though it was purchased again
   FinishTransaction(transaction, true);
}
更复杂的实现可能会检查其他 transaction.OriginalTransaction 属性,例如原始日期和收据编号。 此信息对于某些产品类型(如订阅)非常有用。
还原完成
还原过程完成后(可以是成功或失败),CustomPaymentObserver 将有其他两种方法可供 StoreKit 调用,如下所示:
public override void PaymentQueueRestoreCompletedTransactionsFinished (SKPaymentQueue queue)
{
   Console.WriteLine(" ** RESTORE Finished ");
}
public override void RestoreCompletedTransactionsFailedWithError (SKPaymentQueue queue, NSError error)
{
   Console.WriteLine(" ** RESTORE FailedWithError " + error.LocalizedDescription);
}
在示例中,这些方法不执行任何操作,但实际应用程序可能会选择向用户发送消息或实现其他功能。
保护购买
本文档中的两个示例使用 NSUserDefaults 跟踪购买:
易耗品 – 信用购买的“余额”是随每次购买一起递增的简单 NSUserDefaults 整数值。
非易耗品 - 每个照片滤镜购买都存储为 NSUserDefaults 中的键值对。
使用 NSUserDefaults 虽然使示例代码保持简单,但未提供非常安全的解决方案,因为懂技术的用户有可能更新设置(绕过支付机制)。
注意:实际应用程序应采用安全机制来存储不受用户篡改的购买内容。 这可能涉及加密和/或其他技术,包括远程服务器身份验证。
该机制还应设计为利用 iOS、iTunes 和 iCloud 的内置备份和恢复功能。 这将确保用户还原备份后,其以前的购买将立即可用。
有关更多特定于 iOS 的指南,请参阅 Apple 的安全编码指南。
收据验证和服务器交付的产品
到目前为止,本文档中的示例仅包括应用程序与 App Store 服务器直接通信以进行购买交易,从而解锁已编码到应用中的特性或功能。
Apple 允许由另一台服务器独立验证购买收据,从而为提供额外的一层购买安全性做好准备,这对于在将数字内容作为购买的一部分(如数字书籍或杂志)交付之前验证请求非常有用。
内置产品 - 与本文档中的示例一样,所购买的产品作为应用程序附带的功能存在。 应用内购买使用户能够访问该功能。 产品 ID 已硬编码。
服务器交付的产品 – 产品可下载内容组成,这些内容存储在远程服务器上,直到交易成功导致下载这些内容。 示例可能包括书籍或杂志问题。 产品 ID 通常源自外部服务器(其中还托管了产品内容)。 应用程序必须在交易完成时实现一种可靠的记录方式,如果内容下载失败,则可以重新尝试,而不会使用户感到困惑。
服务器交付的产品
在购买过程中,需要从远程服务器下载某些产品的内容,例如书籍和杂志(甚至游戏级别)。 这意味着在购买产品内容后,需要额外的服务器来存储和交付产品内容。
获取服务器交付产品的价格
由于产品是远程交付的,因此还可以随时间推移添加更多产品(无需更新应用代码),例如添加更多书籍或新一期的杂志。 以便应用程序可以发现这些新闻产品并将其显示给用户,其他服务器应存储并提供此信息。
- 产品信息必须存储在多个位置:在服务器上和在 iTunes Connect 中。 此外,每个产品都有与之关联的内容文件。 成功购买后,将交付这些文件。 
- 当用户希望购买产品时,应用程序必须确定哪些产品可用。 此信息虽然可以进行缓存,但应从存储产品主列表的远程服务器传送。 
- 服务器返回要分析的应用程序的产品 ID 的列表。 
- 然后,应用程序确定将这些产品 ID 中的哪些发送到 StoreKit 以检索价格和说明。 
- StoreKit 将产品 ID 列表发送到 Apple 的服务器。 
- iTunes 服务器使用有效的产品信息(说明和当前价格)进行响应。 
- 产品信息传递给应用程序的 - SKProductsRequestDelegate以向用户显示。
购买服务器交付的产品
由于远程服务器需要某种方法来验证内容请求是否有效(即已支付),因此需传递收据信息以进行身份验证。 远程服务器将这些数据转发到 iTunes 进行验证,如果成功,则在发给应用程序的响应中包含产品内容。
- 应用向队列添加 - SKPayment。 如果需要,系统会提示用户输入其 Apple ID,并要求确认付款。
- StoreKit 将请求发送到服务器进行处理。 
- 交易完成后,服务器将使用交易收据进行响应。 
- SKPaymentTransactionObserver子类接收并处理收据。 由于产品必须从服务器下载,因此应用程序会向远程服务器发起网络请求。
- 下载请求附带收据数据,以便远程服务器可以验证它是否有权访问内容。 应用程序的网络客户端等待对此请求的响应。 
- 当服务器收到内容请求时,它会分析收据数据,并将请求直接发送到 iTunes 服务器,以验证是否为有效交易的收据。 服务器应使用一些逻辑来确定是将请求发送到生产 URL 还是沙盒 URL。 如果收到状态为 21007(沙盒收据已发送到生产服务器),Apple 建议始终使用生产 URL 并切换到沙盒。 有关更多详细信息,请参阅 Apple 的收据验证编程指南。 
- 如果收据有效,iTunes 将检查收据并返回状态零。 
- 服务器等待 iTunes 的响应。 如果收到有效的响应,代码应找到关联的产品内容文件,以包含在对应用程序的响应中。 
- 应用程序接收和分析响应,将产品内容保存到设备的文件系统。 
- 应用程序启用该产品,然后调用 StoreKit 的 - FinishTransaction。 然后,应用程序可选择显示所购内容(例如,显示所购书籍或杂志的第一页)。
对于非常大的产品内容文件,另一种实现方法是在步骤 #9 中简单地存储交易收据,以便快速完成交易,并为用户提供一个用户界面,以便用户稍后下载实际的产品内容。 后续下载请求可以重新发送存储的收据以访问所需的产品内容文件。
编写服务器端收据验证码
可以通过简单的 HTTP POST 请求/响应(包含工作流关系图中的步骤 #5 到 #8)使用服务器端代码验证收据。
提取应用中的 SKPaymentTansaction.TransactionReceipt 属性。 这是需要发送到 iTunes 进行验证的数据(步骤 #5)。
Base64 编码交易收据数据(步骤 #5 或 #6)。
创建如下所示的简单 JSON 有效负载:
{
   "receipt-data" : "(base-64 encoded receipt here)"
}
通过 HTTP POST 将 JSON 发送到 https://buy.itunes.apple.com/verifyReceipt 进行生产或发送到 https://sandbox.itunes.apple.com/verifyReceipt 进行测试。
JSON 响应将包含以下键:
{
   "status" : 0,
   "receipt" : { (receipt repeated here) }
}
状态为零表示收据有效。 服务器可以继续提供购买的产品内容。 收据键包含一个 JSON 字典,其属性与应用收到的 SKPaymentTransaction 对象相同,因此服务器代码可以查询该字典,以检索购买的 product_id 和数量等信息。
有关其他信息,请参阅 Apple 的收据验证编程指南文档。

