IAPDemo.cs 32 KB

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