StoreKit 概览和检索 Xamarin.iOS 中的产品信息
应用内购买的用户界面显示在下面的屏幕截图中。 在发生任何交易之前,应用程序必须检索产品的价格和说明以进行显示。 然后,当用户按下“购买”时,应用程序向 StoreKit 发出请求,它负责管理确认对话框和 Apple ID 登录。 假设交易成功,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 操作都需要真实的设备来进行测试。 检索产品信息(即价格和说明)将在模拟器中进行,但购买和还原操作将返回错误(例如 FailedTransaction Code=5002 发生了未知错误)。
注意:StoreKit 无法在 iOS 模拟器中运行。 在 iOS 模拟器中运行应用程序时,如果应用程序尝试检索付款队列,StoreKit 会记录警告。 必须在实际的设备上测试商店。
重要提示:不要在“设置”应用程序中使用测试帐户登录。 可以使用“设置”应用程序退出登录任何现有的 Apple ID 帐户,然后必须等待在应用内购买序列中收到提示再使用测试 Apple ID 登录。
如果尝试使用测试帐户登录到真实商店,它将自动转换为真实的 Apple ID。 该帐户将不再可用于测试。
若要测试 StoreKit 代码,必须退出登录常规 iTunes 测试帐户,并使用链接到测试商店的特殊测试帐户(在 iTunes Connect 中创建)登录。 若要退出登录当前帐户,请访问“设置 > iTunes 和 App Store”,如下所示:
然后,在应用中受 StoreKit 请求时使用测试帐户登录:
若要在 iTunes Connect 中创建测试用户,请单击主页上的“用户和角色”。
选择“沙盒测试人员”
它会显示现有用户的列表。 可以添加新用户或删除现有记录。 门户(当前)不允许查看或编辑现有测试用户,因此建议留好创建的每个测试用户的记录(尤其是你分配的密码)。 删除测试用户后,不能将电子邮件地址重新用于另一个测试帐户。
新的测试用户具有与真实 Apple ID 类似的特性(例如名称、密码、机密问题和答案)。 保留此处输入的所有详细信息的记录。 “选择 iTunes Store”字段将确定在以该用户身份登录时,应用内购买将使用哪种货币和语言。
检索产品信息
销售应用内购买产品的第一步是显示它:从 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 控件(请注意,我们显示了 LocalizedTitle
和 LocalizedDescription
– 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 和 App Store”中退出登录,然后重启应用程序以使用不同的帐户登录。 若要更改设备的语言,请转到“设置 > 常规 > 国际 > 语言”。