From 7e1eef56fd984baa2e18ca3278ae6213243e70fa Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Mon, 9 Oct 2017 12:37:35 -0700 Subject: [PATCH] Find original transactions that are pending/purchased Fixes #51 --- .../InAppBillingImplementation.cs | 765 +++++++++--------- 1 file changed, 401 insertions(+), 364 deletions(-) diff --git a/src/Plugin.InAppBilling.iOS/InAppBillingImplementation.cs b/src/Plugin.InAppBilling.iOS/InAppBillingImplementation.cs index 39a7333..6f7caab 100644 --- a/src/Plugin.InAppBilling.iOS/InAppBillingImplementation.cs +++ b/src/Plugin.InAppBilling.iOS/InAppBillingImplementation.cs @@ -9,232 +9,269 @@ namespace Plugin.InAppBilling { - /// - /// Implementation for InAppBilling - /// - [Preserve(AllMembers = true)] - public class InAppBillingImplementation : BaseInAppBilling - { - /// - /// Default constructor for In App Billing on iOS - /// - public InAppBillingImplementation() - { - paymentObserver = new PaymentObserver(); - SKPaymentQueue.DefaultQueue.AddTransactionObserver(paymentObserver); - } - - /// - /// Gets or sets if in testing mode. Only for UWP - /// - public override bool InTestingMode { get; set; } - - /// - /// Connect to billing service - /// - /// If Success - public override Task ConnectAsync() => Task.FromResult(true); - - /// - /// Disconnect from the billing service - /// - /// Task to disconnect - public override Task DisconnectAsync() => Task.CompletedTask; - - /// - /// Get product information of a specific product - /// - /// Sku or Id of the product(s) - /// Type of product offering - /// - public async override Task> GetProductInfoAsync(ItemType itemType, params string[] productIds) - { - var products = await GetProductAsync(productIds); - - return products.Select(p => new InAppBillingProduct - { - LocalizedPrice = p.LocalizedPrice(), - MicrosPrice = (long)(p.Price.DoubleValue * 1000000d), - Name = p.LocalizedTitle, - ProductId = p.ProductIdentifier, - Description = p.LocalizedDescription, - CurrencyCode = p.PriceLocale?.CurrencyCode ?? string.Empty - }); - } - - Task> GetProductAsync(string[] productId) - { - var productIdentifiers = NSSet.MakeNSObjectSet(productId.Select(i => new NSString(i)).ToArray()); - - var productRequestDelegate = new ProductRequestDelegate(); - - //set up product request for in-app purchase - var productsRequest = new SKProductsRequest(productIdentifiers) - { - Delegate = productRequestDelegate // SKProductsRequestDelegate.ReceivedResponse - }; - productsRequest.Start(); - - return productRequestDelegate.WaitForResponse(); - } - - - /// - /// Get all current purhcase for a specifiy product type. - /// - /// Type of product - /// Interface to verify purchase - /// The current purchases - public async override Task> GetPurchasesAsync(ItemType itemType, IInAppBillingVerifyPurchase verifyPurchase = null) - { - var purchases = await RestoreAsync(); + /// + /// Implementation for InAppBilling + /// + [Preserve(AllMembers = true)] + public class InAppBillingImplementation : BaseInAppBilling + { + /// + /// Default constructor for In App Billing on iOS + /// + public InAppBillingImplementation() + { + paymentObserver = new PaymentObserver(); + SKPaymentQueue.DefaultQueue.AddTransactionObserver(paymentObserver); + } + + /// + /// Gets or sets if in testing mode. Only for UWP + /// + public override bool InTestingMode { get; set; } + + /// + /// Connect to billing service + /// + /// If Success + public override Task ConnectAsync() => Task.FromResult(true); + + /// + /// Disconnect from the billing service + /// + /// Task to disconnect + public override Task DisconnectAsync() => Task.CompletedTask; + + /// + /// Get product information of a specific product + /// + /// Sku or Id of the product(s) + /// Type of product offering + /// + public async override Task> GetProductInfoAsync(ItemType itemType, params string[] productIds) + { + var products = await GetProductAsync(productIds); + + return products.Select(p => new InAppBillingProduct + { + LocalizedPrice = p.LocalizedPrice(), + MicrosPrice = (long)(p.Price.DoubleValue * 1000000d), + Name = p.LocalizedTitle, + ProductId = p.ProductIdentifier, + Description = p.LocalizedDescription, + CurrencyCode = p.PriceLocale?.CurrencyCode ?? string.Empty + }); + } + + Task> GetProductAsync(string[] productId) + { + var productIdentifiers = NSSet.MakeNSObjectSet(productId.Select(i => new NSString(i)).ToArray()); + + var productRequestDelegate = new ProductRequestDelegate(); + + //set up product request for in-app purchase + var productsRequest = new SKProductsRequest(productIdentifiers) + { + Delegate = productRequestDelegate // SKProductsRequestDelegate.ReceivedResponse + }; + productsRequest.Start(); + + return productRequestDelegate.WaitForResponse(); + } + + + /// + /// Get all current purhcase for a specifiy product type. + /// + /// Type of product + /// Interface to verify purchase + /// The current purchases + public async override Task> GetPurchasesAsync(ItemType itemType, IInAppBillingVerifyPurchase verifyPurchase = null) + { + var purchases = await RestoreAsync(); if (purchases == null) return null; - var converted = purchases + var converted = purchases .Where(p => p != null) .Select(p2 => p2.ToIABPurchase()); - //try to validate purchases - var valid = await ValidateReceipt(verifyPurchase, string.Empty, string.Empty); + //try to validate purchases + var valid = await ValidateReceipt(verifyPurchase, string.Empty, string.Empty); - return valid ? converted : null; - } + return valid ? converted : null; + } - Task RestoreAsync() - { - var tcsTransaction = new TaskCompletionSource(); + Task RestoreAsync() + { + var tcsTransaction = new TaskCompletionSource(); - Action handler = null; - handler = new Action(transactions => - { + var allTransactions = new List(); - // Unsubscribe from future events - paymentObserver.TransactionsRestored -= handler; + Action handler = null; + handler = new Action(transactions => + { - if (transactions == null) - tcsTransaction.TrySetException(new InAppBillingPurchaseException(PurchaseError.RestoreFailed, "Restore Transactions Failed")); - else - tcsTransaction.TrySetResult(transactions); - }); + // Unsubscribe from future events + paymentObserver.TransactionsRestored -= handler; - paymentObserver.TransactionsRestored += handler; + if (transactions == null) + { + if (allTransactions.Count == 0) + tcsTransaction.TrySetException(new InAppBillingPurchaseException(PurchaseError.RestoreFailed, "Restore Transactions Failed")); + else + tcsTransaction.TrySetResult(allTransactions.ToArray()); + } + else + { + allTransactions.AddRange(transactions); + tcsTransaction.TrySetResult(allTransactions.ToArray()); + } + }); - // Start receiving restored transactions - SKPaymentQueue.DefaultQueue.RestoreCompletedTransactions(); + paymentObserver.TransactionsRestored += handler; - return tcsTransaction.Task; - } + foreach (var trans in SKPaymentQueue.DefaultQueue.Transactions) + { + var original = FindOriginalTransaction(trans); + if (original == null) + continue; + allTransactions.Add(original); + } + // Start receiving restored transactions + SKPaymentQueue.DefaultQueue.RestoreCompletedTransactions(); + return tcsTransaction.Task; + } - /// - /// Purchase a specific product or subscription - /// - /// Sku or ID of product - /// Type of product being requested - /// Developer specific payload - /// Interface to verify purchase - /// - public async override Task PurchaseAsync(string productId, ItemType itemType, string payload, IInAppBillingVerifyPurchase verifyPurchase = null) - { - var p = await PurchaseAsync(productId); - var reference = new DateTime(2001, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); - - var purchase = new InAppBillingPurchase - { - TransactionDateUtc = reference.AddSeconds(p.TransactionDate.SecondsSinceReferenceDate), - Id = p.TransactionIdentifier, - ProductId = p.Payment?.ProductIdentifier ?? string.Empty, - State = p.GetPurchaseState() - }; + static SKPaymentTransaction FindOriginalTransaction(SKPaymentTransaction transaction) + { + if (transaction == null) + return null; - if (verifyPurchase == null) - return purchase; + if (transaction.TransactionState == SKPaymentTransactionState.Purchased || + transaction.TransactionState == SKPaymentTransactionState.Purchasing) + return transaction; - var validated = await ValidateReceipt(verifyPurchase, purchase.ProductId, purchase.Id); + if (transaction.OriginalTransaction != null) + return FindOriginalTransaction(transaction.OriginalTransaction); - return validated ? purchase : null; - } + return transaction; - Task ValidateReceipt(IInAppBillingVerifyPurchase verifyPurchase, string productId, string transactionId) - { - if (verifyPurchase == null) - return Task.FromResult(true); + } - // Get the receipt data for (server-side) validation. - // See: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573 - var receiptUrl = NSData.FromUrl(NSBundle.MainBundle.AppStoreReceiptUrl); - var receipt = receiptUrl.GetBase64EncodedString(NSDataBase64EncodingOptions.None); - return verifyPurchase.VerifyPurchase(receipt, string.Empty, productId, transactionId); - } - Task PurchaseAsync(string productId) - { - var tcsTransaction = new TaskCompletionSource(); - Action handler = null; - handler = new Action((tran, success) => - { + /// + /// Purchase a specific product or subscription + /// + /// Sku or ID of product + /// Type of product being requested + /// Developer specific payload + /// Interface to verify purchase + /// + public async override Task PurchaseAsync(string productId, ItemType itemType, string payload, IInAppBillingVerifyPurchase verifyPurchase = null) + { + var p = await PurchaseAsync(productId); + + var reference = new DateTime(2001, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + + var purchase = new InAppBillingPurchase + { + TransactionDateUtc = reference.AddSeconds(p.TransactionDate.SecondsSinceReferenceDate), + Id = p.TransactionIdentifier, + ProductId = p.Payment?.ProductIdentifier ?? string.Empty, + State = p.GetPurchaseState() + }; + + if (verifyPurchase == null) + return purchase; + + var validated = await ValidateReceipt(verifyPurchase, purchase.ProductId, purchase.Id); + + return validated ? purchase : null; + } + + Task ValidateReceipt(IInAppBillingVerifyPurchase verifyPurchase, string productId, string transactionId) + { + if (verifyPurchase == null) + return Task.FromResult(true); + + // Get the receipt data for (server-side) validation. + // See: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573 + var receiptUrl = NSData.FromUrl(NSBundle.MainBundle.AppStoreReceiptUrl); + var receipt = receiptUrl.GetBase64EncodedString(NSDataBase64EncodingOptions.None); + + return verifyPurchase.VerifyPurchase(receipt, string.Empty, productId, transactionId); + } + + + Task PurchaseAsync(string productId) + { + var tcsTransaction = new TaskCompletionSource(); + + Action handler = null; + handler = new Action((tran, success) => + { if (tran?.Payment == null) return; - // Only handle results from this request - if (productId != tran.Payment.ProductIdentifier) - return; - - // Unsubscribe from future events - paymentObserver.TransactionCompleted -= handler; - - if (success) - { - tcsTransaction.TrySetResult(tran); - return; - } - - var errorCode = tran?.Error?.Code ?? -1; - var description = tran?.Error?.LocalizedDescription ?? string.Empty; - var error = PurchaseError.GeneralError; - switch (errorCode) - { - case (int)SKError.PaymentCancelled: - error = PurchaseError.UserCancelled; - break; - case (int)SKError.PaymentInvalid: - error = PurchaseError.PaymentInvalid; - break; - case (int)SKError.PaymentNotAllowed: - error = PurchaseError.PaymentNotAllowed; - break; - case (int)SKError.ProductNotAvailable: - error = PurchaseError.ItemUnavailable; - break; - case (int)SKError.Unknown: - error = PurchaseError.GeneralError; - break; - case (int)SKError.ClientInvalid: - error = PurchaseError.BillingUnavailable; - break; - } - - tcsTransaction.TrySetException(new InAppBillingPurchaseException(error, description)); - - }); - - paymentObserver.TransactionCompleted += handler; - - var payment = SKPayment.CreateFrom(productId); - SKPaymentQueue.DefaultQueue.AddPayment(payment); - - return tcsTransaction.Task; - } + // Only handle results from this request + if (productId != tran.Payment.ProductIdentifier) + return; + + // Unsubscribe from future events + paymentObserver.TransactionCompleted -= handler; + + if (success) + { + tcsTransaction.TrySetResult(tran); + return; + } + + var errorCode = tran?.Error?.Code ?? -1; + var description = tran?.Error?.LocalizedDescription ?? string.Empty; + var error = PurchaseError.GeneralError; + switch (errorCode) + { + case (int)SKError.PaymentCancelled: + error = PurchaseError.UserCancelled; + break; + case (int)SKError.PaymentInvalid: + error = PurchaseError.PaymentInvalid; + break; + case (int)SKError.PaymentNotAllowed: + error = PurchaseError.PaymentNotAllowed; + break; + case (int)SKError.ProductNotAvailable: + error = PurchaseError.ItemUnavailable; + break; + case (int)SKError.Unknown: + error = PurchaseError.GeneralError; + break; + case (int)SKError.ClientInvalid: + error = PurchaseError.BillingUnavailable; + break; + } + + tcsTransaction.TrySetException(new InAppBillingPurchaseException(error, description)); + + }); + + paymentObserver.TransactionCompleted += handler; + + var payment = SKPayment.CreateFrom(productId); + SKPaymentQueue.DefaultQueue.AddPayment(payment); + + return tcsTransaction.Task; + } /// @@ -259,231 +296,231 @@ public override Task ConsumePurchaseAsync(string productId public override Task ConsumePurchaseAsync(string productId, ItemType itemType, string payload, IInAppBillingVerifyPurchase verifyPurchase = null) => null; - PaymentObserver paymentObserver; + PaymentObserver paymentObserver; - static DateTime NSDateToDateTimeUtc(NSDate date) - { - var reference = new DateTime(2001, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + static DateTime NSDateToDateTimeUtc(NSDate date) + { + var reference = new DateTime(2001, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); - return reference.AddSeconds(date?.SecondsSinceReferenceDate ?? 0); - } + return reference.AddSeconds(date?.SecondsSinceReferenceDate ?? 0); + } - private bool disposed = false; + private bool disposed = false; - /// - /// Dispose - /// - /// - public override void Dispose(bool disposing) - { - if (disposed) - { - base.Dispose(disposing); - return; - } + /// + /// Dispose + /// + /// + public override void Dispose(bool disposing) + { + if (disposed) + { + base.Dispose(disposing); + return; + } - disposed = true; + disposed = true; - if (!disposing) - { - base.Dispose(disposing); - return; - } + if (!disposing) + { + base.Dispose(disposing); + return; + } - if (paymentObserver != null) - { - SKPaymentQueue.DefaultQueue.RemoveTransactionObserver(paymentObserver); - paymentObserver.Dispose(); - paymentObserver = null; - } + if (paymentObserver != null) + { + SKPaymentQueue.DefaultQueue.RemoveTransactionObserver(paymentObserver); + paymentObserver.Dispose(); + paymentObserver = null; + } - base.Dispose(disposing); - } - } + base.Dispose(disposing); + } + } - [Preserve(AllMembers = true)] - class ProductRequestDelegate : NSObject, ISKProductsRequestDelegate, ISKRequestDelegate - { - TaskCompletionSource> tcsResponse = new TaskCompletionSource>(); + [Preserve(AllMembers = true)] + class ProductRequestDelegate : NSObject, ISKProductsRequestDelegate, ISKRequestDelegate + { + TaskCompletionSource> tcsResponse = new TaskCompletionSource>(); - public Task> WaitForResponse() => + public Task> WaitForResponse() => tcsResponse.Task; - - [Export("request:didFailWithError:")] - public void RequestFailed(SKRequest request, NSError error) => + + [Export("request:didFailWithError:")] + public void RequestFailed(SKRequest request, NSError error) => tcsResponse.TrySetException(new InAppBillingPurchaseException(PurchaseError.ProductRequestFailed, error.LocalizedDescription)); - - public void ReceivedResponse(SKProductsRequest request, SKProductsResponse response) - { - var product = response.Products; - if (product != null) - { - tcsResponse.TrySetResult(product); - return; - } + public void ReceivedResponse(SKProductsRequest request, SKProductsResponse response) + { + var product = response.Products; - tcsResponse.TrySetException(new InAppBillingPurchaseException(PurchaseError.InvalidProduct, "Invalid Product")); - } - } + if (product != null) + { + tcsResponse.TrySetResult(product); + return; + } + tcsResponse.TrySetException(new InAppBillingPurchaseException(PurchaseError.InvalidProduct, "Invalid Product")); + } + } - [Preserve(AllMembers = true)] - class PaymentObserver : SKPaymentTransactionObserver - { - public event Action TransactionCompleted; - public event Action TransactionsRestored; - List restoredTransactions = new List(); + [Preserve(AllMembers = true)] + class PaymentObserver : SKPaymentTransactionObserver + { + public event Action TransactionCompleted; + public event Action TransactionsRestored; - public override void UpdatedTransactions(SKPaymentQueue queue, SKPaymentTransaction[] transactions) - { - var rt = transactions.Where(pt => pt.TransactionState == SKPaymentTransactionState.Restored); + List restoredTransactions = new List(); - // Add our restored transactions to the list - // We might still get more from the initial request so we won't raise the event until - // RestoreCompletedTransactionsFinished is called - if (rt?.Any() ?? false) - restoredTransactions.AddRange(rt); + public override void UpdatedTransactions(SKPaymentQueue queue, SKPaymentTransaction[] transactions) + { + var rt = transactions.Where(pt => pt.TransactionState == SKPaymentTransactionState.Restored); - foreach (var transaction in transactions) - { + // Add our restored transactions to the list + // We might still get more from the initial request so we won't raise the event until + // RestoreCompletedTransactionsFinished is called + if (rt?.Any() ?? false) + restoredTransactions.AddRange(rt); + + foreach (var transaction in transactions) + { if (transaction?.TransactionState == null) break; - Debug.WriteLine($"Updated Transaction | {transaction.ToStatusString()}"); + Debug.WriteLine($"Updated Transaction | {transaction.ToStatusString()}"); - switch (transaction.TransactionState) - { + switch (transaction.TransactionState) + { case SKPaymentTransactionState.Restored: - case SKPaymentTransactionState.Purchased: - TransactionCompleted?.Invoke(transaction, true); - SKPaymentQueue.DefaultQueue.FinishTransaction(transaction); - break; - case SKPaymentTransactionState.Failed: - TransactionCompleted?.Invoke(transaction, false); - SKPaymentQueue.DefaultQueue.FinishTransaction(transaction); - break; - default: - break; - } - } - } - - public override void RestoreCompletedTransactionsFinished(SKPaymentQueue queue) - { + case SKPaymentTransactionState.Purchased: + TransactionCompleted?.Invoke(transaction, true); + SKPaymentQueue.DefaultQueue.FinishTransaction(transaction); + break; + case SKPaymentTransactionState.Failed: + TransactionCompleted?.Invoke(transaction, false); + SKPaymentQueue.DefaultQueue.FinishTransaction(transaction); + break; + default: + break; + } + } + } + + public override void RestoreCompletedTransactionsFinished(SKPaymentQueue queue) + { if (restoredTransactions == null) return; - // This is called after all restored transactions have hit UpdatedTransactions - // at this point we are done with the restore request so let's fire up the event - var allTransactions = restoredTransactions.ToArray(); - + // This is called after all restored transactions have hit UpdatedTransactions + // at this point we are done with the restore request so let's fire up the event + var allTransactions = restoredTransactions.ToArray(); + // Clear out the list of incoming restore transactions for future requests - restoredTransactions.Clear(); + restoredTransactions.Clear(); - TransactionsRestored?.Invoke(allTransactions); + TransactionsRestored?.Invoke(allTransactions); - foreach (var transaction in allTransactions) - SKPaymentQueue.DefaultQueue.FinishTransaction(transaction); - } + foreach (var transaction in allTransactions) + SKPaymentQueue.DefaultQueue.FinishTransaction(transaction); + } // Failure, just fire with null public override void RestoreCompletedTransactionsFailedWithError(SKPaymentQueue queue, NSError error) => - TransactionsRestored?.Invoke(null); - - } + TransactionsRestored?.Invoke(null); + } - [Preserve(AllMembers = true)] - static class SKTransactionExtensions - { - public static string ToStatusString(this SKPaymentTransaction transaction) => - transaction?.ToIABPurchase()?.ToString() ?? string.Empty; + [Preserve(AllMembers = true)] + static class SKTransactionExtensions + { + public static string ToStatusString(this SKPaymentTransaction transaction) => + transaction?.ToIABPurchase()?.ToString() ?? string.Empty; - public static InAppBillingPurchase ToIABPurchase(this SKPaymentTransaction transaction) - { - var p = transaction?.OriginalTransaction ?? transaction; - if (p == null) - return null; + public static InAppBillingPurchase ToIABPurchase(this SKPaymentTransaction transaction) + { + var p = transaction?.OriginalTransaction ?? transaction; + + if (p == null) + return null; - return new InAppBillingPurchase - { - TransactionDateUtc = NSDateToDateTimeUtc(p.TransactionDate), - Id = p.TransactionIdentifier, - ProductId = p.Payment?.ProductIdentifier ?? string.Empty, - State = p.GetPurchaseState() - }; - } + return new InAppBillingPurchase + { + TransactionDateUtc = NSDateToDateTimeUtc(p.TransactionDate), + Id = p.TransactionIdentifier, + ProductId = p.Payment?.ProductIdentifier ?? string.Empty, + State = p.GetPurchaseState() + }; + } - static DateTime NSDateToDateTimeUtc(NSDate date) - { - var reference = new DateTime(2001, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + static DateTime NSDateToDateTimeUtc(NSDate date) + { + var reference = new DateTime(2001, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); - return reference.AddSeconds(date?.SecondsSinceReferenceDate ?? 0); - } + return reference.AddSeconds(date?.SecondsSinceReferenceDate ?? 0); + } - public static PurchaseState GetPurchaseState(this SKPaymentTransaction transaction) - { + public static PurchaseState GetPurchaseState(this SKPaymentTransaction transaction) + { if (transaction?.TransactionState == null) return PurchaseState.Unknown; - switch (transaction.TransactionState) - { - case SKPaymentTransactionState.Restored: - return PurchaseState.Restored; - case SKPaymentTransactionState.Purchasing: - return PurchaseState.Purchasing; - case SKPaymentTransactionState.Purchased: - return PurchaseState.Purchased; - case SKPaymentTransactionState.Failed: - return PurchaseState.Failed; - case SKPaymentTransactionState.Deferred: - return PurchaseState.Deferred; - } - - return PurchaseState.Unknown; - } - - - } - - - [Preserve(AllMembers = true)] - static class SKProductExtension - { - /// - /// Use Apple's sample code for formatting a SKProduct price - /// https://developer.apple.com/library/ios/#DOCUMENTATION/StoreKit/Reference/SKProduct_Reference/Reference/Reference.html#//apple_ref/occ/instp/SKProduct/priceLocale - /// Objective-C version: - /// NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init]; - /// [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; - /// [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle]; - /// [numberFormatter setLocale:product.priceLocale]; - /// NSString *formattedString = [numberFormatter stringFromNumber:product.price]; - /// - public static string LocalizedPrice(this SKProduct product) - { - var formatter = new NSNumberFormatter() - { - FormatterBehavior = NSNumberFormatterBehavior.Version_10_4, - NumberStyle = NSNumberFormatterStyle.Currency, - Locale = product.PriceLocale - }; - var formattedString = formatter.StringFromNumber(product.Price); - Console.WriteLine(" ** formatter.StringFromNumber(" + product.Price + ") = " + formattedString + " for locale " + product.PriceLocale.LocaleIdentifier); - return formattedString; - } - } + switch (transaction.TransactionState) + { + case SKPaymentTransactionState.Restored: + return PurchaseState.Restored; + case SKPaymentTransactionState.Purchasing: + return PurchaseState.Purchasing; + case SKPaymentTransactionState.Purchased: + return PurchaseState.Purchased; + case SKPaymentTransactionState.Failed: + return PurchaseState.Failed; + case SKPaymentTransactionState.Deferred: + return PurchaseState.Deferred; + } + + return PurchaseState.Unknown; + } + + + } + + + [Preserve(AllMembers = true)] + static class SKProductExtension + { + /// + /// Use Apple's sample code for formatting a SKProduct price + /// https://developer.apple.com/library/ios/#DOCUMENTATION/StoreKit/Reference/SKProduct_Reference/Reference/Reference.html#//apple_ref/occ/instp/SKProduct/priceLocale + /// Objective-C version: + /// NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init]; + /// [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; + /// [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle]; + /// [numberFormatter setLocale:product.priceLocale]; + /// NSString *formattedString = [numberFormatter stringFromNumber:product.price]; + /// + public static string LocalizedPrice(this SKProduct product) + { + var formatter = new NSNumberFormatter() + { + FormatterBehavior = NSNumberFormatterBehavior.Version_10_4, + NumberStyle = NSNumberFormatterStyle.Currency, + Locale = product.PriceLocale + }; + var formattedString = formatter.StringFromNumber(product.Price); + Console.WriteLine(" ** formatter.StringFromNumber(" + product.Price + ") = " + formattedString + " for locale " + product.PriceLocale.LocaleIdentifier); + return formattedString; + } + } }