123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333 |
- using System;
- using System.Collections.Generic;
- using System.Diagnostics;
- using System.Globalization;
- using System.Linq;
- using System.Threading.Tasks;
- using Windows.ApplicationModel.Core;
- using Windows.ApplicationModel.Store;
- using Windows.System;
- using Windows.UI.Core;
-
- #pragma warning disable 4014
- namespace UnityEngine.Purchasing.Default
- {
- class WinRTStore : IWindowsIAP
- {
-
- private IWindowsIAPCallback callback;
- private ICurrentApp currentApp;
- private Dictionary<string, string> transactionIdToProductId = new Dictionary<string, string>();
-
- private int m_loginDelay;
-
- public WinRTStore(ICurrentApp currentApp)
- {
- this.currentApp = currentApp;
- }
-
-
- public void Initialize(IWindowsIAPCallback callback)
- {
- this.callback = callback;
- this.m_loginDelay = 30;
- }
-
-
- public void Initialize(IWindowsIAPCallback callback, int delayTime = 30)
- {
- this.callback = callback;
- this.m_loginDelay = delayTime;
- }
-
- public void SetLoginDelay(int delayTime)
- {
- this.m_loginDelay = delayTime;
- }
-
- public int LoginDelay()
- {
- return m_loginDelay;
- }
-
- public void RetrieveProducts(bool persistent)
- {
- RunOnUIThread(() =>
- {
- if (LoginDelay() > 0)
- {
- PollForProducts(persistent, 0, LoginDelay(), true, false);
- }
- else
- {
- PollForProducts(persistent, 0);
- }
- });
- }
-
- private async void PollForProducts(bool persistent, int delay, int retryCount = 10, bool tryLogin = false, bool loginAttempted = false, bool productsOnly = false)
- {
- await Task.Delay(delay);
- try
- {
- var result = await DoRetrieveProducts(productsOnly);
- callback.OnProductListReceived(result);
- }
- catch (Exception e)
- {
-
- LogError("PollForProducts() Exception (persistent = {0}, delay = {1}, retry = {2}), exception: {3}", persistent, delay, retryCount, e.Message);
-
- // NB: persistent here is used to distinguish when this is used by restoreTransactions() so we will
- // keep it intact and supplement for retries on initialization
- //
- if (persistent)
- {
- // This seems to indicate the App is not uploaded on
- // the dev portal, but is undocumented by Microsoft.
- if (e.Message.Contains("801900CC"))
- {
- LogError("Exception loading listing information: {0}", e.Message);
- callback.OnProductListError("AppNotKnown");
- // JDRjr: in the main store code this is not being checked correctly
- // and will result in repeated init attempts. Leaving it for now, but broken...
- }
- else if (e.Message.Contains("80070525"))
- {
- LogError("PollForProducts() User not signed in error HResult = 0x{0:X} (delay = {1}, retry = {2})", e.HResult, delay, retryCount);
- if ((delay == 0) && (productsOnly == false))
- {
- // First time failure give products only a try
- PollForProducts(true, 1000, retryCount, tryLogin, loginAttempted, true);
- }
- else
- {
- // Gonna call this an error
- LogError("Calling OnProductListError() delay = {0}, productsOnly = {1}", delay, productsOnly);
- callback.OnProductListError("801900CC because the C# code is broken");
- }
- }
- else
- {
- // other (no special handling) error codes
- // Wait up to 5 mins.
- // JDRjr: this seems like too long...
- delay = Math.Max(5000, delay);
- var newDelay = Math.Min(300000, delay * 2);
- PollForProducts(true, newDelay);
- }
- }
- else
- {
- // This is a restore attempt that has thrown an exception
- // We should allow for a login attempt here as well...
- if (tryLogin == true)
- {
- var uri = new Uri("ms-windows-store://signin");
- var loginResult = await global::Windows.System.Launcher.LaunchUriAsync(uri);
-
- PollForProducts(true, 1000, retryCount, false, true, false);
- }
- else
- {
- if (retryCount > 0)
- {
- if (loginAttempted)
- {
- // Will wait for retryCount seconds...
- PollForProducts(true, 1000, --retryCount, false, true);
- }
- else
- {
- // Wait up to 5 mins.
- delay = Math.Max(5000, delay);
- var newDelay = Math.Min(300000, delay * 2);
- PollForProducts(true, newDelay, --retryCount, false, false);
- }
- }
- else
- {
- callback.OnProductListError("801900CC because the C# code is broken");
- }
- }
- }
- } // end of catch()
- }
-
- private async Task<WinProductDescription[]> DoRetrieveProducts(bool productsOnly)
- {
- ListingInformation result = await currentApp.LoadListingInformationAsync();
-
- if (productsOnly == false)
- {
- // We need a comprehensive list of transaction IDs for owned items.
- // Microsoft make this difficult by failing to provide transaction IDs
- // on product licenses that are owned.
- // Therefore two data sets are joined; unfulfilled consumables (which have product IDs)
- // and transactions from the App receipt (Durables).
- var unfulfilledConsumables = await currentApp.GetUnfulfilledConsumablesAsync();
- var transactionMap = unfulfilledConsumables.ToDictionary(x => x.ProductId, x => x.TransactionId.ToString());
-
- // Add transaction IDs from our app receipt.
- string appReceipt = null;
- try
- {
- appReceipt = await currentApp.RequestAppReceiptAsync();
- }
- catch (Exception e)
- {
- LogError("Unable to retrieve app receipt:{0}", e.Message);
- }
-
- var receiptTransactions = XMLUtils.ParseProducts(appReceipt);
- foreach (var receiptTran in receiptTransactions)
- {
- transactionMap[receiptTran.productId] = receiptTran.transactionId;
- }
-
- // Create fake transaction Ids for any owned items that we can't find transaction IDs for.
- foreach (var license in currentApp.LicenseInformation.ProductLicenses)
- {
- if (!transactionMap.ContainsKey(license.Key))
- {
- transactionMap[license.Key] = license.Key.GetHashCode().ToString();
- }
- }
-
-
- // Construct our products including receipts and transaction ID where owned
- var productDescriptions = from listing in result.ProductListings.Values
- let priceDecimal = TryParsePrice(listing.FormattedPrice)
- let transactionId = transactionMap.ContainsKey(listing.ProductId) ? transactionMap[listing.ProductId] : null
- let receipt = transactionId == null ? null : appReceipt
- select new WinProductDescription(listing.ProductId,
- listing.FormattedPrice, listing.Name, string.Empty, RegionInfo.CurrentRegion.ISOCurrencySymbol,
- priceDecimal, receipt, transactionId);
-
- // Transaction IDs tracked for finalising transactions
- transactionIdToProductId = transactionMap.ToDictionary(x => x.Value, x => x.Key);
- return productDescriptions.ToArray();
- }
- else
- {
- var productDescriptions = from listing in result.ProductListings.Values
- let priceDecimal = TryParsePrice(listing.FormattedPrice)
- select new WinProductDescription(listing.ProductId,
- listing.FormattedPrice, listing.Name, string.Empty, RegionInfo.CurrentRegion.ISOCurrencySymbol,
- priceDecimal, null, null);
- return productDescriptions.ToArray();
- }
- }
-
- private decimal TryParsePrice(string formattedPrice)
- {
- decimal price = 0;
- decimal.TryParse(formattedPrice, NumberStyles.Currency, CultureInfo.CurrentCulture, out price);
- return price;
- }
-
- public void Purchase(string productId)
- {
- RunOnUIThread(async () =>
- {
- try
- {
- var result = await currentApp.RequestProductPurchaseAsync(productId);
- switch (result.Status)
- {
- case ProductPurchaseStatus.Succeeded:
- onPurchaseSucceeded(productId, result.ReceiptXml, result.TransactionId);
- break;
- case ProductPurchaseStatus.NotFulfilled:
- case ProductPurchaseStatus.AlreadyPurchased:
- case ProductPurchaseStatus.NotPurchased:
- callback.OnPurchaseFailed(productId, result.Status.ToString());
- break;
- }
- }
- catch (Exception e)
- {
- callback.OnPurchaseFailed(productId, e.Message);
- }
- });
- }
-
- private async Task FulfillConsumable(string productId, string transactionId)
- {
- try
- {
- var result = await currentApp.ReportConsumableFulfillmentAsync(productId, Guid.Parse(transactionId));
-
- if (FulfillmentResult.Succeeded == result)
- {
- lock (transactionIdToProductId)
- {
- transactionIdToProductId.Remove(transactionId);
- }
- }
- // It doesn't matter if the consumption succeeds or not.
- // If it doesn't, it will eventually be retried automatically.
- }
- catch (Exception e)
- {
- LogError("Exception consuming {0} : {1} (non-fatal)", productId, e.Message);
- }
- }
-
- private void LogError(string message, params object[] formatArgs)
- {
- callback.logError(string.Format("UnityIAPWin8:" + message, formatArgs));
- }
-
- private void onPurchaseSucceeded(string productId, string receipt, Guid transactionId)
- {
- var tranId = transactionId.ToString();
- // Make a note of which product this transaction pertains to.
- lock (transactionIdToProductId)
- {
- transactionIdToProductId[tranId] = productId;
- }
- callback.OnPurchaseSucceeded(productId, receipt, tranId);
- }
-
- public void FinaliseTransaction(string transactionId)
- {
- RunOnUIThread(() =>
- {
- // We occasionally supply null transaction IDs,
- // to the biller for owned non consumables.
- // The biller will try to finalise these, so we
- // ignore them.
- if (!string.IsNullOrEmpty(transactionId))
- {
- if (transactionIdToProductId.ContainsKey(transactionId))
- {
- FulfillConsumable(transactionIdToProductId[transactionId], transactionId);
- }
- else
- {
- callback.logError("Nothing to fulfill for transaction " + transactionId);
- }
- }
- });
- }
-
- private static void RunOnUIThread(Action a)
- {
- CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
- {
- a();
- });
- }
-
- /// <summary>
- /// Builds a dummy list of Products.
- /// </summary>
- /// <param name="products"> The list of product descriptions. </param>
- public void BuildDummyProducts(List<WinProductDescription> products)
- {
- currentApp.BuildMockProducts(products);
- }
- }
- }
- #pragma warning restore 4014
|