Created
May 12, 2014 13:10
-
-
Save jonathanpeppers/3689857287b50a9053c0 to your computer and use it in GitHub Desktop.
Async IAPs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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