Ei kuvausta
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PurchasingManager.cs 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. #nullable enable
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Collections.ObjectModel;
  5. using System.Linq;
  6. using UnityEngine.Purchasing.Extension;
  7. namespace UnityEngine.Purchasing
  8. {
  9. /// <summary>
  10. /// The main controller for Applications using Unity Purchasing.
  11. /// </summary>
  12. internal class PurchasingManager : IStoreCallback, IStoreController
  13. {
  14. private readonly IStore m_Store;
  15. private IInternalStoreListener? m_Listener;
  16. private readonly ILogger m_Logger;
  17. private readonly TransactionLog m_TransactionLog;
  18. private readonly string m_StoreName;
  19. readonly bool m_logUnavailableProducts;
  20. private readonly IUnityServicesInitializationChecker m_UnityServicesInitializationChecker;
  21. private Action? m_AdditionalProductsCallback;
  22. private Action<InitializationFailureReason>? m_AdditionalProductsFailCallback;
  23. private Action<InitializationFailureReason, string?>? m_AdditionalProductsDetailedFailCallback;
  24. private readonly HashSet<string> purchasesProcessedInSession = new HashSet<string>();
  25. /// <summary>
  26. /// Stores may opt to disable Unity IAP's transaction log.
  27. /// </summary>
  28. public bool useTransactionLog { get; set; }
  29. internal PurchasingManager(TransactionLog tDb, ILogger logger, IStore store, string storeName, IUnityServicesInitializationChecker unityServicesInitializationChecker, bool logUnavailableProducts)
  30. {
  31. m_TransactionLog = tDb;
  32. m_Store = store;
  33. m_Logger = logger;
  34. m_StoreName = storeName;
  35. useTransactionLog = true;
  36. m_logUnavailableProducts = logUnavailableProducts;
  37. m_UnityServicesInitializationChecker = unityServicesInitializationChecker;
  38. }
  39. public void InitiatePurchase(Product product)
  40. {
  41. InitiatePurchase(product, string.Empty);
  42. }
  43. public void InitiatePurchase(string? productId)
  44. {
  45. InitiatePurchase(productId, string.Empty);
  46. }
  47. public void InitiatePurchase(Product? product, string developerPayload)
  48. {
  49. m_UnityServicesInitializationChecker.CheckAndLogWarning();
  50. if (null == product)
  51. {
  52. m_Logger.LogIAPWarning("Trying to purchase null Product");
  53. return;
  54. }
  55. if (!product.availableToPurchase)
  56. {
  57. m_Listener?.OnPurchaseFailed(product, new PurchaseFailureDescription(product.transactionID, PurchaseFailureReason.ProductUnavailable,
  58. "No products were found when fetching from the store"));
  59. return;
  60. }
  61. m_Store.Purchase(product.definition, developerPayload);
  62. }
  63. public void InitiatePurchase(string? purchasableId, string developerPayload)
  64. {
  65. var product = products.WithID(purchasableId);
  66. if (null == product)
  67. {
  68. m_Logger.LogFormat(LogType.Warning, "Unable to purchase unknown product with id: {0}", purchasableId);
  69. }
  70. InitiatePurchase(product, developerPayload);
  71. }
  72. /// <summary>
  73. /// Where an Application returned ProcessingResult.Pending they can manually
  74. /// finish the transaction by calling this method.
  75. /// </summary>
  76. public void ConfirmPendingPurchase(Product product)
  77. {
  78. if (null == product)
  79. {
  80. m_Logger.LogIAPError("Unable to confirm purchase with null Product");
  81. return;
  82. }
  83. if (string.IsNullOrEmpty(product.transactionID))
  84. {
  85. m_Logger.LogIAPError("Unable to confirm purchase; Product has missing or empty transactionID");
  86. return;
  87. }
  88. if (useTransactionLog)
  89. {
  90. m_TransactionLog.Record(product.transactionID);
  91. }
  92. m_Store.FinishTransaction(product.definition, product.transactionID);
  93. m_Listener?.SendTransactionEvent(product);
  94. }
  95. public ProductCollection products { get; private set; } = null!;
  96. /// <summary>
  97. /// Called by our IStore when a purchase succeeds.
  98. /// </summary>
  99. public void OnPurchaseSucceeded(string id, string? receipt, string transactionId)
  100. {
  101. var product = products.WithStoreSpecificID(id);
  102. if (null == product)
  103. {
  104. // If is possible for stores to tell us about products we have not yet
  105. // requested details of.
  106. // We should still tell the App in this scenario, albeit with incomplete information.
  107. var definition = new ProductDefinition(id, ProductType.NonConsumable);
  108. product = new Product(definition, new ProductMetadata());
  109. }
  110. UpdateProductReceiptAndTransactionID(product, receipt, transactionId);
  111. ProcessPurchaseIfNew(product);
  112. }
  113. void UpdateProductReceiptAndTransactionID(Product product, string? receipt, string transactionId)
  114. {
  115. if (product != null)
  116. {
  117. product.receipt = CreateUnifiedReceipt(receipt, transactionId);
  118. product.transactionID = transactionId;
  119. }
  120. }
  121. public void OnAllPurchasesRetrieved(List<Product> purchasedProducts)
  122. {
  123. if (products != null)
  124. {
  125. foreach (var product in products.all)
  126. {
  127. var purchasedProduct = purchasedProducts?.FirstOrDefault(firstPurchasedProduct => firstPurchasedProduct.definition.id == product.definition.id);
  128. if (purchasedProduct != null)
  129. {
  130. HandlePurchaseRetrieved(product, purchasedProduct);
  131. }
  132. else
  133. {
  134. ClearProductReceipt(product);
  135. }
  136. }
  137. }
  138. }
  139. // TODO IAP-2929: Add this to IStoreCallback in a major release
  140. internal static void OnEntitlementRevoked(Product revokedProduct)
  141. {
  142. ClearProductReceipt(revokedProduct);
  143. }
  144. void HandlePurchaseRetrieved(Product product, Product purchasedProduct)
  145. {
  146. UpdateProductReceiptAndTransactionID(product, purchasedProduct.receipt, purchasedProduct.transactionID);
  147. if (initialized && !WasPurchaseAlreadyProcessed(purchasedProduct.transactionID))
  148. {
  149. ProcessPurchaseIfNew(product);
  150. }
  151. }
  152. bool WasPurchaseAlreadyProcessed(string transactionId)
  153. {
  154. return purchasesProcessedInSession.Contains(transactionId);
  155. }
  156. static void ClearProductReceipt(Product product)
  157. {
  158. product.receipt = null;
  159. product.transactionID = null;
  160. }
  161. [Obsolete]
  162. public void OnSetupFailed(InitializationFailureReason reason)
  163. {
  164. OnSetupFailed(reason, null);
  165. }
  166. public void OnSetupFailed(InitializationFailureReason reason, string? message)
  167. {
  168. if (initialized)
  169. {
  170. m_AdditionalProductsFailCallback?.Invoke(reason);
  171. m_AdditionalProductsDetailedFailCallback?.Invoke(reason, message);
  172. }
  173. else
  174. {
  175. m_Listener?.OnInitializeFailed(reason, message);
  176. }
  177. }
  178. public void OnPurchaseFailed(PurchaseFailureDescription description)
  179. {
  180. if (description != null)
  181. {
  182. var product = products.WithStoreSpecificID(description.productId);
  183. if (null == product)
  184. {
  185. m_Logger.LogFormat(LogType.Error, "Failed to purchase unknown product {0}", "productId:" + description.productId + " reason:" + description.reason + " message:" + description.message);
  186. return;
  187. }
  188. m_Logger.LogFormat(LogType.Warning, "onPurchaseFailedEvent({0})", "productId:" + product.definition.id + " message:" + description.message);
  189. m_Listener?.OnPurchaseFailed(product, description);
  190. }
  191. }
  192. /// <summary>
  193. /// Called back by our IStore when it has fetched the latest product data.
  194. /// </summary>
  195. public void OnProductsRetrieved(List<ProductDescription> products)
  196. {
  197. var unknownProducts = new HashSet<Product>();
  198. foreach (var product in products)
  199. {
  200. var matchedProduct = this.products.WithStoreSpecificID(product.storeSpecificId);
  201. if (null == matchedProduct)
  202. {
  203. var definition = new ProductDefinition(product.storeSpecificId,
  204. product.storeSpecificId, product.type);
  205. matchedProduct = new Product(definition, product.metadata);
  206. unknownProducts.Add(matchedProduct);
  207. }
  208. matchedProduct.availableToPurchase = true;
  209. matchedProduct.metadata = product.metadata;
  210. matchedProduct.transactionID = product.transactionId;
  211. if (!string.IsNullOrEmpty(product.receipt))
  212. {
  213. matchedProduct.receipt = CreateUnifiedReceipt(product.receipt, product.transactionId);
  214. }
  215. }
  216. if (unknownProducts.Count > 0)
  217. {
  218. this.products.AddProducts(unknownProducts);
  219. }
  220. // Fire our initialisation events if this is a first poll.
  221. CheckForInitialization(products.Count);
  222. ProcessPurchaseOnStart();
  223. }
  224. string CreateUnifiedReceipt(string? rawReceipt, string transactionId)
  225. {
  226. return UnifiedReceiptFormatter.FormatUnifiedReceipt(rawReceipt, transactionId, m_StoreName);
  227. }
  228. void ProcessPurchaseOnStart()
  229. {
  230. foreach (var product in products.set)
  231. {
  232. if (!string.IsNullOrEmpty(product.receipt) && !string.IsNullOrEmpty(product.transactionID))
  233. {
  234. ProcessPurchaseIfNew(product);
  235. }
  236. }
  237. }
  238. [Obsolete]
  239. public void FetchAdditionalProducts(HashSet<ProductDefinition> additionalProducts, Action successCallback,
  240. Action<InitializationFailureReason> failCallback)
  241. {
  242. m_AdditionalProductsCallback = successCallback;
  243. m_AdditionalProductsFailCallback = failCallback;
  244. products.AddProducts(additionalProducts.Select(x => new Product(x, new ProductMetadata())));
  245. m_Store.RetrieveProducts(new ReadOnlyCollection<ProductDefinition>(additionalProducts.ToList()));
  246. }
  247. public void FetchAdditionalProducts(HashSet<ProductDefinition> additionalProducts, Action successCallback, Action<InitializationFailureReason, string?> failCallback)
  248. {
  249. m_AdditionalProductsCallback = successCallback;
  250. m_AdditionalProductsDetailedFailCallback = failCallback;
  251. products.AddProducts(additionalProducts.Select(x => new Product(x, new ProductMetadata())));
  252. m_Store.RetrieveProducts(new ReadOnlyCollection<ProductDefinition>(additionalProducts.ToList()));
  253. }
  254. /// <summary>
  255. /// Checks the product's transaction ID for uniqueness
  256. /// against the transaction log and calls the Application's
  257. /// ProcessPurchase method if so.
  258. /// </summary>
  259. private void ProcessPurchaseIfNew(Product product)
  260. {
  261. if (HasRecordedTransaction(product.transactionID))
  262. {
  263. m_Store.FinishTransaction(product.definition, product.transactionID);
  264. return;
  265. }
  266. purchasesProcessedInSession.Add(product.transactionID);
  267. var p = new PurchaseEventArgs(product);
  268. // Applications may elect to delay confirmations of purchases,
  269. // such as when persisting purchase state asynchronously.
  270. if (m_Listener?.ProcessPurchase(p) == PurchaseProcessingResult.Complete)
  271. {
  272. ConfirmPendingPurchase(product);
  273. }
  274. }
  275. bool HasRecordedTransaction(string transactionId)
  276. {
  277. return useTransactionLog && m_TransactionLog.HasRecordOf(transactionId);
  278. }
  279. private bool initialized;
  280. private void CheckForInitialization(int productCount)
  281. {
  282. if (!initialized)
  283. {
  284. initialized = true;
  285. if (productCount > 0 && HasAvailableProductsToPurchase())
  286. {
  287. m_Listener?.OnInitialized(this);
  288. }
  289. else
  290. {
  291. var message = productCount == 0 ?
  292. "No product returned from the store." :
  293. "Products returned from the store don't match.";
  294. m_Listener?.OnInitializeFailed(InitializationFailureReason.NoProductsAvailable,
  295. message);
  296. }
  297. }
  298. else
  299. {
  300. m_AdditionalProductsCallback?.Invoke();
  301. }
  302. }
  303. bool HasAvailableProductsToPurchase()
  304. {
  305. var available = false;
  306. foreach (var product in products.set)
  307. {
  308. if (product.availableToPurchase)
  309. {
  310. available = true;
  311. }
  312. else if (m_logUnavailableProducts)
  313. {
  314. m_Logger.LogFormat(LogType.Warning, "Unavailable product {0}-{1}", product.definition.id, product.definition.storeSpecificId);
  315. }
  316. }
  317. return available;
  318. }
  319. public void Initialize(IInternalStoreListener listener, HashSet<ProductDefinition> products)
  320. {
  321. m_Listener = listener;
  322. m_Store.Initialize(this);
  323. var prods = products.Select(x => new Product(x, new ProductMetadata())).ToArray();
  324. this.products = new ProductCollection(prods);
  325. var productCollection = new ReadOnlyCollection<ProductDefinition>(products.ToList());
  326. // Start the initialisation process by fetching product metadata.
  327. m_Store.RetrieveProducts(productCollection);
  328. }
  329. }
  330. }