Skip to content

Instantly share code, notes, and snippets.

@jonathanpeppers
Created May 12, 2014 13:10
Show Gist options
  • Save jonathanpeppers/3689857287b50a9053c0 to your computer and use it in GitHub Desktop.
Save jonathanpeppers/3689857287b50a9053c0 to your computer and use it in GitHub Desktop.
Async IAPs
using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using MonoTouch.Foundation;
using MonoTouch.UIKit;
using MonoTouch.StoreKit;
namespace InAppPurchases
{
/// <summary>
/// Simple class describing an IAP
/// </summary>
public class Purchase
{
public string Id { get; set; }
public string Price { get; set; }
public string Title { get; set; }
public string Description { get; set; }
}
public class PurchaseService
{
//Copying Apple's generic error when something goes wrong
private const string DefaultError = "Could not load iTunes store.";
private readonly Dictionary<string, SKProduct> _products = new Dictionary<string, SKProduct>();
private ObserverDelegate _observer;
private SKProductsRequest _request = null;
private RequestDelegate _requestDelegate = null;
private TaskCompletionSource<Dictionary<string, Purchase>> _getPurchasesSource;
private TaskCompletionSource<bool> _purchaseSource;
private TaskCompletionSource<string[]> _restoreSource;
public async Task Purchase(string id)
{
if (_observer == null)
{
_observer = new ObserverDelegate(this);
SKPaymentQueue.DefaultQueue.AddTransactionObserver(_observer);
}
_purchaseSource = new TaskCompletionSource<bool>();
if (!SKPaymentQueue.CanMakePayments)
{
_purchaseSource.TrySetException(new Exception("Payments are restricted."));
}
else
{
SKProduct product;
if (!_products.TryGetValue(id, out product))
{
//Try to download the product
await GetPurchases(id);
if (!_products.TryGetValue(id, out product))
{
_purchaseSource.TrySetException(new Exception("Product not found!"));
}
}
SKPaymentQueue.DefaultQueue.AddPayment(SKPayment.PaymentWithProduct(product));
}
await _purchaseSource.Task;
}
public Task<Dictionary<string, Purchase>> GetPurchases(params string[] ids)
{
_getPurchasesSource = new TaskCompletionSource<Dictionary<string, Purchase>>();
_request = new SKProductsRequest(new NSSet(ids));
if (_requestDelegate == null)
{
_requestDelegate = new RequestDelegate(this);
}
_request.Delegate = _requestDelegate;
_request.Start();
return _getPurchasesSource.Task;
}
public Task<string[]> RestorePurchases()
{
if (_observer == null)
{
_observer = new ObserverDelegate(this);
SKPaymentQueue.DefaultQueue.AddTransactionObserver(_observer);
}
_restoreSource = new TaskCompletionSource<string[]>();
SKPaymentQueue.DefaultQueue.RestoreCompletedTransactions();
return _restoreSource.Task;
}
private void CompletePurchaseSuccessfully(SKPaymentTransaction transaction)
{
if (_purchaseSource != null)
{
_purchaseSource.TrySetResult(true);
}
}
private void CompletePurchaseWithError(Exception exc)
{
if (_purchaseSource != null)
{
_purchaseSource.TrySetException(exc);
}
}
private void CompleteRestoreSuccessfully(string[] purchaseIds)
{
if (_restoreSource != null)
{
_restoreSource.TrySetResult(purchaseIds);
}
}
private void CompleteRestoreWithError(Exception exc)
{
if (_restoreSource != null)
{
_restoreSource.TrySetException(exc);
}
}
private void CompletePurchasesRequestSuccessfully(SKProduct[] products)
{
var dictionary = new Dictionary<string, Purchase>();
using (var formatter = new NSNumberFormatter())
{
formatter.FormatterBehavior = NSNumberFormatterBehavior.Version_10_4;
formatter.NumberStyle = NSNumberFormatterStyle.Currency;
foreach (var product in products)
{
formatter.Locale = product.PriceLocale;
var purchase = new Purchase
{
Id = product.ProductIdentifier,
Price = formatter.StringFromNumber(product.Price),
Title = product.LocalizedTitle,
Description = product.LocalizedDescription,
};
dictionary[purchase.Id] = purchase;
_products[product.ProductIdentifier] = product;
}
}
if (_getPurchasesSource != null)
{
_getPurchasesSource.TrySetResult(dictionary);
}
if (_request != null)
{
_request.Delegate = null;
_request.Dispose();
_request = null;
}
}
private void CompletePurchasesRequestWithError(Exception exc)
{
if (_getPurchasesSource != null)
{
_getPurchasesSource.TrySetException(exc);
}
if (_request != null)
{
_request.Delegate = null;
_request.Dispose();
_request = null;
}
}
/// <summary>
/// Delegate for retrieving purchase info
/// </summary>
private class RequestDelegate : SKProductsRequestDelegate
{
private readonly PurchaseService _purchaseService;
public RequestDelegate(PurchaseService purchaseService)
{
_purchaseService = purchaseService;
}
public override void ReceivedResponse(SKProductsRequest request, SKProductsResponse response)
{
if (response.Products == null || response.Products.Length == 0)
{
Console.WriteLine("Error in ReceivedResponse:" + DefaultError);
_purchaseService.CompletePurchasesRequestWithError(new Exception());
}
else
{
_purchaseService.CompletePurchasesRequestSuccessfully(response.Products);
}
}
public override void RequestFailed(SKRequest request, NSError error)
{
//This crap is null randomly in production, I wrote a strongly worded letter to Tim Cook
if (error == null)
{
Console.WriteLine("Error in RequestFailed: error is null");
_purchaseService.CompletePurchasesRequestWithError(new Exception(PurchaseService.DefaultError));
}
else
{
Console.WriteLine("Error in RequestFailed: " + error.LocalizedDescription);
_purchaseService.CompletePurchasesRequestWithError(new Exception(error.LocalizedDescription));
}
}
}
/// <summary>
/// Observer for the callbacks on actual transactions
/// </summary>
private class ObserverDelegate : SKPaymentTransactionObserver
{
private readonly PurchaseService _purchaseService;
private List<string> _purchases;
public ObserverDelegate(PurchaseService purchaseService)
{
_purchaseService = purchaseService;
}
public override void UpdatedTransactions(SKPaymentQueue queue, SKPaymentTransaction[] transactions)
{
foreach (SKPaymentTransaction transaction in transactions)
{
if (transaction.TransactionState != SKPaymentTransactionState.Purchasing)
{
SKPaymentQueue.DefaultQueue.FinishTransaction(transaction);
}
switch (transaction.TransactionState)
{
case SKPaymentTransactionState.Failed:
Console.WriteLine("SKPayment failed: " + transaction.Error.LocalizedDescription);
_purchaseService.CompletePurchaseWithError(new Exception(transaction.Error.LocalizedDescription));
break;
case SKPaymentTransactionState.Purchased:
Console.WriteLine("Successfully purchased: " + transaction.Payment.ProductIdentifier);
_purchaseService.CompletePurchaseSuccessfully(transaction);
break;
case SKPaymentTransactionState.Restored:
Console.WriteLine("Successfully restored: " + transaction.Payment.ProductIdentifier);
if (_purchases == null)
_purchases = new List<string>();
_purchases.Add(transaction.Payment.ProductIdentifier);
break;
default:
break;
}
}
}
public override void PaymentQueueRestoreCompletedTransactionsFinished(SKPaymentQueue queue)
{
_purchaseService.CompleteRestoreSuccessfully(_purchases == null ? null : _purchases.ToArray());
_purchases = null;
}
public override void RestoreCompletedTransactionsFailedWithError(SKPaymentQueue queue, NSError error)
{
//This crap is null randomly in production, I wrote a strongly worded letter to Tim Cook
if (error == null)
{
Console.WriteLine("RestoreCompletedTransactionsFailedWithError: error is null");
_purchaseService.CompleteRestoreWithError(new Exception(PurchaseService.DefaultError));
}
else
{
Console.WriteLine("RestoreCompletedTransactionsFailedWithError: " + error.LocalizedDescription);
_purchaseService.CompleteRestoreWithError(new Exception(error.LocalizedDescription));
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment