Compra de productos consumibles en Xamarin.iOS

Los productos consumibles son los más sencillos de implementar, ya que no hay ningún requisito de "restauración". Resultan útiles para productos como monedas de juegos o partes de funcionalidades de uso único. Los usuarios pueden volver a comprar productos consumibles una y otra vez.

Entrega de productos integrada

El código de ejemplo que acompaña a este documento muestra productos integrados: los identificadores de producto se codifican de forma rígida en la aplicación porque están estrechamente acoplados al código que "desbloquea" la característica después del pago. El proceso de compra se puede visualizar de la siguiente manera:

The purchasing process visualization

El flujo de trabajo básico es:

  1. La aplicación agrega un SKPayment a la cola. Si fuera necesario, se le pedirá al usuario su id. de Apple y se le solicitará que confirme el pago.

  2. StoreKit envía la solicitud al servidor para su procesamiento.

  3. Una vez completada la transacción, el servidor responde con un recibo de transacción.

  4. La subclase SKPaymentTransactionObserver recibe el recibo y lo procesa.

  5. La aplicación habilita el producto (actualizando NSUserDefaults o algún otro mecanismo) y, a continuación, llama al FinishTransaction de StoreKit.

Hay otro tipo de flujo de trabajo: Productos entregados por el servidor, que se describe más adelante en el documento (consulte la sección Comprobación de recibos y Productos entregados por el servidor).

Ejemplo de productos consumibles

El código InAppPurchaseSample contiene un proyecto denominado Consumibles que implementa una "moneda del juego" básica (denominada "créditos de mono"). En el ejemplo se muestra cómo implementar dos productos de compra desde la aplicación para permitir que el usuario compre tantos "créditos de mono" como desee: en una aplicación real también habría alguna manera de gastarlos.

La aplicación se muestra en estas capturas de pantalla: cada compra agrega más "créditos de mono" al saldo del usuario:

Each purchase adds more monkey credits to the users balance

Las interacciones entre clases personalizadas, StoreKit y App Store son así:

The interactions between custom classes, StoreKit and the App Store

Métodos ViewController

Además de las propiedades y los métodos necesarios para recuperar la información del producto, el controlador de vista requiere observadores de notificaciones adicionales para escuchar las notificaciones relacionadas con la compra. Estos son solo NSObjects, que se registrarán y quitarán en ViewWillAppear y ViewWillDisappear, respectivamente.

NSObject succeededObserver, failedObserver;

El constructor también creará la subclase SKProductsRequestDelegate ( InAppPurchaseManager) que, a su vez, creará y registrará el SKPaymentTransactionObserver ( CustomPaymentObserver).

La primera parte del procesamiento de una transacción de compra desde la aplicación es controlar la pulsación del botón cuando el usuario desea comprar algo, tal y como se muestra en el código siguiente de la aplicación de ejemplo:

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

La segunda parte de la interfaz de usuario controla la notificación de que la transacción se realizó correctamente, en este caso, actualizando el saldo mostrado:

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

La parte final de la interfaz de usuario muestra un mensaje si se cancela una transacción por algún motivo. En el código de ejemplo, un mensaje se escribe simplemente en la ventana de salida:

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

Además de estos métodos en el controlador de vista, una transacción de compra de producto consumible también requiere código en SKProductsRequestDelegate y SKPaymentTransactionObserver.

Métodos InAppPurchaseManager

El código de ejemplo implementa una serie de métodos relacionados con la compra en la clase InAppPurchaseManager, incluyendo el método PurchaseProduct que crea una instancia SKPayment y la agrega a la cola para su procesamiento:

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

Agregar el pago a la cola es una operación asincrónica. La aplicación recupera el control mientras StoreKit procesa la transacción y la envía a los servidores de Apple. En este momento, iOS comprobará que el usuario haya iniciado sesión en la App Store y le pedirá un id. de Apple y una contraseña si fuera necesario.

Suponiendo que el usuario se autentique correctamente en la App Store y acepte la transacción, SKPaymentTransactionObserver recibirá la respuesta de StoreKit y llamará al siguiente método para realizar la transacción y finalizarla.

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

El último paso consiste en asegurarse de notificar a StoreKit que se completó correctamente la transacción mediante una llamada a FinishTransaction:

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

Una vez entregado el producto, se debe llamar a SKPaymentQueue.DefaultQueue.FinishTransaction para quitar la transacción de la cola de pago.

Métodos SKPaymentTransactionObserver (CustomPaymentObserver)

StoreKit llama al método UpdatedTransactions cuando recibe una respuesta de los servidores de Apple y pasa una matriz de SKPaymentTransaction objetos para que el código la inspeccione. El método recorre en bucle cada transacción y realiza una función diferente en función del estado de la transacción (como se muestra aquí):

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

El método CompleteTransaction se trató anteriormente en esta sección: guarda los detalles de compra en NSUserDefaults, finaliza la transacción con StoreKit y, por último, notifica a la interfaz de usuario que se actualizará.

Compra de varios productos

Si tiene sentido en la aplicación comprar varios productos, use la clase SKMutablePayment y establezca el campo Cantidad:

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

El código que controla la transacción completada también debe consultar la propiedad Cantidad para cumplir correctamente la compra:

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

Cuando el usuario compre varias cantidades, la alerta de confirmación de StoreKit reflejará la cantidad, el precio unitario y el precio total que se cobrarán, tal y como se muestra en la siguiente captura de pantalla:

Confirming a purchase

Control de interrupciones de red

Las compras desde la aplicación requieren una conexión de red en funcionamiento para que StoreKit se comunique con los servidores de Apple. Si una conexión de red no estuviera disponible, la compra desde la aplicación no estará disponible.

Solicitudes de producto

Si la red no estuviera disponible al realizar un SKProductRequest, se llamará al método RequestFailed de la subclase SKProductsRequestDelegate ( InAppPurchaseManager), tal y como se muestra a continuación:

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 escucha la notificación y muestra un mensaje en los botones de compra:

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

Dado que las conexiones de red podrían ser efímeras en dispositivos móviles, es posible que las aplicaciones quieran supervisar el estado de la red mediante el marco SystemConfiguration y volver a intentarlo cuando haya conexiones de red disponibles. Consulte el de Apple o el que usen.

Transacciones de compra

La cola de pago de StoreKit almacenará y reenviará las solicitudes de compra si fuera posible, por lo que el efecto de interrupciones de red variará en función de cuándo se produjo un error en la red durante el proceso de compra.

Si se produjese un error durante una transacción, la subclase SKPaymentTransactionObserver ( CustomPaymentObserver) tendrá la llamada de método UpdatedTransactions realizada y la clase SKPaymentTransaction estará en estado Con errores.

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

El método FailedTransaction detecta si el error se debe a la cancelación del usuario, tal y como se muestra aquí:

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

Incluso si se produjese un error en una transacción, se deberá llamar al método FinishTransaction para quitar la transacción de la cola de pago:

SKPaymentQueue.DefaultQueue.FinishTransaction(transaction);

A continuación, el código de ejemplo envía una notificación para que ViewController pueda mostrar un mensaje. Las aplicaciones no deberían mostrar un mensaje adicional si el usuario canceló la transacción. Otros códigos de error que podrían producirse incluyen:

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.

Control de restricciones

La característica Configuración > General > Restricciones de iOS permite a los usuarios bloquear determinadas características de sus dispositivos.

Consulte si al usuario se le permitirá realizar compras desde la aplicación a través del método SKPaymentQueue.CanMakePayments. Si se devolviese false, el usuario no podrá acceder a compras desde la aplicación. StoreKit mostrará automáticamente un mensaje de error al usuario si se intentasen realizar compras. Al comprobar este valor, la aplicación podría ocultar los botones de compra o realizar alguna otra acción para ayudar al usuario.

En el archivo InAppPurchaseManager.cs, el método CanMakePayments ajusta la función StoreKit de la siguiente manera:

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

Para probar este método, use la característica Restricciones de iOS para deshabilitar las Compras desde la aplicación:

Use the Restrictions feature of iOS to disable In-App Purchases

Este código de ejemplo de ConsumableViewController reacciona a la devolución de false de CanMakePayments mostrando el texto Deshabilitado de AppStore en los botones deshabilitados.

// 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);
}

La aplicación tendrá este aspecto cuando la característica Compras desde la aplicación esté restringida: los botones de compra se deshabilitarán.

The application looks like this when the In-App Purchases feature is restricted the purchase buttons are disabled

La información del producto todavía se podrá solicitar cuando CanMakePayments sea false, por lo que la aplicación todavía podrá recuperar y mostrar los precios. Esto significa que si quitamos la comprobación de CanMakePayments del código, los botones de compra seguirán estando activos, pero cuando se intente realizar una compra, el usuario verá un mensaje indicando que Las compras desde la aplicación no están permitidas (generado por StoreKit cuando se accede a la cola de pago):

In-app purchases are not allowed

Las aplicaciones reales podrían adoptar un enfoque diferente para controlar la restricción, como ocultar los botones por completo y, quizá, ofrecer un mensaje más detallado que la alerta que muestra StoreKit automáticamente.