lib_unity_purchase/Runtime/Purchasing/PurchasingManager.cs

380 lines
14 KiB
C#

#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;
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)
{
m_TransactionLog = tDb;
m_Store = store;
m_Logger = logger;
m_StoreName = storeName;
useTransactionLog = true;
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();
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()
{
if (!initialized)
{
initialized = true;
var hasAvailableProductsToPurchase = HasAvailableProductsToPurchase();
if (hasAvailableProductsToPurchase)
{
m_Listener?.OnInitialized(this);
}
else
{
m_Listener?.OnInitializeFailed(InitializationFailureReason.NoProductsAvailable);
}
}
else
{
m_AdditionalProductsCallback?.Invoke();
}
}
bool HasAvailableProductsToPurchase(bool shouldLogUnavailableProducts = true)
{
var available = false;
foreach (var product in products.set)
{
if (product.availableToPurchase)
{
available = true;
}
else if (shouldLogUnavailableProducts)
{
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);
}
}
}