In App Purchase MonoGame iOS

This article uses Space Racer MT, a game developed by JMA Web Technologies, to demonstrate these features. To download the game, please click here.

This scenario covers an in-app purchase for buying more levels.

How It Works

The InAppPurchase manager sends a payment request to Apple, using the supplied ProductID. Upon success, the app stores the receipt and sets a is purchased property to true. The receipt ensures the customer purchases new products only. The is purchased property it set in the game to activate the purchase. In Space Racer MT, the is purchased property enables extra levels and difficulties.

The code uses the SKProductsRequest and SKProductsResponse objects to communicate to Apple. The SKProductsRequest contains the ProductID. Apple will return the price and title, based on that ID, for the proper country and language. The price needs to change for each country, using the SKProductExtender class, since the US dollar is a different amount.

It takes a few seconds to reach Apple and there is code for loading until Apple responds.

Installation

Here are the steps to integrate your game with iOS in app purchases:

First, uninstall your app from your device.

In iTunes Connect, add an in app purchase:

Manage In-App Purchases > Create New

Note the product ID because you pass this value to Apple.

image

In my case, I chose non-consumable because I only want the user to purchase the upgrade once. Consumable means that the upgrade can be purchased multiple times, such as slot machine tokens.

When you make the code, here are some notes:

First, you need to return the product’s localized price. $.99 is different in most countries.

Second, the in app purchase system has downtime. You need to write code for this scenario.

Here is the code:

PurchaseScreen.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using GameStateManagement;
using Microsoft.Xna.Framework.GamerServices;
  
#if IOS
using SpaceRacerMT;
using MonoTouch.Foundation;
using MonoTouch.StoreKit;
using MonoTouch.UIKit;
#endif
  
  
namespace SpaceRacer.Screens
{
    class PurchaseScreen : MenuScreen
    {
  
        static InAppPurchaseManager app;
        static MenuEntry buyLevels;
        static MenuEntry buyDifficulty;
        static MenuEntry restoreEntry;
         
        public PurchaseScreen (bool showHigh) : base("PURCHASE")
        {
            PrepareInAppPurchase ();
            buyLevels = new MenuEntry ("Buy 3 More Levels");
            buyDifficulty = new MenuEntry("Buy Hard Difficulty");
            restoreEntry = new MenuEntry("Restore Purchases");
                 
            if(Reachability.IsHostReachable("google.com"))
            {
                buyLevels.Selected += BuyLevelsMenuEntrySelected;
                buyDifficulty.Selected += BuyDifficultyMenuEntrySelected;
            }
            else
            {
                UIAlertView alert = new UIAlertView("No Internet", "Please log onto the internet before purchasing.", null, "OK", null);
                alert.Show();
            }
  
  
            restoreEntry.Selected += RestoreMenuEntrySelected;
  
            HaveItemsBeenPurchased();
  
            MenuEntry mainMenuEntry = new MenuEntry("Main Menu");
            mainMenuEntry.Selected += MainMenuEntrySelected;
            MenuEntries.Add(mainMenuEntry);
        }
  
        void HaveItemsBeenPurchased ()
        {
            bool isProUpgradePurchased = NSUserDefaults.StandardUserDefaults.BoolForKey("isProUpgradePurchased");
            bool isHardUpgradeDifficultyPurchased = NSUserDefaults.StandardUserDefaults.BoolForKey("isHardUpgradeDifficultyPurchased");
  
            if(!isProUpgradePurchased)
            {
                MenuEntries.Add(buyLevels);
            }
  
            if(!isHardUpgradeDifficultyPurchased)
            {
                MenuEntries.Add(buyDifficulty);
            }
  
            if(!isHardUpgradeDifficultyPurchased || !isProUpgradePurchased)
            {
                MenuEntries.Add(restoreEntry);
            }
  
            if(isHardUpgradeDifficultyPurchased && isProUpgradePurchased)
            {
                UIAlertView alert = new UIAlertView("No Purchase", "You have purchased all the items in the store", null, "OK", null);
                alert.Show();
            }
        }
        void RestoreMenuEntrySelected(object sender, PlayerIndexEventArgs e)
        {
            SKPaymentQueue.DefaultQueue.RestoreCompletedTransactions();
            ScreenManager.AddScreen(new MainMenuScreen(), e.PlayerIndex);
        }
         
        void BuyLevelsMenuEntrySelected (object sender, PlayerIndexEventArgs e)
        {
            app.PurchaseProUpgrade();
        }
         
        void BuyDifficultyMenuEntrySelected (object sender, PlayerIndexEventArgs e)
        {
            app.PurchaseHardDifficultyUpgrade();
        }
  
        void MainMenuEntrySelected (object sender, PlayerIndexEventArgs e)
        {
            ScreenManager.AddScreen (new MainMenuScreen (), e.PlayerIndex);
        }
  
        void PrepareInAppPurchase ()
        {   
            app = new InAppPurchaseManager ();
            app.LoadStore();
        }
  
    }
}

InAppPurchaseManager.cs

using System;
using MonoTouch.StoreKit;
using MonoTouch.Foundation;
using System.Globalization;
using System.Threading;
  
/// this is the first pass translation of the code used in the store kit from this tutorial http://troybrant.net/blog/2010/01/in-app-purchases-a-full-walkthrough/
using System.Collections.Generic;
  
  
    public class InAppPurchaseManager : SKProductsRequestDelegate
    {
        public static string InAppPurchaseManagerTransactionFailedNotification  = "InAppPurchaseManagerTransactionFailedNotification";
        public static string InAppPurchaseManagerTransactionSucceededNotification  = "InAppPurchaseManagerTransactionSucceededNotification";
        public static string InAppPurchaseManagerProductsFetchedNotification  = "InAppPurchaseManagerProductsFetchedNotification";
  
        public static string InAppPurchaseProUpgradeProductId = "com.jmawebtechnologies.spaceracermt.newproupgrade";
        public static string InAppPurchaseHardDifficultyUpgradeProductId = "com.jmawebtechnologies.spaceracermt.newharddifficultyupgrade";
  
        private MonoTouch.StoreKit.SKProduct proUpgradeProduct;
        private SKProductsRequest productsRequest;
        private MySKPaymentObserver theObserver;
         
        public SKProduct[] products;
     
        public InAppPurchaseManager ()
        {
            theObserver = new MySKPaymentObserver(this);
#if DEBUG
  
        Console.WriteLine("Adding SKPayment Observer.");
  
#endif
        }
     
        /// <summary>
        /// Gets data for products
        /// </summary>
        public void requestUpgradeProductData()
        {
            NSSet productIdentifiers  = NSSet.MakeNSObjectSet<NSString>(new NSString[]{new NSString(InAppPurchaseProUpgradeProductId), new NSString(InAppPurchaseHardDifficultyUpgradeProductId)});
            productsRequest  = new SKProductsRequest(productIdentifiers);
            productsRequest.Delegate = this;
            productsRequest.Start();
        }
  
        public override void ReceivedResponse (SKProductsRequest request, SKProductsResponse response)
        {
  
            products = response.Products;
             
#if DEBUG
            foreach(SKProduct proUpgradeProduct in products)
            {
                Console.WriteLine("Product title: " + proUpgradeProduct.LocalizedTitle);
                Console.WriteLine("Product description: " + proUpgradeProduct.LocalizedDescription);
                Console.WriteLine("Product price: " + proUpgradeProduct.LocalizedPrice());
                Console.WriteLine("Product id: " + proUpgradeProduct.ProductIdentifier);
            }
  
            foreach(string invalidProductId in response.InvalidProducts)
            {
                Console.WriteLine("Invalid product id: " + invalidProductId );
            }
#endif
            // finally release the reqest we alloc/init’ed in requestProUpgradeProductData
            productsRequest.Dispose();
  
  
            NSNotificationCenter.DefaultCenter.PostNotificationName(InAppPurchaseManagerProductsFetchedNotification,this,null);
  
  
        }
  
        //
        // call this method once on startup
        //
        public void LoadStore()
        {
            SKPaymentQueue.DefaultQueue.AddTransactionObserver(theObserver);
            this.requestUpgradeProductData();
        }
        //
        // call this before making a purchase
        //
        public bool canMakeProUpgrade()
        {
            return SKPaymentQueue.CanMakePayments;  
        }
  
        //
        // kick off the upgrade transaction
        //
        public void PurchaseProUpgrade()
        {
            SKPayment payment = SKPayment.PaymentWithProduct(InAppPurchaseProUpgradeProductId); 
            SKPaymentQueue.DefaultQueue.AddPayment(payment);
        }
 
        public void PurchaseHardDifficultyUpgrade()
        {
            SKPayment payment = SKPayment.PaymentWithProduct(InAppPurchaseHardDifficultyUpgradeProductId);
            SKPaymentQueue.DefaultQueue.AddPayment(payment);
        }
  
  
  
        //
        // saves a record of the transaction by storing the receipt to disk
        //
        public void recordTransaction(SKPaymentTransaction transaction)
        {
            if(transaction.Payment.ProductIdentifier == InAppPurchaseProUpgradeProductId)
            {
                NSUserDefaults.StandardUserDefaults.SetNativeField("proUpgradeTransactionReceipt",transaction.TransactionReceipt);
                NSUserDefaults.StandardUserDefaults.Synchronize();
            }
             
                if(transaction.Payment.ProductIdentifier == InAppPurchaseHardDifficultyUpgradeProductId)
            {
                NSUserDefaults.StandardUserDefaults.SetNativeField("hardDifficultyUpgradeTransactionReceipt",transaction.TransactionReceipt);
                NSUserDefaults.StandardUserDefaults.Synchronize();
            }
        }
  
        //
        // enable pro features
        //
        public void provideContent(string productID)
        {
            if(productID == InAppPurchaseProUpgradeProductId)
            {
                NSUserDefaults.StandardUserDefaults.SetBool(true,"isProUpgradePurchased");
                NSUserDefaults.StandardUserDefaults.Synchronize();
            }
 
            if (productID == InAppPurchaseHardDifficultyUpgradeProductId)
            {
                NSUserDefaults.StandardUserDefaults.SetBool(true, "isHardUpgradeDifficultyPurchased");
                NSUserDefaults.StandardUserDefaults.Synchronize();
            }
        }
  
  
        //
        // removes the transaction from the queue and posts a notification with the transaction result
        //
        public void finishTransaction(SKPaymentTransaction transaction, bool wasSuccessful)
        {
            // remove the transaction from the payment queue.
            SKPaymentQueue.DefaultQueue.FinishTransaction(transaction);
            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);
            }
        }
        //
        // called when the transaction was successful
        //
        public void completeTransaction(SKPaymentTransaction transaction)
        {
            this.recordTransaction(transaction);
            this.provideContent(transaction.Payment.ProductIdentifier);
            this.finishTransaction(transaction,true);
        }
        //
        // called when a transaction has been restored and and successfully completed
        //
        public void restoreTransaction(SKPaymentTransaction transaction)
        {
            this.recordTransaction(transaction.OriginalTransaction);
            this.provideContent(transaction.OriginalTransaction.Payment.ProductIdentifier);
            this.finishTransaction(transaction.OriginalTransaction, true);
        }
        //
        // called when a transaction has failed
        //
        public void failedTransaction(SKPaymentTransaction transaction)
        {   
            //SKErrorPaymentCancelled == 2
            if (transaction.Error.Code != 2)
            {
                // error!
                this.finishTransaction(transaction,false);
            }
            else
            {
            // this is fine, the user just cancelled, so don’t notify
                SKPaymentQueue.DefaultQueue.FinishTransaction(transaction); 
            }
        }
  
        // Handles custom observer
        private class MySKPaymentObserver : SKPaymentTransactionObserver
        {
            private InAppPurchaseManager theManager;
            public MySKPaymentObserver(InAppPurchaseManager manager)
            {
                theManager = manager;
            }
  
            public override void PaymentQueueRestoreCompletedTransactionsFinished (SKPaymentQueue queue)
            {
                foreach(SKPaymentTransaction transaction in queue.Transactions)
                {
  
                    #if DEBUG
                    Console.WriteLine("Restoring Transaction " + transaction.Payment.ProductIdentifier);
                    #endif
                    theManager.restoreTransaction(transaction);
                }
            }
            //
            // called when the transaction status is updated
            //
            public override void UpdatedTransactions (SKPaymentQueue queue, SKPaymentTransaction[] transactions)
            {
                foreach (SKPaymentTransaction transaction in transactions)
                {
#if DEBUG
                Console.WriteLine(transaction.Payment.ProductIdentifier + " " + transaction.TransactionState);
#endif
                    switch (transaction.TransactionState)
                    {
                        case SKPaymentTransactionState.Purchased:
                           theManager.completeTransaction(transaction);
                            break;
                        case SKPaymentTransactionState.Failed:
                           theManager.failedTransaction(transaction);
                            break;
                        case SKPaymentTransactionState.Restored:
                            theManager.restoreTransaction(transaction);
                            break;
                        default:
                            break;
                    }
                }
  
            }
  
        }   
    }
  
    public static class SKProductExtender
    {
        public static string LocalizedPrice( this SKProduct product)
        {
#if DEBUG
            Console.WriteLine("product.PriceLocale.LocaleIdentifier="+product.PriceLocale.LocaleIdentifier);
#endif 
        // returns en_AU@currency=AUD for me
            string localeIdString = product.PriceLocale.LocaleIdentifier;
            string locale = localeIdString; // default
            string currency = "USD";
            if (localeIdString.IndexOf('@') > 0)
            {
                locale = localeIdString.Substring(0, localeIdString.IndexOf('@'));
                currency = localeIdString.Substring(localeIdString.IndexOf('=')+1,3);
            }
            Console.WriteLine("locale " + locale);
            Console.WriteLine("currency " + currency);
  
            Thread.CurrentThread.CurrentCulture=new System.Globalization.CultureInfo(locale.Replace("_","-"));
            return ((double)product.Price.FloatValue).ToString("C2");           
        }
    }

Posted: 7/30/2012 9:16:34 AM