Purchasing Consumable Products in Xamarin.iOS
Consumable products are the simplest to implement, since there is no ‘restore’ requirement. They are useful for products like in-game currency or a single-use piece of functionality. Users can re-purchase consumable products over-and-over again.
Built-In Product Delivery
The sample code accompanying this document demonstrates built-in products – the Product IDs are hardcoded into the application because they are tightly coupled to the code that ‘unlocks’ the feature after payment. The purchasing process can be visualized like this:
The basic workflow is:
The app adds an
SKPayment
to the queue. If required the user will be prompted for their Apple ID, and asked to confirm the payment.StoreKit sends the request to the server for processing.
When the transaction is complete, the server responds with a transaction receipt.
The
SKPaymentTransactionObserver
subclass receives the receipt and processes it.The application enables the product (by updating
NSUserDefaults
or some other mechanism), and then calls StoreKit’sFinishTransaction
.
There is another type of workflow – Server-Delivered Products – that is discussed later in the document (see the section Receipt Verification and Server-Delivered Products).
Consumable Products example
The sample contains a project called Consumables that implements a basic ‘in-game currency’ (called “monkey credits”). The sample shows how to implement two in-app purchase products to allow the user to buy as many “monkey credits” as they wish – in a real application there would also be some way of spending them!
The application is shown in these screenshots – each purchase adds more “monkey credits” to the user’s balance:
The interactions between custom classes, StoreKit and the App Store look like this:
ViewController Methods
In addition to the properties and methods required to retrieve product
information, the view controller requires additional notification observers to
listen for purchase-related notifications. These are just NSObjects
that will be registered and removed in ViewWillAppear
and ViewWillDisappear
respectively.
NSObject succeededObserver, failedObserver;
The constructor will also create the SKProductsRequestDelegate
subclass ( InAppPurchaseManager
) that in turn creates and registers
the SKPaymentTransactionObserver
( CustomPaymentObserver
).
The first part of processing an in-app purchase transaction is to handle the button press when the user wishes to buy something, as shown in the following code from the sample application:
buy5Button.TouchUpInside += (sender, e) => {
iap.PurchaseProduct (Buy5ProductId);
};
buy10Button.TouchUpInside += (sender, e) => {
iap.PurchaseProduct (Buy10ProductId);
};
The second part of the user interface is handling the notification that the transaction succeeded, in this case by updating the displayed balance:
succeededObserver = NSNotificationCenter.DefaultCenter.AddObserver (InAppPurchaseManager.InAppPurchaseManagerTransactionSucceededNotification,
(notification) => {
balanceLabel.Text = CreditManager.Balance() + " monkey credits";
});
The final part of the user interface is displaying a message if a transaction is cancelled for some reason. In the example code a message is simply written to the output window:
failedObserver = NSNotificationCenter.DefaultCenter.AddObserver (InAppPurchaseManager.InAppPurchaseManagerTransactionFailedNotification,
(notification) => {
Console.WriteLine ("Transaction Failed");
});
In addition to these methods on the view controller, a consumable product
purchase transaction also requires code on the SKProductsRequestDelegate
and the SKPaymentTransactionObserver
.
InAppPurchaseManager Methods
The sample code implements a number of purchase related methods on the
InAppPurchaseManager class, including the PurchaseProduct
method
that creates an SKPayment
instance and adds it to the queue for
processing:
public void PurchaseProduct(string appStoreProductId)
{
SKPayment payment = SKPayment.PaymentWithProduct (appStoreProductId);
SKPaymentQueue.DefaultQueue.AddPayment (payment);
}
Adding the payment to the queue is an asynchronous operation. The application regains control while StoreKit processes the transaction and sends it to Apple’s servers. It is at this point that iOS will verify the user is logged in to the App Store and prompt her for an Apple ID and password if required.
Assuming the user successfully authenticates with the App Store
and agrees to the transaction, the SKPaymentTransactionObserver
will receive StoreKit’s response and call the following method to fulfill the
transaction and finalize it.
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);
}
The last step is to ensure that you notify StoreKit that you have
successfully fulfilled the transaction, by calling 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);
}
}
}
Once the product is delivered, SKPaymentQueue.DefaultQueue.FinishTransaction
must be called to
remove the transaction from the payment queue.
SKPaymentTransactionObserver (CustomPaymentObserver) Methods
StoreKit calls the UpdatedTransactions
method when it receives a
response from Apple’s servers, and passes an array of SKPaymentTransaction
objects for your code to inspect. The method
loops through each transaction and performs a different function based on the
transaction state (as shown here):
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;
}
}
}
The CompleteTransaction
method was covered earlier in this
section – it saves the purchase details to NSUserDefaults
,
finalizes the transaction with StoreKit and finally notifies the UI to
update.
Purchasing Multiple Products
If it makes sense in your application to purchase multiple products, use the SKMutablePayment
class and set the Quantity field:
public void PurchaseProduct(string appStoreProductId)
{
SKMutablePayment payment = SKMutablePayment.PaymentWithProduct (appStoreProductId);
payment.Quantity = 4; // hardcoded as an example
SKPaymentQueue.DefaultQueue.AddPayment (payment);
}
The code handling the completed transaction must also query the Quantity property to correctly fulfill the purchase:
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);
}
When the user purchases multiple quantities, the StoreKit confirmation alert will reflect the quantity, the unit price and the total price they’ll be charged, as shown in the following screenshot:
Handling Network Outages
In-app purchases require a working network connection for StoreKit to communicate with Apple’s servers. If a network connection is not available, then in-app purchasing will be unavailable.
Product Requests
If the network is unavailable while making an SKProductRequest
,
the RequestFailed
method of the SKProductsRequestDelegate
subclass
( InAppPurchaseManager
) will be called, as shown below:
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);
}
}
The ViewController then listens for the notification and displays a message in the purchase buttons:
requestObserver = NSNotificationCenter.DefaultCenter.AddObserver (InAppPurchaseManager.InAppPurchaseManagerRequestFailedNotification,
(notification) => {
Console.WriteLine ("Request Failed");
buy5Button.SetTitle ("Network down?", UIControlState.Disabled);
buy10Button.SetTitle ("Network down?", UIControlState.Disabled);
});
Because a network connection can be transient on mobile devices, applications may wish to monitor network status using the SystemConfiguration framework, and re-try when a network connection is available. Refer to Apple’s or the that uses it.
Purchase Transactions
The StoreKit payment queue will store and forward purchase requests if possible, so the effect of a network outage will vary depending on when the network failed during the purchase process.
If an error does
occur during a transaction, the SKPaymentTransactionObserver
subclass ( CustomPaymentObserver
) will have the UpdatedTransactions
method called and the SKPaymentTransaction
class will be in the Failed state.
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;
}
}
}
The FailedTransaction
method detects whether the error was due
to user-cancellation, as shown here:
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);
}
Even if a transaction fails, the FinishTransaction
method must
be called to remove the transaction from the payment queue:
SKPaymentQueue.DefaultQueue.FinishTransaction(transaction);
The example code then sends a notification so that the ViewController can display a message. Applications should not show an additional message if the user canceled the transaction. Other error codes that might occur include:
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.
Handling Restrictions
The Settings > General > Restrictions feature of iOS allows users to lock certain features of their device.
You
can query whether the user is allowed to make in-app purchases via the SKPaymentQueue.CanMakePayments
method. If this returns false then
the user cannot access in-app purchasing. StoreKit will automatically display an
error message to the user if a purchase is attempted. By checking this value
your application can instead hide the purchase buttons or take some other action
to help the user.
In the InAppPurchaseManager.cs
file the CanMakePayments
method wraps the StoreKit function like
this:
public bool CanMakePayments()
{
return SKPaymentQueue.CanMakePayments;
}
To test this method, use the Restrictions feature of iOS to disable In-App Purchases:
This example code from ConsumableViewController
reacts to CanMakePayments
returning false by displaying AppStore Disabled text on the disabled buttons.
// 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);
}
The application looks like this when the In-App Purchases feature is restricted – the purchase buttons are disabled.
Product information can still be requested when CanMakePayments
is
false, so the app can still retrieve and display prices. This means if we
removed the CanMakePayments
check from the code the purchase
buttons would still be active, however when a purchase is attempted the user
will see a message that In-app purchases are not allowed
(generated by StoreKit when the payment queue is accessed):
Real-world applications may take a different approach to handling the restriction, such as hiding the buttons altogether and perhaps offering a more detailed message than the alert that StoreKit shows automatically.