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.
 
 
 

933 lines
40 KiB

  1. #if UNITY_PURCHASING
  2. #if UNITY_ANDROID || UNITY_IPHONE || UNITY_STANDALONE_OSX || UNITY_TVOS
  3. // You must obfuscate your secrets using Window > Unity IAP > Receipt Validation Obfuscator
  4. // before receipt validation will compile in this sample.
  5. //#define RECEIPT_VALIDATION
  6. #endif
  7. //#define DELAY_CONFIRMATION // Returns PurchaseProcessingResult.Pending from ProcessPurchase, then calls ConfirmPendingPurchase after a delay
  8. //#define USE_PAYOUTS // Enables use of PayoutDefinitions to specify what the player should receive when a product is purchased
  9. //#define INTERCEPT_PROMOTIONAL_PURCHASES // Enables intercepting promotional purchases that come directly from the Apple App Store
  10. //#define SUBSCRIPTION_MANAGER //Enables subscription product manager for AppleStore and GooglePlay store
  11. using System;
  12. using System.Collections;
  13. using System.Collections.Generic;
  14. using UnityEngine;
  15. using UnityEngine.UI;
  16. using UnityEngine.Purchasing;
  17. using UnityEngine.Store; // UnityChannel
  18. #if RECEIPT_VALIDATION
  19. using UnityEngine.Purchasing.Security;
  20. #endif
  21. /// <summary>
  22. /// An example of Unity IAP functionality.
  23. /// To use with your account, configure the product ids (AddProduct).
  24. /// </summary>
  25. [AddComponentMenu("Unity IAP/Demo")]
  26. public class IAPDemo : MonoBehaviour, IStoreListener
  27. {
  28. // Unity IAP objects
  29. private IStoreController m_Controller;
  30. private IAppleExtensions m_AppleExtensions;
  31. private IMoolahExtension m_MoolahExtensions;
  32. private ISamsungAppsExtensions m_SamsungExtensions;
  33. private IMicrosoftExtensions m_MicrosoftExtensions;
  34. private IUnityChannelExtensions m_UnityChannelExtensions;
  35. private ITransactionHistoryExtensions m_TransactionHistoryExtensions;
  36. private IGooglePlayStoreExtensions m_GooglePlayStoreExtensions;
  37. #pragma warning disable 0414
  38. private bool m_IsGooglePlayStoreSelected;
  39. #pragma warning restore 0414
  40. private bool m_IsSamsungAppsStoreSelected;
  41. private bool m_IsCloudMoolahStoreSelected;
  42. private bool m_IsUnityChannelSelected;
  43. private string m_LastTransactionID;
  44. private bool m_IsLoggedIn;
  45. private UnityChannelLoginHandler unityChannelLoginHandler; // Helper for interfacing with UnityChannel API
  46. private bool m_FetchReceiptPayloadOnPurchase = false;
  47. private bool m_PurchaseInProgress;
  48. private Dictionary<string, IAPDemoProductUI> m_ProductUIs = new Dictionary<string, IAPDemoProductUI>();
  49. public GameObject productUITemplate;
  50. public RectTransform contentRect;
  51. public Button restoreButton;
  52. public Button loginButton;
  53. public Button validateButton;
  54. public Text versionText;
  55. #if RECEIPT_VALIDATION
  56. private CrossPlatformValidator validator;
  57. #endif
  58. /// <summary>
  59. /// This will be called when Unity IAP has finished initialising.
  60. /// </summary>
  61. public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
  62. {
  63. m_Controller = controller;
  64. m_AppleExtensions = extensions.GetExtension<IAppleExtensions>();
  65. m_SamsungExtensions = extensions.GetExtension<ISamsungAppsExtensions>();
  66. m_MoolahExtensions = extensions.GetExtension<IMoolahExtension>();
  67. m_MicrosoftExtensions = extensions.GetExtension<IMicrosoftExtensions>();
  68. m_UnityChannelExtensions = extensions.GetExtension<IUnityChannelExtensions>();
  69. m_TransactionHistoryExtensions = extensions.GetExtension<ITransactionHistoryExtensions>();
  70. m_GooglePlayStoreExtensions = extensions.GetExtension<IGooglePlayStoreExtensions>();
  71. // Sample code for expose product sku details for google play store
  72. // Key is product Id (Sku), value is the skuDetails json string
  73. //Dictionary<string, string> google_play_store_product_SKUDetails_json = m_GooglePlayStoreExtensions.GetProductJSONDictionary();
  74. // Sample code for manually finish a transaction (consume a product on GooglePlay store)
  75. //m_GooglePlayStoreExtensions.FinishAdditionalTransaction(productId, transactionId);
  76. InitUI(controller.products.all);
  77. // On Apple platforms we need to handle deferred purchases caused by Apple's Ask to Buy feature.
  78. // On non-Apple platforms this will have no effect; OnDeferred will never be called.
  79. m_AppleExtensions.RegisterPurchaseDeferredListener(OnDeferred);
  80. #if SUBSCRIPTION_MANAGER
  81. Dictionary<string, string> introductory_info_dict = m_AppleExtensions.GetIntroductoryPriceDictionary();
  82. #endif
  83. // Sample code for expose product sku details for apple store
  84. //Dictionary<string, string> product_details = m_AppleExtensions.GetProductDetails();
  85. Debug.Log("Available items:");
  86. foreach (var item in controller.products.all)
  87. {
  88. if (item.availableToPurchase)
  89. {
  90. Debug.Log(string.Join(" - ",
  91. new[]
  92. {
  93. item.metadata.localizedTitle,
  94. item.metadata.localizedDescription,
  95. item.metadata.isoCurrencyCode,
  96. item.metadata.localizedPrice.ToString(),
  97. item.metadata.localizedPriceString,
  98. item.transactionID,
  99. item.receipt
  100. }));
  101. #if INTERCEPT_PROMOTIONAL_PURCHASES
  102. // Set all these products to be visible in the user's App Store according to Apple's Promotional IAP feature
  103. // https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StoreKitGuide/PromotingIn-AppPurchases/PromotingIn-AppPurchases.html
  104. m_AppleExtensions.SetStorePromotionVisibility(item, AppleStorePromotionVisibility.Show);
  105. #endif
  106. #if SUBSCRIPTION_MANAGER
  107. // this is the usage of SubscriptionManager class
  108. if (item.receipt != null) {
  109. if (item.definition.type == ProductType.Subscription) {
  110. if (checkIfProductIsAvailableForSubscriptionManager(item.receipt)) {
  111. string intro_json = (introductory_info_dict == null || !introductory_info_dict.ContainsKey(item.definition.storeSpecificId)) ? null : introductory_info_dict[item.definition.storeSpecificId];
  112. SubscriptionManager p = new SubscriptionManager(item, intro_json);
  113. SubscriptionInfo info = p.getSubscriptionInfo();
  114. Debug.Log("product id is: " + info.getProductId());
  115. Debug.Log("purchase date is: " + info.getPurchaseDate());
  116. Debug.Log("subscription next billing date is: " + info.getExpireDate());
  117. Debug.Log("is subscribed? " + info.isSubscribed().ToString());
  118. Debug.Log("is expired? " + info.isExpired().ToString());
  119. Debug.Log("is cancelled? " + info.isCancelled());
  120. Debug.Log("product is in free trial peroid? " + info.isFreeTrial());
  121. Debug.Log("product is auto renewing? " + info.isAutoRenewing());
  122. Debug.Log("subscription remaining valid time until next billing date is: " + info.getRemainingTime());
  123. Debug.Log("is this product in introductory price period? " + info.isIntroductoryPricePeriod());
  124. Debug.Log("the product introductory localized price is: " + info.getIntroductoryPrice());
  125. Debug.Log("the product introductory price period is: " + info.getIntroductoryPricePeriod());
  126. Debug.Log("the number of product introductory price period cycles is: " + info.getIntroductoryPricePeriodCycles());
  127. } else {
  128. Debug.Log("This product is not available for SubscriptionManager class, only products that are purchase by 1.19+ SDK can use this class.");
  129. }
  130. } else {
  131. Debug.Log("the product is not a subscription product");
  132. }
  133. } else {
  134. Debug.Log("the product should have a valid receipt");
  135. }
  136. #endif
  137. }
  138. }
  139. // Populate the product menu now that we have Products
  140. AddProductUIs(m_Controller.products.all);
  141. LogProductDefinitions();
  142. }
  143. #if SUBSCRIPTION_MANAGER
  144. private bool checkIfProductIsAvailableForSubscriptionManager(string receipt) {
  145. var receipt_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(receipt);
  146. if (!receipt_wrapper.ContainsKey("Store") || !receipt_wrapper.ContainsKey("Payload")) {
  147. Debug.Log("The product receipt does not contain enough information");
  148. return false;
  149. }
  150. var store = (string)receipt_wrapper ["Store"];
  151. var payload = (string)receipt_wrapper ["Payload"];
  152. if (payload != null ) {
  153. switch (store) {
  154. case GooglePlay.Name:
  155. {
  156. var payload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(payload);
  157. if (!payload_wrapper.ContainsKey("json")) {
  158. Debug.Log("The product receipt does not contain enough information, the 'json' field is missing");
  159. return false;
  160. }
  161. var original_json_payload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode((string)payload_wrapper["json"]);
  162. if (original_json_payload_wrapper == null || !original_json_payload_wrapper.ContainsKey("developerPayload")) {
  163. Debug.Log("The product receipt does not contain enough information, the 'developerPayload' field is missing");
  164. return false;
  165. }
  166. var developerPayloadJSON = (string)original_json_payload_wrapper["developerPayload"];
  167. var developerPayload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(developerPayloadJSON);
  168. if (developerPayload_wrapper == null || !developerPayload_wrapper.ContainsKey("is_free_trial") || !developerPayload_wrapper.ContainsKey("has_introductory_price_trial")) {
  169. Debug.Log("The product receipt does not contain enough information, the product is not purchased using 1.19 or later");
  170. return false;
  171. }
  172. return true;
  173. }
  174. case AppleAppStore.Name:
  175. case AmazonApps.Name:
  176. case MacAppStore.Name:
  177. {
  178. return true;
  179. }
  180. default:
  181. {
  182. return false;
  183. }
  184. }
  185. }
  186. return false;
  187. }
  188. #endif
  189. /// <summary>
  190. /// This will be called when a purchase completes.
  191. /// </summary>
  192. public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
  193. {
  194. Debug.Log("Purchase OK: " + e.purchasedProduct.definition.id);
  195. Debug.Log("Receipt: " + e.purchasedProduct.receipt);
  196. m_LastTransactionID = e.purchasedProduct.transactionID;
  197. m_PurchaseInProgress = false;
  198. // Decode the UnityChannelPurchaseReceipt, extracting the gameOrderId
  199. if (m_IsUnityChannelSelected)
  200. {
  201. var unifiedReceipt = JsonUtility.FromJson<UnifiedReceipt>(e.purchasedProduct.receipt);
  202. if (unifiedReceipt != null && !string.IsNullOrEmpty(unifiedReceipt.Payload))
  203. {
  204. var purchaseReceipt = JsonUtility.FromJson<UnityChannelPurchaseReceipt>(unifiedReceipt.Payload);
  205. Debug.LogFormat(
  206. "UnityChannel receipt: storeSpecificId = {0}, transactionId = {1}, orderQueryToken = {2}",
  207. purchaseReceipt.storeSpecificId, purchaseReceipt.transactionId, purchaseReceipt.orderQueryToken);
  208. }
  209. }
  210. #if RECEIPT_VALIDATION // Local validation is available for GooglePlay, Apple, and UnityChannel stores
  211. if (m_IsGooglePlayStoreSelected ||
  212. (m_IsUnityChannelSelected && m_FetchReceiptPayloadOnPurchase) ||
  213. Application.platform == RuntimePlatform.IPhonePlayer ||
  214. Application.platform == RuntimePlatform.OSXPlayer ||
  215. Application.platform == RuntimePlatform.tvOS) {
  216. try {
  217. var result = validator.Validate(e.purchasedProduct.receipt);
  218. Debug.Log("Receipt is valid. Contents:");
  219. foreach (IPurchaseReceipt productReceipt in result) {
  220. Debug.Log(productReceipt.productID);
  221. Debug.Log(productReceipt.purchaseDate);
  222. Debug.Log(productReceipt.transactionID);
  223. GooglePlayReceipt google = productReceipt as GooglePlayReceipt;
  224. if (null != google) {
  225. Debug.Log(google.purchaseState);
  226. Debug.Log(google.purchaseToken);
  227. }
  228. UnityChannelReceipt unityChannel = productReceipt as UnityChannelReceipt;
  229. if (null != unityChannel) {
  230. Debug.Log(unityChannel.productID);
  231. Debug.Log(unityChannel.purchaseDate);
  232. Debug.Log(unityChannel.transactionID);
  233. }
  234. AppleInAppPurchaseReceipt apple = productReceipt as AppleInAppPurchaseReceipt;
  235. if (null != apple) {
  236. Debug.Log(apple.originalTransactionIdentifier);
  237. Debug.Log(apple.subscriptionExpirationDate);
  238. Debug.Log(apple.cancellationDate);
  239. Debug.Log(apple.quantity);
  240. }
  241. // For improved security, consider comparing the signed
  242. // IPurchaseReceipt.productId, IPurchaseReceipt.transactionID, and other data
  243. // embedded in the signed receipt objects to the data which the game is using
  244. // to make this purchase.
  245. }
  246. } catch (IAPSecurityException ex) {
  247. Debug.Log("Invalid receipt, not unlocking content. " + ex);
  248. return PurchaseProcessingResult.Complete;
  249. }
  250. }
  251. #endif
  252. // Unlock content from purchases here.
  253. #if USE_PAYOUTS
  254. if (e.purchasedProduct.definition.payouts != null) {
  255. Debug.Log("Purchase complete, paying out based on defined payouts");
  256. foreach (var payout in e.purchasedProduct.definition.payouts) {
  257. Debug.Log(string.Format("Granting {0} {1} {2} {3}", payout.quantity, payout.typeString, payout.subtype, payout.data));
  258. }
  259. }
  260. #endif
  261. // Indicate if we have handled this purchase.
  262. // PurchaseProcessingResult.Complete: ProcessPurchase will not be called
  263. // with this product again, until next purchase.
  264. // PurchaseProcessingResult.Pending: ProcessPurchase will be called
  265. // again with this product at next app launch. Later, call
  266. // m_Controller.ConfirmPendingPurchase(Product) to complete handling
  267. // this purchase. Use to transactionally save purchases to a cloud
  268. // game service.
  269. #if DELAY_CONFIRMATION
  270. StartCoroutine(ConfirmPendingPurchaseAfterDelay(e.purchasedProduct));
  271. return PurchaseProcessingResult.Pending;
  272. #else
  273. UpdateProductUI(e.purchasedProduct);
  274. return PurchaseProcessingResult.Complete;
  275. #endif
  276. }
  277. #if DELAY_CONFIRMATION
  278. private HashSet<string> m_PendingProducts = new HashSet<string>();
  279. private IEnumerator ConfirmPendingPurchaseAfterDelay(Product p)
  280. {
  281. m_PendingProducts.Add(p.definition.id);
  282. Debug.Log("Delaying confirmation of " + p.definition.id + " for 5 seconds.");
  283. var end = Time.time + 5f;
  284. while (Time.time < end) {
  285. yield return null;
  286. var remaining = Mathf.CeilToInt (end - Time.time);
  287. UpdateProductPendingUI (p, remaining);
  288. }
  289. Debug.Log("Confirming purchase of " + p.definition.id);
  290. m_Controller.ConfirmPendingPurchase(p);
  291. m_PendingProducts.Remove(p.definition.id);
  292. UpdateProductUI (p);
  293. }
  294. #endif
  295. /// <summary>
  296. /// This will be called if an attempted purchase fails.
  297. /// </summary>
  298. public void OnPurchaseFailed(Product item, PurchaseFailureReason r)
  299. {
  300. Debug.Log("Purchase failed: " + item.definition.id);
  301. Debug.Log(r);
  302. // Detailed debugging information
  303. Debug.Log("Store specific error code: " + m_TransactionHistoryExtensions.GetLastStoreSpecificPurchaseErrorCode());
  304. if (m_TransactionHistoryExtensions.GetLastPurchaseFailureDescription() != null)
  305. {
  306. Debug.Log("Purchase failure description message: " +
  307. m_TransactionHistoryExtensions.GetLastPurchaseFailureDescription().message);
  308. }
  309. if (m_IsUnityChannelSelected)
  310. {
  311. var extra = m_UnityChannelExtensions.GetLastPurchaseError();
  312. var purchaseError = JsonUtility.FromJson<UnityChannelPurchaseError>(extra);
  313. if (purchaseError != null && purchaseError.purchaseInfo != null)
  314. {
  315. // Additional information about purchase failure.
  316. var purchaseInfo = purchaseError.purchaseInfo;
  317. Debug.LogFormat(
  318. "UnityChannel purchaseInfo: productCode = {0}, gameOrderId = {1}, orderQueryToken = {2}",
  319. purchaseInfo.productCode, purchaseInfo.gameOrderId, purchaseInfo.orderQueryToken);
  320. }
  321. // Determine if the user already owns this item and that it can be added to
  322. // their inventory, if not already present.
  323. #if UNITY_5_6_OR_NEWER
  324. if (r == PurchaseFailureReason.DuplicateTransaction)
  325. {
  326. // Unlock `item` in inventory if not already present.
  327. Debug.Log("Duplicate transaction detected, unlock this item");
  328. }
  329. #else // Building using Unity strictly less than 5.6; e.g 5.3-5.5.
  330. // In Unity 5.3 the enum PurchaseFailureReason.DuplicateTransaction
  331. // may not be available (is available in 5.6 ... specifically
  332. // 5.5.1p1+, 5.4.4p2+) and can be substituted with this call.
  333. if (r == PurchaseFailureReason.Unknown)
  334. {
  335. if (purchaseError != null && purchaseError.error != null &&
  336. purchaseError.error.Equals("DuplicateTransaction"))
  337. {
  338. // Unlock `item` in inventory if not already present.
  339. Debug.Log("Duplicate transaction detected, unlock this item");
  340. }
  341. }
  342. #endif
  343. }
  344. m_PurchaseInProgress = false;
  345. }
  346. public void OnInitializeFailed(InitializationFailureReason error)
  347. {
  348. Debug.Log("Billing failed to initialize!");
  349. switch (error)
  350. {
  351. case InitializationFailureReason.AppNotKnown:
  352. Debug.LogError("Is your App correctly uploaded on the relevant publisher console?");
  353. break;
  354. case InitializationFailureReason.PurchasingUnavailable:
  355. // Ask the user if billing is disabled in device settings.
  356. Debug.Log("Billing disabled!");
  357. break;
  358. case InitializationFailureReason.NoProductsAvailable:
  359. // Developer configuration error; check product metadata.
  360. Debug.Log("No products available for purchase!");
  361. break;
  362. }
  363. }
  364. [Serializable]
  365. public class UnityChannelPurchaseError
  366. {
  367. public string error;
  368. public UnityChannelPurchaseInfo purchaseInfo;
  369. }
  370. [Serializable]
  371. public class UnityChannelPurchaseInfo
  372. {
  373. public string productCode; // Corresponds to storeSpecificId
  374. public string gameOrderId; // Corresponds to transactionId
  375. public string orderQueryToken;
  376. }
  377. public void Awake()
  378. {
  379. var module = StandardPurchasingModule.Instance();
  380. // The FakeStore supports: no-ui (always succeeding), basic ui (purchase pass/fail), and
  381. // developer ui (initialization, purchase, failure code setting). These correspond to
  382. // the FakeStoreUIMode Enum values passed into StandardPurchasingModule.useFakeStoreUIMode.
  383. module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser;
  384. var builder = ConfigurationBuilder.Instance(module);
  385. // Set this to true to enable the Microsoft IAP simulator for local testing.
  386. builder.Configure<IMicrosoftConfiguration>().useMockBillingSystem = false;
  387. m_IsGooglePlayStoreSelected =
  388. Application.platform == RuntimePlatform.Android && module.appStore == AppStore.GooglePlay;
  389. // CloudMoolah Configuration setings
  390. // All games must set the configuration. the configuration need to apply on the CloudMoolah Portal.
  391. // CloudMoolah APP Key
  392. builder.Configure<IMoolahConfiguration>().appKey = "d93f4564c41d463ed3d3cd207594ee1b";
  393. // CloudMoolah Hash Key
  394. builder.Configure<IMoolahConfiguration>().hashKey = "cc";
  395. // This enables the CloudMoolah test mode for local testing.
  396. // You would remove this, or set to CloudMoolahMode.Production, before building your release package.
  397. builder.Configure<IMoolahConfiguration>().SetMode(CloudMoolahMode.AlwaysSucceed);
  398. // This records whether we are using Cloud Moolah IAP.
  399. // Cloud Moolah requires logging in to access your Digital Wallet, so:
  400. // A) IAPDemo (this) displays the Cloud Moolah GUI button for Cloud Moolah
  401. m_IsCloudMoolahStoreSelected =
  402. Application.platform == RuntimePlatform.Android && module.appStore == AppStore.CloudMoolah;
  403. // UnityChannel, provides access to Xiaomi MiPay.
  404. // Products are required to be set in the IAP Catalog window. The file "MiProductCatalog.prop"
  405. // is required to be generated into the project's
  406. // Assets/Plugins/Android/assets folder, based off the contents of the
  407. // IAP Catalog window, for MiPay.
  408. m_IsUnityChannelSelected =
  409. Application.platform == RuntimePlatform.Android && module.appStore == AppStore.XiaomiMiPay;
  410. // UnityChannel supports receipt validation through a backend fetch.
  411. builder.Configure<IUnityChannelConfiguration>().fetchReceiptPayloadOnPurchase = m_FetchReceiptPayloadOnPurchase;
  412. // Define our products.
  413. // Either use the Unity IAP Catalog, or manually use the ConfigurationBuilder.AddProduct API.
  414. // Use IDs from both the Unity IAP Catalog and hardcoded IDs via the ConfigurationBuilder.AddProduct API.
  415. // Use the products defined in the IAP Catalog GUI.
  416. // E.g. Menu: "Window" > "Unity IAP" > "IAP Catalog", then add products, then click "App Store Export".
  417. var catalog = ProductCatalog.LoadDefaultCatalog();
  418. foreach (var product in catalog.allValidProducts)
  419. {
  420. if (product.allStoreIDs.Count > 0)
  421. {
  422. var ids = new IDs();
  423. foreach (var storeID in product.allStoreIDs)
  424. {
  425. ids.Add(storeID.id, storeID.store);
  426. }
  427. builder.AddProduct(product.id, product.type, ids);
  428. }
  429. else
  430. {
  431. builder.AddProduct(product.id, product.type);
  432. }
  433. }
  434. // In this case our products have the same identifier across all the App stores,
  435. // except on the Mac App store where product IDs cannot be reused across both Mac and
  436. // iOS stores.
  437. // So on the Mac App store our products have different identifiers,
  438. // and we tell Unity IAP this by using the IDs class.
  439. builder.AddProduct("100.gold.coins", ProductType.Consumable, new IDs
  440. {
  441. {"com.unity3d.unityiap.unityiapdemo.100goldcoins.7", MacAppStore.Name},
  442. {"000000596586", TizenStore.Name},
  443. {"com.ff", MoolahAppStore.Name},
  444. {"100.gold.coins", AmazonApps.Name},
  445. {"100.gold.coins", AppleAppStore.Name}
  446. }
  447. #if USE_PAYOUTS
  448. , new List<PayoutDefinition> {
  449. new PayoutDefinition(PayoutType.Item, "", 1, "item_id:76543"),
  450. new PayoutDefinition(PayoutType.Currency, "gold", 50)
  451. }
  452. #endif //USE_PAYOUTS
  453. );
  454. builder.AddProduct("500.gold.coins", ProductType.Consumable, new IDs
  455. {
  456. {"com.unity3d.unityiap.unityiapdemo.500goldcoins.7", MacAppStore.Name},
  457. {"000000596581", TizenStore.Name},
  458. {"com.ee", MoolahAppStore.Name},
  459. {"500.gold.coins", AmazonApps.Name},
  460. }
  461. #if USE_PAYOUTS
  462. , new PayoutDefinition(PayoutType.Currency, "gold", 500)
  463. #endif //USE_PAYOUTS
  464. );
  465. builder.AddProduct("300.gold.coins", ProductType.Consumable, new IDs
  466. {
  467. }
  468. #if USE_PAYOUTS
  469. , new List<PayoutDefinition> {
  470. new PayoutDefinition(PayoutType.Item, "", 1, "item_id:76543"),
  471. new PayoutDefinition(PayoutType.Currency, "gold", 50)
  472. }
  473. #endif //USE_PAYOUTS
  474. );
  475. builder.AddProduct("sub1", ProductType.Subscription, new IDs
  476. {
  477. });
  478. builder.AddProduct("sub2", ProductType.Subscription, new IDs
  479. {
  480. });
  481. // Write Amazon's JSON description of our products to storage when using Amazon's local sandbox.
  482. // This should be removed from a production build.
  483. //builder.Configure<IAmazonConfiguration>().WriteSandboxJSON(builder.products);
  484. // This enables simulated purchase success for Samsung IAP.
  485. // You would remove this, or set to SamsungAppsMode.Production, before building your release package.
  486. builder.Configure<ISamsungAppsConfiguration>().SetMode(SamsungAppsMode.AlwaysSucceed);
  487. // This records whether we are using Samsung IAP. Currently ISamsungAppsExtensions.RestoreTransactions
  488. // displays a blocking Android Activity, so:
  489. // A) Unity IAP does not automatically restore purchases on Samsung Galaxy Apps
  490. // B) IAPDemo (this) displays the "Restore" GUI button for Samsung Galaxy Apps
  491. m_IsSamsungAppsStoreSelected =
  492. Application.platform == RuntimePlatform.Android && module.appStore == AppStore.SamsungApps;
  493. // This selects the GroupId that was created in the Tizen Store for this set of products
  494. // An empty or non-matching GroupId here will result in no products available for purchase
  495. builder.Configure<ITizenStoreConfiguration>().SetGroupId("100000085616");
  496. #if INTERCEPT_PROMOTIONAL_PURCHASES
  497. // On iOS and tvOS we can intercept promotional purchases that come directly from the App Store.
  498. // On other platforms this will have no effect; OnPromotionalPurchase will never be called.
  499. builder.Configure<IAppleConfiguration>().SetApplePromotionalPurchaseInterceptorCallback(OnPromotionalPurchase);
  500. Debug.Log("Setting Apple promotional purchase interceptor callback");
  501. #endif
  502. #if RECEIPT_VALIDATION
  503. string appIdentifier;
  504. #if UNITY_5_6_OR_NEWER
  505. appIdentifier = Application.identifier;
  506. #else
  507. appIdentifier = Application.bundleIdentifier;
  508. #endif
  509. validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(),
  510. UnityChannelTangle.Data(), appIdentifier);
  511. #endif
  512. Action initializeUnityIap = () =>
  513. {
  514. // Now we're ready to initialize Unity IAP.
  515. UnityPurchasing.Initialize(this, builder);
  516. };
  517. bool needExternalLogin = m_IsUnityChannelSelected;
  518. if (!needExternalLogin)
  519. {
  520. initializeUnityIap();
  521. }
  522. else
  523. {
  524. // Call UnityChannel initialize and (later) login asynchronously
  525. // UnityChannel configuration settings. Required for Xiaomi MiPay.
  526. // Collect this app configuration from the Unity Developer website at
  527. // [2017-04-17 PENDING - Contact support representative]
  528. // https://developer.cloud.unity3d.com/ providing your Xiaomi MiPay App
  529. // ID, App Key, and App Secret. This permits Unity to proxy from the
  530. // user's device into the MiPay system.
  531. // IMPORTANT PRE-BUILD STEP: For mandatory Chinese Government app auditing
  532. // and for MiPay testing, enable debug mode (test mode)
  533. // using the `AppInfo.debug = true;` when initializing Unity Channel.
  534. AppInfo unityChannelAppInfo = new AppInfo();
  535. unityChannelAppInfo.appId = "abc123appId";
  536. unityChannelAppInfo.appKey = "efg456appKey";
  537. unityChannelAppInfo.clientId = "hij789clientId";
  538. unityChannelAppInfo.clientKey = "klm012clientKey";
  539. unityChannelAppInfo.debug = false;
  540. // Shared handler for Unity Channel initialization, here, and login, later
  541. unityChannelLoginHandler = new UnityChannelLoginHandler();
  542. unityChannelLoginHandler.initializeFailedAction = (string message) =>
  543. {
  544. Debug.LogError("Failed to initialize and login to UnityChannel: " + message);
  545. };
  546. unityChannelLoginHandler.initializeSucceededAction = () => { initializeUnityIap(); };
  547. StoreService.Initialize(unityChannelAppInfo, unityChannelLoginHandler);
  548. }
  549. }
  550. // For handling initialization and login of UnityChannel, returning control to our store after.
  551. class UnityChannelLoginHandler : ILoginListener
  552. {
  553. internal Action initializeSucceededAction;
  554. internal Action<string> initializeFailedAction;
  555. internal Action<UserInfo> loginSucceededAction;
  556. internal Action<string> loginFailedAction;
  557. public void OnInitialized()
  558. {
  559. initializeSucceededAction();
  560. }
  561. public void OnInitializeFailed(string message)
  562. {
  563. initializeFailedAction(message);
  564. }
  565. public void OnLogin(UserInfo userInfo)
  566. {
  567. loginSucceededAction(userInfo);
  568. }
  569. public void OnLoginFailed(string message)
  570. {
  571. loginFailedAction(message);
  572. }
  573. }
  574. /// <summary>
  575. /// This will be called after a call to IAppleExtensions.RestoreTransactions().
  576. /// </summary>
  577. private void OnTransactionsRestored(bool success)
  578. {
  579. Debug.Log("Transactions restored." + success);
  580. }
  581. /// <summary>
  582. /// iOS Specific.
  583. /// This is called as part of Apple's 'Ask to buy' functionality,
  584. /// when a purchase is requested by a minor and referred to a parent
  585. /// for approval.
  586. ///
  587. /// When the purchase is approved or rejected, the normal purchase events
  588. /// will fire.
  589. /// </summary>
  590. /// <param name="item">Item.</param>
  591. private void OnDeferred(Product item)
  592. {
  593. Debug.Log("Purchase deferred: " + item.definition.id);
  594. }
  595. #if INTERCEPT_PROMOTIONAL_PURCHASES
  596. private void OnPromotionalPurchase(Product item) {
  597. Debug.Log("Attempted promotional purchase: " + item.definition.id);
  598. // Promotional purchase has been detected. Handle this event by, e.g. presenting a parental gate.
  599. // Here, for demonstration purposes only, we will wait five seconds before continuing the purchase.
  600. StartCoroutine(ContinuePromotionalPurchases());
  601. }
  602. private IEnumerator ContinuePromotionalPurchases()
  603. {
  604. Debug.Log("Continuing promotional purchases in 5 seconds");
  605. yield return new WaitForSeconds(5);
  606. Debug.Log("Continuing promotional purchases now");
  607. m_AppleExtensions.ContinuePromotionalPurchases (); // iOS and tvOS only; does nothing on Mac
  608. }
  609. #endif
  610. private void InitUI(IEnumerable<Product> items)
  611. {
  612. // Show Restore, Register, Login, and Validate buttons on supported platforms
  613. restoreButton.gameObject.SetActive(true);
  614. loginButton.gameObject.SetActive(NeedLoginButton());
  615. validateButton.gameObject.SetActive(NeedValidateButton());
  616. ClearProductUIs();
  617. restoreButton.onClick.AddListener(RestoreButtonClick);
  618. loginButton.onClick.AddListener(LoginButtonClick);
  619. validateButton.onClick.AddListener(ValidateButtonClick);
  620. versionText.text = "Unity version: " + Application.unityVersion + "\n" +
  621. "IAP version: " + StandardPurchasingModule.k_PackageVersion;
  622. }
  623. public void PurchaseButtonClick(string productID)
  624. {
  625. if (m_PurchaseInProgress == true)
  626. {
  627. Debug.Log("Please wait, purchase in progress");
  628. return;
  629. }
  630. if (m_Controller == null)
  631. {
  632. Debug.LogError("Purchasing is not initialized");
  633. return;
  634. }
  635. if (m_Controller.products.WithID(productID) == null)
  636. {
  637. Debug.LogError("No product has id " + productID);
  638. return;
  639. }
  640. // For platforms needing Login, games utilizing a connected backend
  641. // game server may wish to login.
  642. // Standalone games may not need to login.
  643. if (NeedLoginButton() && m_IsLoggedIn == false)
  644. {
  645. Debug.LogWarning("Purchase notifications will not be forwarded server-to-server. Login incomplete.");
  646. }
  647. // Don't need to draw our UI whilst a purchase is in progress.
  648. // This is not a requirement for IAP Applications but makes the demo
  649. // scene tidier whilst the fake purchase dialog is showing.
  650. m_PurchaseInProgress = true;
  651. //Sample code how to add accountId in developerPayload to pass it to getBuyIntentExtraParams
  652. //Dictionary<string, string> payload_dictionary = new Dictionary<string, string>();
  653. //payload_dictionary["accountId"] = "Faked account id";
  654. //payload_dictionary["developerPayload"] = "Faked developer payload";
  655. //m_Controller.InitiatePurchase(m_Controller.products.WithID(productID), MiniJson.JsonEncode(payload_dictionary));
  656. m_Controller.InitiatePurchase(m_Controller.products.WithID(productID), "developerPayload");
  657. }
  658. public void RestoreButtonClick()
  659. {
  660. if (m_IsCloudMoolahStoreSelected)
  661. {
  662. if (m_IsLoggedIn == false)
  663. {
  664. Debug.LogError("CloudMoolah purchase restoration aborted. Login incomplete.");
  665. }
  666. else
  667. {
  668. // Restore abnornal transaction identifer, if Client don't receive transaction identifer.
  669. m_MoolahExtensions.RestoreTransactionID((RestoreTransactionIDState restoreTransactionIDState) =>
  670. {
  671. Debug.Log("restoreTransactionIDState = " + restoreTransactionIDState.ToString());
  672. bool success =
  673. restoreTransactionIDState != RestoreTransactionIDState.RestoreFailed &&
  674. restoreTransactionIDState != RestoreTransactionIDState.NotKnown;
  675. OnTransactionsRestored(success);
  676. });
  677. }
  678. }
  679. else if (m_IsSamsungAppsStoreSelected)
  680. {
  681. m_SamsungExtensions.RestoreTransactions(OnTransactionsRestored);
  682. }
  683. else if (Application.platform == RuntimePlatform.WSAPlayerX86 ||
  684. Application.platform == RuntimePlatform.WSAPlayerX64 ||
  685. Application.platform == RuntimePlatform.WSAPlayerARM)
  686. {
  687. m_MicrosoftExtensions.RestoreTransactions();
  688. }
  689. else if (m_IsGooglePlayStoreSelected)
  690. {
  691. m_GooglePlayStoreExtensions.RestoreTransactions(OnTransactionsRestored);
  692. }
  693. else
  694. {
  695. m_AppleExtensions.RestoreTransactions(OnTransactionsRestored);
  696. }
  697. }
  698. public void LoginButtonClick()
  699. {
  700. if (!m_IsUnityChannelSelected)
  701. {
  702. Debug.Log("Login is only required for the Xiaomi store");
  703. return;
  704. }
  705. unityChannelLoginHandler.loginSucceededAction = (UserInfo userInfo) =>
  706. {
  707. m_IsLoggedIn = true;
  708. Debug.LogFormat("Succeeded logging into UnityChannel. channel {0}, userId {1}, userLoginToken {2} ",
  709. userInfo.channel, userInfo.userId, userInfo.userLoginToken);
  710. };
  711. unityChannelLoginHandler.loginFailedAction = (string message) =>
  712. {
  713. m_IsLoggedIn = false;
  714. Debug.LogError("Failed logging into UnityChannel. " + message);
  715. };
  716. StoreService.Login(unityChannelLoginHandler);
  717. }
  718. public void ValidateButtonClick()
  719. {
  720. // For local validation, see ProcessPurchase.
  721. if (!m_IsUnityChannelSelected)
  722. {
  723. Debug.Log("Remote purchase validation is only supported for the Xiaomi store");
  724. return;
  725. }
  726. string txId = m_LastTransactionID;
  727. m_UnityChannelExtensions.ValidateReceipt(txId, (bool success, string signData, string signature) =>
  728. {
  729. Debug.LogFormat("ValidateReceipt transactionId {0}, success {1}, signData {2}, signature {3}",
  730. txId, success, signData, signature);
  731. // May use signData and signature results to validate server-to-server
  732. });
  733. }
  734. private void ClearProductUIs()
  735. {
  736. foreach (var productUIKVP in m_ProductUIs)
  737. {
  738. GameObject.Destroy(productUIKVP.Value.gameObject);
  739. }
  740. m_ProductUIs.Clear();
  741. }
  742. private void AddProductUIs(Product[] products)
  743. {
  744. ClearProductUIs();
  745. var templateRectTransform = productUITemplate.GetComponent<RectTransform>();
  746. var height = templateRectTransform.rect.height;
  747. var currPos = templateRectTransform.localPosition;
  748. contentRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, products.Length * height);
  749. foreach (var p in products)
  750. {
  751. var newProductUI = GameObject.Instantiate(productUITemplate.gameObject) as GameObject;
  752. newProductUI.transform.SetParent(productUITemplate.transform.parent, false);
  753. var rect = newProductUI.GetComponent<RectTransform>();
  754. rect.localPosition = currPos;
  755. currPos += Vector3.down * height;
  756. newProductUI.SetActive(true);
  757. var productUIComponent = newProductUI.GetComponent<IAPDemoProductUI>();
  758. productUIComponent.SetProduct(p, PurchaseButtonClick);
  759. m_ProductUIs[p.definition.id] = productUIComponent;
  760. }
  761. }
  762. private void UpdateProductUI(Product p)
  763. {
  764. if (m_ProductUIs.ContainsKey(p.definition.id))
  765. {
  766. m_ProductUIs[p.definition.id].SetProduct(p, PurchaseButtonClick);
  767. }
  768. }
  769. private void UpdateProductPendingUI(Product p, int secondsRemaining)
  770. {
  771. if (m_ProductUIs.ContainsKey(p.definition.id))
  772. {
  773. m_ProductUIs[p.definition.id].SetPendingTime(secondsRemaining);
  774. }
  775. }
  776. private bool NeedRestoreButton()
  777. {
  778. return Application.platform == RuntimePlatform.IPhonePlayer ||
  779. Application.platform == RuntimePlatform.OSXPlayer ||
  780. Application.platform == RuntimePlatform.tvOS ||
  781. Application.platform == RuntimePlatform.WSAPlayerX86 ||
  782. Application.platform == RuntimePlatform.WSAPlayerX64 ||
  783. Application.platform == RuntimePlatform.WSAPlayerARM ||
  784. m_IsSamsungAppsStoreSelected ||
  785. m_IsCloudMoolahStoreSelected;
  786. }
  787. private bool NeedLoginButton()
  788. {
  789. return m_IsUnityChannelSelected;
  790. }
  791. private bool NeedValidateButton()
  792. {
  793. return m_IsUnityChannelSelected;
  794. }
  795. private void LogProductDefinitions()
  796. {
  797. var products = m_Controller.products.all;
  798. foreach (var product in products)
  799. {
  800. #if UNITY_5_6_OR_NEWER
  801. Debug.Log(string.Format("id: {0}\nstore-specific id: {1}\ntype: {2}\nenabled: {3}\n", product.definition.id, product.definition.storeSpecificId, product.definition.type.ToString(), product.definition.enabled ? "enabled" : "disabled"));
  802. #else
  803. Debug.Log(string.Format("id: {0}\nstore-specific id: {1}\ntype: {2}\n", product.definition.id,
  804. product.definition.storeSpecificId, product.definition.type.ToString()));
  805. #endif
  806. }
  807. }
  808. }
  809. #endif // UNITY_PURCHASING