PurchasingManager.cs 12 KB

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