123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- #nullable enable
- using System;
- using System.Collections.Generic;
- using System.Collections.ObjectModel;
- using System.Linq;
- using UnityEngine.Purchasing.Extension;
-
- namespace UnityEngine.Purchasing
- {
- /// <summary>
- /// The main controller for Applications using Unity Purchasing.
- /// </summary>
- internal class PurchasingManager : IStoreCallback, IStoreController
- {
- private readonly IStore m_Store;
- private IInternalStoreListener? m_Listener;
- private readonly ILogger m_Logger;
- private readonly TransactionLog m_TransactionLog;
- private readonly string m_StoreName;
- readonly bool m_logUnavailableProducts;
- private readonly IUnityServicesInitializationChecker m_UnityServicesInitializationChecker;
- private Action? m_AdditionalProductsCallback;
- private Action<InitializationFailureReason>? m_AdditionalProductsFailCallback;
- private Action<InitializationFailureReason, string?>? m_AdditionalProductsDetailedFailCallback;
-
- private readonly HashSet<string> purchasesProcessedInSession = new HashSet<string>();
-
- /// <summary>
- /// Stores may opt to disable Unity IAP's transaction log.
- /// </summary>
- public bool useTransactionLog { get; set; }
-
- internal PurchasingManager(TransactionLog tDb, ILogger logger, IStore store, string storeName, IUnityServicesInitializationChecker unityServicesInitializationChecker, bool logUnavailableProducts)
- {
- m_TransactionLog = tDb;
- m_Store = store;
- m_Logger = logger;
- m_StoreName = storeName;
- useTransactionLog = true;
- m_logUnavailableProducts = logUnavailableProducts;
- m_UnityServicesInitializationChecker = unityServicesInitializationChecker;
- }
-
- public void InitiatePurchase(Product product)
- {
- InitiatePurchase(product, string.Empty);
- }
-
- public void InitiatePurchase(string? productId)
- {
- InitiatePurchase(productId, string.Empty);
- }
-
- public void InitiatePurchase(Product? product, string developerPayload)
- {
- m_UnityServicesInitializationChecker.CheckAndLogWarning();
-
- if (null == product)
- {
- m_Logger.LogIAPWarning("Trying to purchase null Product");
- return;
- }
-
- if (!product.availableToPurchase)
- {
- m_Listener?.OnPurchaseFailed(product, new PurchaseFailureDescription(product.transactionID, PurchaseFailureReason.ProductUnavailable,
- "No products were found when fetching from the store"));
- return;
- }
-
- m_Store.Purchase(product.definition, developerPayload);
- }
-
- public void InitiatePurchase(string? purchasableId, string developerPayload)
- {
- var product = products.WithID(purchasableId);
- if (null == product)
- {
- m_Logger.LogFormat(LogType.Warning, "Unable to purchase unknown product with id: {0}", purchasableId);
- }
-
- InitiatePurchase(product, developerPayload);
- }
-
- /// <summary>
- /// Where an Application returned ProcessingResult.Pending they can manually
- /// finish the transaction by calling this method.
- /// </summary>
- public void ConfirmPendingPurchase(Product product)
- {
- if (null == product)
- {
- m_Logger.LogIAPError("Unable to confirm purchase with null Product");
- return;
- }
-
- if (string.IsNullOrEmpty(product.transactionID))
- {
- m_Logger.LogIAPError("Unable to confirm purchase; Product has missing or empty transactionID");
- return;
- }
-
- if (useTransactionLog)
- {
- m_TransactionLog.Record(product.transactionID);
- }
-
- m_Store.FinishTransaction(product.definition, product.transactionID);
- m_Listener?.SendTransactionEvent(product);
- }
-
- public ProductCollection products { get; private set; } = null!;
-
- /// <summary>
- /// Called by our IStore when a purchase succeeds.
- /// </summary>
- public void OnPurchaseSucceeded(string id, string? receipt, string transactionId)
- {
- var product = products.WithStoreSpecificID(id);
- if (null == product)
- {
- // If is possible for stores to tell us about products we have not yet
- // requested details of.
- // We should still tell the App in this scenario, albeit with incomplete information.
- var definition = new ProductDefinition(id, ProductType.NonConsumable);
- product = new Product(definition, new ProductMetadata());
- }
-
- UpdateProductReceiptAndTransactionID(product, receipt, transactionId);
- ProcessPurchaseIfNew(product);
- }
-
- void UpdateProductReceiptAndTransactionID(Product product, string? receipt, string transactionId)
- {
- if (product != null)
- {
- product.receipt = CreateUnifiedReceipt(receipt, transactionId);
- product.transactionID = transactionId;
- }
- }
-
- public void OnAllPurchasesRetrieved(List<Product> purchasedProducts)
- {
- if (products != null)
- {
- foreach (var product in products.all)
- {
- var purchasedProduct = purchasedProducts?.FirstOrDefault(firstPurchasedProduct => firstPurchasedProduct.definition.id == product.definition.id);
- if (purchasedProduct != null)
- {
- HandlePurchaseRetrieved(product, purchasedProduct);
- }
- else
- {
- ClearProductReceipt(product);
- }
- }
- }
- }
-
- // TODO IAP-2929: Add this to IStoreCallback in a major release
- internal static void OnEntitlementRevoked(Product revokedProduct)
- {
- ClearProductReceipt(revokedProduct);
- }
-
- void HandlePurchaseRetrieved(Product product, Product purchasedProduct)
- {
- UpdateProductReceiptAndTransactionID(product, purchasedProduct.receipt, purchasedProduct.transactionID);
- if (initialized && !WasPurchaseAlreadyProcessed(purchasedProduct.transactionID))
- {
- ProcessPurchaseIfNew(product);
- }
- }
-
- bool WasPurchaseAlreadyProcessed(string transactionId)
- {
- return purchasesProcessedInSession.Contains(transactionId);
- }
-
- static void ClearProductReceipt(Product product)
- {
- product.receipt = null;
- product.transactionID = null;
- }
-
- [Obsolete]
- public void OnSetupFailed(InitializationFailureReason reason)
- {
- OnSetupFailed(reason, null);
- }
-
- public void OnSetupFailed(InitializationFailureReason reason, string? message)
- {
- if (initialized)
- {
- m_AdditionalProductsFailCallback?.Invoke(reason);
- m_AdditionalProductsDetailedFailCallback?.Invoke(reason, message);
- }
- else
- {
- m_Listener?.OnInitializeFailed(reason, message);
- }
- }
-
- public void OnPurchaseFailed(PurchaseFailureDescription description)
- {
- if (description != null)
- {
- var product = products.WithStoreSpecificID(description.productId);
- if (null == product)
- {
- m_Logger.LogFormat(LogType.Error, "Failed to purchase unknown product {0}", "productId:" + description.productId + " reason:" + description.reason + " message:" + description.message);
- return;
- }
-
- m_Logger.LogFormat(LogType.Warning, "onPurchaseFailedEvent({0})", "productId:" + product.definition.id + " message:" + description.message);
- m_Listener?.OnPurchaseFailed(product, description);
- }
- }
-
- /// <summary>
- /// Called back by our IStore when it has fetched the latest product data.
- /// </summary>
- public void OnProductsRetrieved(List<ProductDescription> products)
- {
- var unknownProducts = new HashSet<Product>();
- foreach (var product in products)
- {
- var matchedProduct = this.products.WithStoreSpecificID(product.storeSpecificId);
- if (null == matchedProduct)
- {
- var definition = new ProductDefinition(product.storeSpecificId,
- product.storeSpecificId, product.type);
- matchedProduct = new Product(definition, product.metadata);
- unknownProducts.Add(matchedProduct);
- }
-
- matchedProduct.availableToPurchase = true;
- matchedProduct.metadata = product.metadata;
- matchedProduct.transactionID = product.transactionId;
-
- if (!string.IsNullOrEmpty(product.receipt))
- {
- matchedProduct.receipt = CreateUnifiedReceipt(product.receipt, product.transactionId);
- }
- }
-
- if (unknownProducts.Count > 0)
- {
- this.products.AddProducts(unknownProducts);
- }
-
- // Fire our initialisation events if this is a first poll.
- CheckForInitialization(products.Count);
-
- ProcessPurchaseOnStart();
- }
-
- string CreateUnifiedReceipt(string? rawReceipt, string transactionId)
- {
- return UnifiedReceiptFormatter.FormatUnifiedReceipt(rawReceipt, transactionId, m_StoreName);
- }
-
- void ProcessPurchaseOnStart()
- {
- foreach (var product in products.set)
- {
- if (!string.IsNullOrEmpty(product.receipt) && !string.IsNullOrEmpty(product.transactionID))
- {
- ProcessPurchaseIfNew(product);
- }
- }
- }
-
- [Obsolete]
- public void FetchAdditionalProducts(HashSet<ProductDefinition> additionalProducts, Action successCallback,
- Action<InitializationFailureReason> failCallback)
- {
- m_AdditionalProductsCallback = successCallback;
- m_AdditionalProductsFailCallback = failCallback;
- products.AddProducts(additionalProducts.Select(x => new Product(x, new ProductMetadata())));
- m_Store.RetrieveProducts(new ReadOnlyCollection<ProductDefinition>(additionalProducts.ToList()));
- }
-
- public void FetchAdditionalProducts(HashSet<ProductDefinition> additionalProducts, Action successCallback, Action<InitializationFailureReason, string?> failCallback)
- {
- m_AdditionalProductsCallback = successCallback;
- m_AdditionalProductsDetailedFailCallback = failCallback;
- products.AddProducts(additionalProducts.Select(x => new Product(x, new ProductMetadata())));
- m_Store.RetrieveProducts(new ReadOnlyCollection<ProductDefinition>(additionalProducts.ToList()));
- }
-
- /// <summary>
- /// Checks the product's transaction ID for uniqueness
- /// against the transaction log and calls the Application's
- /// ProcessPurchase method if so.
- /// </summary>
- private void ProcessPurchaseIfNew(Product product)
- {
- if (HasRecordedTransaction(product.transactionID))
- {
- m_Store.FinishTransaction(product.definition, product.transactionID);
- return;
- }
-
- purchasesProcessedInSession.Add(product.transactionID);
-
- var p = new PurchaseEventArgs(product);
-
- // Applications may elect to delay confirmations of purchases,
- // such as when persisting purchase state asynchronously.
- if (m_Listener?.ProcessPurchase(p) == PurchaseProcessingResult.Complete)
- {
- ConfirmPendingPurchase(product);
- }
- }
-
- bool HasRecordedTransaction(string transactionId)
- {
- return useTransactionLog && m_TransactionLog.HasRecordOf(transactionId);
- }
-
- private bool initialized;
-
- private void CheckForInitialization(int productCount)
- {
- if (!initialized)
- {
- initialized = true;
- if (productCount > 0 && HasAvailableProductsToPurchase())
- {
- m_Listener?.OnInitialized(this);
- }
- else
- {
- var message = productCount == 0 ?
- "No product returned from the store." :
- "Products returned from the store don't match.";
- m_Listener?.OnInitializeFailed(InitializationFailureReason.NoProductsAvailable,
- message);
- }
- }
- else
- {
- m_AdditionalProductsCallback?.Invoke();
- }
- }
-
- bool HasAvailableProductsToPurchase()
- {
- var available = false;
- foreach (var product in products.set)
- {
- if (product.availableToPurchase)
- {
- available = true;
- }
- else if (m_logUnavailableProducts)
- {
- m_Logger.LogFormat(LogType.Warning, "Unavailable product {0}-{1}", product.definition.id, product.definition.storeSpecificId);
- }
- }
-
- return available;
- }
-
- public void Initialize(IInternalStoreListener listener, HashSet<ProductDefinition> products)
- {
- m_Listener = listener;
- m_Store.Initialize(this);
-
- var prods = products.Select(x => new Product(x, new ProductMetadata())).ToArray();
- this.products = new ProductCollection(prods);
-
- var productCollection = new ReadOnlyCollection<ProductDefinition>(products.ToList());
-
- // Start the initialisation process by fetching product metadata.
- m_Store.RetrieveProducts(productCollection);
- }
- }
- }
|