在 Xamarin.iOS 中购买易耗品

易耗品是最简单的实现,因为没有“还原”要求。 它们适用于游戏内货币或单用功能等产品。 用户可以不限次数地再次购买易耗品。

内置产品交付

本文档随附的示例代码演示了产品 ID 硬编码到应用程序中的内置产品,因为它们与付款后解锁功能的代码紧密耦合。 购买过程可以如下所示进行可视化:

购买过程可视化效果

基本工作流为:

  1. 应用向队列添加 SKPayment。 如果需要,系统会提示用户输入其 Apple ID,并要求确认付款。

  2. StoreKit 将请求发送到服务器进行处理。

  3. 交易完成后,服务器将使用交易收据进行响应。

  4. SKPaymentTransactionObserver 子类接收并处理收据。

  5. 应用程序启用产品(通过更新 NSUserDefaults 或其他机制),然后调用 StoreKit 的 FinishTransaction

文档中稍后将讨论另一种类型的工作流 - 服务器交付的产品(请参阅收据验证和服务器交付的产品部分)。

易耗品示例

该示例包含一个被称为易耗品的项目,用于实现基本的“游戏内货币”(称为“猴子积分”)。 该示例演示如何实现两种应用内购买产品,以允许用户在实际应用程序中购买尽可能多的“猴子信用额度”,也会有某种方式花费它们!

应用程序显示在以下屏幕截图中,每次购买都会向用户余额添加更多“猴子信用额度”:

每次购买都会为用户余额增加更多的猴子积分

自定义类、StoreKit 和 App Store 之间的交互如下所示:

自定义类、StoreKit 和 App Store 之间的交互

ViewController 方法

除了检索产品信息所需的属性和方法外,视图控制器还要求其他通知观察程序侦听与购买相关的通知。 这些只是 NSObjects,它将分别在 ViewWillAppearViewWillDisappear 中注册和删除。

NSObject succeededObserver, failedObserver;

构造函数还将创建 SKProductsRequestDelegate 子类 (InAppPurchaseManager),进而创建并注册 SKPaymentTransactionObserver (CustomPaymentObserver)。

处理应用内购买交易的第一部分是当用户希望购买内容时处理按钮按下,如示例应用程序中的以下代码所示:

buy5Button.TouchUpInside += (sender, e) => {
   iap.PurchaseProduct (Buy5ProductId);
};​
buy10Button.TouchUpInside += (sender, e) => {
   iap.PurchaseProduct (Buy10ProductId);
};

用户界面的第二部分是通过更新显示的余额来处理事务成功的通知:

succeededObserver = NSNotificationCenter.DefaultCenter.AddObserver (InAppPurchaseManager.InAppPurchaseManagerTransactionSucceededNotification,
(notification) => {
   balanceLabel.Text = CreditManager.Balance() + " monkey credits";
});

用户界面的最后一部分会在交易因故取消时显示一条信息。 在示例代码中,仅将消息写入输出窗口:

failedObserver = NSNotificationCenter.DefaultCenter.AddObserver (InAppPurchaseManager.InAppPurchaseManagerTransactionFailedNotification,
(notification) => {
   Console.WriteLine ("Transaction Failed");
});

除了视图控制器上的这些方法外,易耗品购买交易还需要 SKProductsRequestDelegateSKPaymentTransactionObserver 上的代码。

InAppPurchaseManager 方法

示例代码在 InAppPurchaseManager 类上实现许多与购买相关的方法,包括创建 SKPayment 实例并将其添加到队列中进行处理的 PurchaseProduct 方法:

public void PurchaseProduct(string appStoreProductId)
{
   SKPayment payment = SKPayment.PaymentWithProduct (appStoreProductId);​
   SKPaymentQueue.DefaultQueue.AddPayment (payment);
}

将付款添加到队列是一项异步操作。 当 StoreKit 处理事务并将其发送到 Apple 服务器时,应用程序将重新获得控制权。 此时,iOS 将验证用户是否已登录到 App Store,并在需要时提示她输入 Apple ID 和密码。

假设用户已成功通过 App Store 进行身份验证并同意交易,则 SKPaymentTransactionObserver 将收到 StoreKit 的响应,并调用以下方法来完成交易并完成该事务。

public void CompleteTransaction (SKPaymentTransaction transaction)
{
   var productId = transaction.Payment.ProductIdentifier;
   // Register the purchase, so it is remembered for next time
   PhotoFilterManager.Purchase(productId);
   FinishTransaction(transaction, true);
}

最后一步是通过调用 FinishTransaction,确保通知 StoreKit 已成功完成事务:

public void FinishTransaction(SKPaymentTransaction transaction, bool wasSuccessful)
{
   // remove the transaction from the payment queue.
   SKPaymentQueue.DefaultQueue.FinishTransaction(transaction);  // THIS IS IMPORTANT - LET'S APPLE KNOW WE'RE DONE !!!!
   using (var pool = new NSAutoreleasePool()) {
       NSDictionary userInfo = NSDictionary.FromObjectsAndKeys(new NSObject[] {transaction},new NSObject[] {new NSString("transaction")});
       if (wasSuccessful) {
           // send out a notification that we've finished the transaction
           NSNotificationCenter.DefaultCenter.PostNotificationName (InAppPurchaseManagerTransactionSucceededNotification, this, userInfo);
       } else {
           // send out a notification for the failed transaction
           NSNotificationCenter.DefaultCenter.PostNotificationName (InAppPurchaseManagerTransactionFailedNotification, this, userInfo);
       }
   }
}

交付产品后,必须调用 SKPaymentQueue.DefaultQueue.FinishTransaction 才能从付款队列中删除交易。

SKPaymentTransactionObserver (CustomPaymentObserver) 方法

StoreKit 从 Apple 服务器收到响应时调用 UpdatedTransactions 方法,并传递代码要检查的 SKPaymentTransaction 对象的数组。 该方法循环访问每个事务,并根据事务状态执行不同的函数(如下所示):

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;
           default:
               break;
       }
   }
}

本部分前面介绍了 CompleteTransaction 方法 - 它将购买详细信息保存到 NSUserDefaults,最后使用 StoreKit 完成交易,并通知 UI 更新。

购买多个产品

如果应用程序中购买多个产品是有意义的,请使用 SKMutablePayment 类并设置“数量”字段:

public void PurchaseProduct(string appStoreProductId)
{
   SKMutablePayment payment = SKMutablePayment.PaymentWithProduct (appStoreProductId);
   payment.Quantity = 4; // hardcoded as an example
   SKPaymentQueue.DefaultQueue.AddPayment (payment);
}

处理已完成事务的代码还必须查询 Quantity 属性才能正确完成购买:

public void CompleteTransaction (SKPaymentTransaction transaction)
{
   var productId = transaction.Payment.ProductIdentifier;
   var qty = transaction.Payment.Quantity;
   if (productId == ConsumableViewController.Buy5ProductId)
       CreditManager.Add(5 * qty);
   else if (productId == ConsumableViewController.Buy10ProductId)
       CreditManager.Add(10 * qty);
   else
       Console.WriteLine ("Shouldn't happen, there are only two products");
   FinishTransaction(transaction, true);
}

当用户购买多个数量时,StoreKit 确认警报将反映数量、单价和要收取的总价格,如以下屏幕截图所示:

确认购买

处理网络中断

应用内购买需要有效的网络连接,以便 StoreKit 与 Apple 服务器通信。 如果网络连接不可用,则应用内购买将不可用。

产品请求

如果在创建 SKProductRequest 时网络不可用,将调用 SKProductsRequestDelegate 子类( InAppPurchaseManager )的 RequestFailed 方法,如下所示:

public override void RequestFailed (SKRequest request, NSError error)
{
   using (var pool = new NSAutoreleasePool()) {
       NSDictionary userInfo = NSDictionary.FromObjectsAndKeys(new NSObject[] {error},new NSObject[] {new NSString("error")});
       // send out a notification for the failed transaction
       NSNotificationCenter.DefaultCenter.PostNotificationName (InAppPurchaseManagerRequestFailedNotification, this, userInfo);
   }
}

然后,ViewController 侦听通知,并在购买按钮中显示一条消息:

requestObserver = NSNotificationCenter.DefaultCenter.AddObserver (InAppPurchaseManager.InAppPurchaseManagerRequestFailedNotification,
(notification) => {
   Console.WriteLine ("Request Failed");
   buy5Button.SetTitle ("Network down?", UIControlState.Disabled);
   buy10Button.SetTitle ("Network down?", UIControlState.Disabled);
});

由于网络连接在移动设备上可能是暂时性的,因此应用程序可能希望使用 SystemConfiguration 框架监视网络状态,并在网络连接可用时重新尝试。 请参阅 Apple 的或使用它的。

购买交易

如果可能,StoreKit 付款队列将存储并转发购买请求,因此网络中断的影响将因在购买过程中网络失败而有所不同。

如果在事务期间发生错误,SKPaymentTransactionObserver 子类( CustomPaymentObserver)将调用 UpdatedTransactions 方法,并且 SKPaymentTransaction 类将处于“失败”状态。

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;
           default:
               break;
       }
   }
}

FailedTransaction 方法检测错误是否是由于用户取消导致的,如下所示:

public void FailedTransaction (SKPaymentTransaction transaction)
{
   //SKErrorPaymentCancelled == 2
   if (transaction.Error.Code == 2) // user cancelled
       Console.WriteLine("User CANCELLED FailedTransaction Code=" + transaction.Error.Code + " " + transaction.Error.LocalizedDescription);
   else // error!
       Console.WriteLine("FailedTransaction Code=" + transaction.Error.Code + " " + transaction.Error.LocalizedDescription);
   FinishTransaction(transaction,false);
}

即使事务失败,也必须调用 FinishTransaction 方法才能从付款队列中删除事务:

SKPaymentQueue.DefaultQueue.FinishTransaction(transaction);

然后,示例代码发送通知,以便 ViewController 可以显示消息。 如果用户取消了事务,应用程序不应显示其他消息。 可能发生的其他错误代码包括:

FailedTransaction Code=0 Cannot connect to iTunes Store
FailedTransaction Code=5002 An unknown error has occurred
FailedTransaction Code=5020 Forget Your Password?
Applications may detect and respond to specific error codes, or handle them in the same way.

处理限制

iOS 的“设置”“常规”>“限制”>功能允许用户锁定其设备的某些功能。

可以通过 SKPaymentQueue.CanMakePayments 方法查询用户是否允许进行应用内购买。 如果返回 false,则用户无法访问应用内购买。 如果尝试购买,StoreKit 将自动向用户显示错误消息。 通过检查此值,应用程序可以隐藏购买按钮或采取其他一些操作来帮助用户。

InAppPurchaseManager.cs 文件中,CanMakePayments 方法包装 StoreKit 函数,如下所示:

public bool CanMakePayments()
{
   return SKPaymentQueue.CanMakePayments;​
}

若要测试此方法,请使用 iOS 的限制功能禁用应用内购买

使用 iOS 的限制功能禁用应用内购买

ConsumableViewController 中的此示例代码通过显示禁用按钮上的 AppStore Disabled 文本来响应返回 false 的 CanMakePayments

// only if we can make payments, request the prices
if (iap.CanMakePayments()) {
   // now go get prices, if we don't have them already
   if (!pricesLoaded)
       iap.RequestProductData(products); // async request via StoreKit -> App Store
} else {
   // can't make payments (purchases turned off in Settings?)
   // the buttons are disabled by default, and only enabled when prices are retrieved
   buy5Button.SetTitle ("AppStore disabled", UIControlState.Disabled);
   buy10Button.SetTitle ("AppStore disabled", UIControlState.Disabled);
}

当应用内购买功能受到限制时,应用程序将如下所示 - 已禁用购买按钮。

当应用内购买功能受到限制时,应用程序将如下所示,购买按钮会被禁用

CanMakePayments 为 false 时仍可以请求产品信息,因此应用仍可以检索和显示价格。 这意味着,如果我们从代码中删除了 CanMakePayments 检查,则购买按钮仍将处于活动状态,但是当尝试购买时,用户会看到一条消息,指出不允许应用内购买(访问付款队列时由 StoreKit 生成):

不允许应用内购买

实际应用程序可能采用不同的方法来处理限制,例如完全隐藏按钮,并可能提供比 StoreKit 自动显示的警报更详细的消息。