PurchasingManager.cs 12 KB

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