StoreKit 概览和检索 Xamarin.iOS 中的产品信息

应用内购买的用户界面显示在下面的屏幕截图中。 在发生任何交易之前,应用程序必须检索产品的价格和说明以进行显示。 然后,当用户按下“购买”时,应用程序向 StoreKit 发出请求,它负责管理确认对话框和 Apple ID 登录。 假设交易成功,StoreKit 会通知应用程序代码,它必须存储交易结果,并向用户提供对其购买的访问权限。

StoreKit 通知应用程序代码,它必须存储交易结果,并向用户提供对其购买的访问权限

实现应用内购买需要 StoreKit 框架中的以下类:

SKProductsRequest – 向 StoreKit 请求批准销售产品 (App Store)。 可以配置多个产品 ID。

  • SKProductsRequestDelegate – 声明处理产品请求和响应的方法。
  • SKProductsResponse – 从 StoreKit (App Store) 发送回委托。 包含与请求一起发送的产品 ID 匹配的 SKProducts。
  • SKProduct – 从 StoreKit 检索的产品(你已在 iTunes Connect 中配置的)。 包含有关产品的信息,例如产品 ID、标题、说明和价格。
  • SKPayment – 使用产品 ID 创建,并添加到付款队列以执行购买。
  • SKPaymentQueue – 要发送到 Apple 的排队付款请求。 每个付款的处理都会触发通知。
  • SKPaymentTransaction – 表示已完成的交易(由 App Store 处理并通过 StoreKit 发送回应用程序的购买请求)。 交易可以为“已购买”、“已还原”或“失败”。
  • SKPaymentTransactionObserver – 响应 StoreKit 付款队列生成的事件的自定义子类。
  • StoreKit 操作是异步的 – 启动 SKProductRequest 或将 SKPayment 添加到队列后,控件会返回到代码。 当 StoreKit 从 Apple 的服务器接收数据时,它将对 SKProductsRequestDelegate 或 SKPaymentTransactionObserver 子类调用方法。

下图显示了各种 StoreKit 类之间的关系(必须在应用程序中实现抽象类):

必须在应用中实现各种 StoreKit 类抽象类之间的关系

本文档稍后将更详细地介绍这些类。

测试

大多数 StoreKit 操作都需要真实的设备来进行测试。 检索产品信息(即价格和说明)将在模拟器中进行,但购买和还原操作将返回错误(例如 FailedTransaction Code=5002 发生了未知错误)。

注意:StoreKit 无法在 iOS 模拟器中运行。 在 iOS 模拟器中运行应用程序时,如果应用程序尝试检索付款队列,StoreKit 会记录警告。 必须在实际的设备上测试商店。

重要提示:不要在“设置”应用程序中使用测试帐户登录。 可以使用“设置”应用程序退出登录任何现有的 Apple ID 帐户,然后必须等待在应用内购买序列中收到提示再使用测试 Apple ID 登录。

如果尝试使用测试帐户登录到真实商店,它将自动转换为真实的 Apple ID。 该帐户将不再可用于测试。

若要测试 StoreKit 代码,必须退出登录常规 iTunes 测试帐户,并使用链接到测试商店的特殊测试帐户(在 iTunes Connect 中创建)登录。 若要退出登录当前帐户,请访问“设置 > iTunes 和 App Store”,如下所示:

若要退出登录当前帐户,请访问“设置 iTunes”和“应用商店”

然后,在应用中受 StoreKit 请求时使用测试帐户登录:

若要在 iTunes Connect 中创建测试用户,请单击主页上的“用户和角色”。

若要在 iTunes Connect 中创建测试用户,请单击主页上的“用户和角色”

选择“沙盒测试人员”

选择“沙盒测试人员”

它会显示现有用户的列表。 可以添加新用户或删除现有记录。 门户(当前)不允许查看或编辑现有测试用户,因此建议留好创建的每个测试用户的记录(尤其是你分配的密码)。 删除测试用户后,不能将电子邮件地址重新用于另一个测试帐户。

显示现有用户的列表

新的测试用户具有与真实 Apple ID 类似的特性(例如名称、密码、机密问题和答案)。 保留此处输入的所有详细信息的记录。 “选择 iTunes Store”字段将确定在以该用户身份登录时,应用内购买将使用哪种货币和语言。

“选择 iTunes 应用商店”字段将确定用户的应用内购买的货币和语言

检索产品信息

销售应用内购买产品的第一步是显示它:从 App Store 检索当前价格和说明以进行显示。

无论应用销售的产品类型如何(消耗品、非消耗品或订阅类型),检索要显示的产品信息的过程都是相同的。 本文随附的 InAppPurchaseSample 代码包含一个名为“Consumables”的项目,它演示如何检索产品信息以供显示。 它演示如何:

  • 创建 SKProductsRequestDelegate 的实现并实现 ReceivedResponse 抽象方法。 示例代码调用此 InAppPurchaseManager 类。
  • 请与 StoreKit 联系,查看付款是否被允许(使用 SKPaymentQueue.CanMakePayments)。
  • 使用 iTunes Connect 中定义的产品 ID 实例化 SKProductsRequest。 这是通过示例的 InAppPurchaseManager.RequestProductData 方法完成的。
  • SKProductsRequest 调用 Start 方法。 这会触发对 App Store 服务器的异步调用。 委托 (InAppPurchaseManager) 将被回调以提供结果。
  • 委托的 (InAppPurchaseManager) ReceivedResponse 方法会用 App Store 返回的数据(产品价格和说明或有关无效产品的消息)更新 UI。

整体交互如下所示(StoreKit 内置于 iOS,App Store 表示 Apple 的服务器):

检索产品信息图

显示产品信息示例

易耗品示例代码演示了如何检索产品信息。 该示例的主屏幕显示从 App Store 检索到的两个产品的信息:

主屏幕显示从应用商店检索的产品信息

下面更详细地介绍了用于检索和显示产品信息的示例代码。

ViewController 方法

ConsumableViewController 类将管理两个产品的价格显示,它们的产品 ID 已在类中硬编码。

public static string Buy5ProductId = "com.xamarin.storekit.testing.consume5credits",
   Buy10ProductId = "com.xamarin.storekit.testing.consume10credits";
List<string> products;
InAppPurchaseManager iap;
public ConsumableViewController () : base()
{
   // two products for sale on this page
   products = new List<string>() {Buy5ProductId, Buy10ProductId};
   iap = new InAppPurchaseManager();
}

在类级别,还应声明一个用于设置 NSNotificationCenter 观察程序的 NSObject:

NSObject priceObserver;

在 ViewWillAppear 方法中,观察程序是使用默认通知中心创建和分配的:

priceObserver = NSNotificationCenter.DefaultCenter.AddObserver (
  InAppPurchaseManager.InAppPurchaseManagerProductsFetchedNotification,
(notification) => {
   // display code goes here, to handle the response from the App Store
}

ViewWillAppear 方法结束时,调用 RequestProductData 方法以启动 StoreKit 请求。 发出此请求后,StoreKit 将异步联系 Apple 的服务器以获取信息并将其馈送给应用。 这是由下一节中介绍的 SKProductsRequestDelegate 子类 (InAppPurchaseManager) 实现的。

iap.RequestProductData(products);

显示价格和说明的代码会从 SKProduct 中检索信息并将其分配给 UIKit 控件(请注意,我们显示了 LocalizedTitleLocalizedDescription – StoreKit 会根据用户帐户设置自动解析正确的文本和价格)。 以下代码属于上面创建的通知:

priceObserver = NSNotificationCenter.DefaultCenter.AddObserver (
  InAppPurchaseManager.InAppPurchaseManagerProductsFetchedNotification,
(notification) => {
   // display code goes here, to handle the response from the App Store
   var info = notification.UserInfo;
   if (info.ContainsKey(NSBuy5ProductId)) {
       var product = (SKProduct) info.ObjectForKey(NSBuy5ProductId);
       buy5Button.Enabled = true;
       buy5Title.Text = product.LocalizedTitle;
       buy5Description.Text = product.LocalizedDescription;
       buy5Button.SetTitle("Buy " + product.Price, UIControlState.Normal); // price display should be localized
   }
}

最后,ViewWillDisappear 方法应确保移除观察程序:

NSNotificationCenter.DefaultCenter.RemoveObserver (priceObserver);

SKProductRequestDelegate (InAppPurchaseManager) 方法

当应用程序希望检索产品价格和其他信息时,将调用 RequestProductData 方法。 它会将产品 ID 的集合分析为正确的数据类型,然后使用该信息创建一个 SKProductsRequest。 调用 Start 方法会导致向 Apple 的服务器发出网络请求。 请求将在成功完成后异步运行并调用委托的 ReceivedResponse 方法。

public void RequestProductData (List<string> productIds)
{
   var array = new NSString[productIds.Count];
   for (var i = 0; i < productIds.Count; i++) {
       array[i] = new NSString(productIds[i]);
   }
   NSSet productIdentifiers = NSSet.MakeNSObjectSet<NSString>(array);​​​
   productsRequest = new SKProductsRequest(productIdentifiers);
   productsRequest.Delegate = this; // for SKProductsRequestDelegate.ReceivedResponse
   productsRequest.Start();
}

iOS 将根据应用程序正在运行的预配配置文件自动将请求路由到 App Store 的“沙盒”或“生产”版本 – 因此当你在开发或测试应用时,该请求将有权访问 iTunes Connect 中配置的每个产品(甚至包括尚未提交或尚未由 Apple 批准的产品)。 当应用程序在生产环境中时,StoreKit 请求将仅返回“已批准”的产品的信息。

在 Apple 的服务器用数据响应后,会调用 ReceivedResponse 替代方法。 由于这是在后台调用的,因此代码应分析有效数据,并使用通知将产品信息发送到正在“侦听”该通知的任何 ViewControllers。 用于收集有效产品信息并发送通知的代码如下所示:

public override void ReceivedResponse (SKProductsRequest request, SKProductsResponse response)
{
   SKProduct[] products = response.Products;
   NSDictionary userInfo = null;
   if (products.Length > 0) {
       NSObject[] productIdsArray = new NSObject[response.Products.Length];
       NSObject[] productsArray = new NSObject[response.Products.Length];
       for (int i = 0; i < response.Products.Length; i++) {
           productIdsArray[i] = new NSString(response.Products[i].ProductIdentifier);
           productsArray[i] = response.Products[i];
       }
       userInfo = NSDictionary.FromObjectsAndKeys (productsArray, productIdsArray);
   }
   NSNotificationCenter.DefaultCenter.PostNotificationName (InAppPurchaseManagerProductsFetchedNotification, this, userInfo);
}

虽然关系图中未显示,但还应替代 RequestFailed 方法,以便你可以向用户提供一些反馈,以防 App Store 服务器无法访问(或发生其他错误)。 示例代码仅写入控制台,但真实的应用程序可能会选择查询 error.Code 属性并实现自定义行为(例如向用户发出警报)。

public override void RequestFailed (SKRequest request, NSError error)
{
   Console.WriteLine (" ** InAppPurchaseManager RequestFailed() " + error.LocalizedDescription);
}

此屏幕截图显示了刚刚加载后的示例应用程序(没有可用的产品信息时):

没有可用的产品信息时刚刚加载后的示例应用

无效产品

SKProductsRequest 还可返回无效产品 ID 的列表。 返回无效的产品通常是由于以下原因之一:

产品 ID 被错误键入 – 仅接受有效的产品 ID。

产品尚未获得批准 – 测试时,所有“已通过销售审批”的产品都应由 SKProductsRequest 返回;但在生产中,只会返回“已批准”的产品。

应用 ID 不明确 – 通配符应用 ID(带星号)不允许应用内购买。

预配配置文件不正确 – 如果在预配门户中更改了你的应用程序配置(例如启用了应用内购买),请记得在生成应用时重新生成和使用正确的预配配置文件。

未签订 iOS 付费应用程序合同 – 除非 Apple 开发人员帐户有有效的合同,否则 StoreKit 功能将完全无法工作。

二进制文件处于“已被拒绝”状态 – 如果有以前提交的二进制文件处于“已被拒绝”状态(由 App Store 团队或开发人员提交),则 StoreKit 功能将无法工作。

示例代码中的 ReceivedResponse 方法会将无效的产品输出到控制台:

public override void ReceivedResponse (SKProductsRequest request, SKProductsResponse response)
{
   // code removed for clarity
   foreach (string invalidProductId in response.InvalidProducts) {
       Console.WriteLine("Invalid product id: " + invalidProductId );
   }
}

显示本地化价格

价格层会为所有国际 App Store 中的每个产品指定特定价格。 若要确保为每个货币正确显示价格,请使用以下扩展方法(在 SKProductExtension.cs 中定义),而不是使用每个 SKProduct 的 Price 属性:

public static class SKProductExtension {
   public static string LocalizedPrice (this SKProduct product)
   {
       var formatter = new NSNumberFormatter ();
       formatter.FormatterBehavior = NSNumberFormatterBehavior.Version_10_4;  
       formatter.NumberStyle = NSNumberFormatterStyle.Currency;
       formatter.Locale = product.PriceLocale;
       var formattedString = formatter.StringFromNumber(product.Price);
       return formattedString;
   }
}

设置按钮的标题的代码使用如下所示的扩展方法:

string Buy = "Buy {0}"; // or a localizable string
buy5Button.SetTitle(String.Format(Buy, product.LocalizedPrice()), UIControlState.Normal);

使用两个不同的 iTunes 测试帐户(一个用于美国商店,另一个用于日本商店)会导致以下屏幕截图:

显示语言特定结果的两个不同的 iTunes 测试帐户

请注意,应用商店会影响用于产品信息和价格货币的语言,而设备的语言设置会影响标签和其他本地化内容。

回想一下,若要使用不同的商店测试帐户,你必须在“设置 > iTunes 和 App Store”中退出登录,然后重启应用程序以使用不同的帐户登录。 若要更改设备的语言,请转到“设置 > 常规 > 国际 > 语言”。