RedeemLicenseHelper.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Net;
  5. using System.Net.Http;
  6. using System.Text;
  7. using System.Threading.Tasks;
  8. using SingularityGroup.HotReload.DTO;
  9. using SingularityGroup.HotReload.Newtonsoft.Json;
  10. using UnityEditor;
  11. using UnityEngine;
  12. namespace SingularityGroup.HotReload.Editor {
  13. internal enum RedeemStage {
  14. None,
  15. Registration,
  16. Redeem,
  17. Login
  18. }
  19. // IMPORTANT: don't rename
  20. internal enum RegistrationOutcome {
  21. None,
  22. Indie,
  23. Business,
  24. }
  25. internal class RedeemLicenseHelper {
  26. public static readonly RedeemLicenseHelper I = new RedeemLicenseHelper();
  27. private string _pendingCompanySize;
  28. private string _pendingInvoiceNumber;
  29. private string _pendingRedeemEmail;
  30. private const string registerFlagPath = PackageConst.LibraryCachePath + "/registerFlag.txt";
  31. public const string registerOutcomePath = PackageConst.LibraryCachePath + "/registerOutcome.txt";
  32. public RedeemStage RedeemStage { get; private set; }
  33. public RegistrationOutcome RegistrationOutcome { get; private set; }
  34. public bool RegistrationRequired => RedeemStage != RedeemStage.None;
  35. private string status;
  36. private string error;
  37. const string statusSuccess = "success";
  38. const string statusAlreadyClaimed = "already redeemed by this user/device";
  39. const string unknownError = "We apologize, an error happened while redeeming your license. Please reach out to customer support for assistance.";
  40. private GUILayoutOption[] secondaryButtonLayoutOptions = new[] { GUILayout.MaxWidth(100) };
  41. private bool requestingRedeem;
  42. private HttpClient redeemClient;
  43. const string redeemUrl = "https://vmhzj6jonn3qy7hk7tx7levpli0bstpj.lambda-url.us-east-1.on.aws/redeem";
  44. public RedeemLicenseHelper() {
  45. if (File.Exists(registerFlagPath)) {
  46. RedeemStage = RedeemStage.Registration;
  47. }
  48. try {
  49. if (File.Exists(registerOutcomePath)) {
  50. RegistrationOutcome outcome;
  51. if (Enum.TryParse(File.ReadAllText(registerOutcomePath), out outcome)) {
  52. RegistrationOutcome = outcome;
  53. }
  54. }
  55. } catch (Exception e) {
  56. Log.Warning($"Failed determining registration outcome with {e.GetType().Name}: {e.Message}");
  57. }
  58. }
  59. public void RenderStage(HotReloadRunTabState state) {
  60. if (state.redeemStage == RedeemStage.Registration) {
  61. RenderRegistration();
  62. } else if (state.redeemStage == RedeemStage.Redeem) {
  63. RenderRedeem();
  64. } else if (state.redeemStage == RedeemStage.Login) {
  65. RenderLogin(state);
  66. }
  67. }
  68. private void RenderRegistration() {
  69. var message = PackageConst.IsAssetStoreBuild
  70. ? "Unity Pro users are required to obtain an additional license. You are eligible to redeem one if your company has ten or fewer employees. Please enter your company details below."
  71. : "The licensing model for Unity Pro users varies depending on the number of employees in your company. Please enter your company details below.";
  72. if (error != null) {
  73. EditorGUILayout.HelpBox(error, MessageType.Warning);
  74. } else {
  75. EditorGUILayout.HelpBox(message, MessageType.Info);
  76. }
  77. EditorGUILayout.Space();
  78. EditorGUILayout.Space();
  79. EditorGUILayout.LabelField("Company size (number of employees)");
  80. GUI.SetNextControlName("company_size");
  81. _pendingCompanySize = EditorGUILayout.TextField(_pendingCompanySize)?.Trim();
  82. EditorGUILayout.Space();
  83. if (GUILayout.Button("Proceed")) {
  84. int companySize;
  85. if (!int.TryParse(_pendingCompanySize, out companySize)) {
  86. error = "Please enter a number.";
  87. } else {
  88. error = null;
  89. HandleRegistration(companySize);
  90. }
  91. }
  92. }
  93. void HandleRegistration(int companySize) {
  94. RequestHelper.RequestEditorEvent(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.Licensing, StatEventType.Register), new EditorExtraData { { StatKey.CompanySize, companySize } });
  95. if (companySize > 10) {
  96. FinishRegistration(RegistrationOutcome.Business);
  97. EditorCodePatcher.DownloadAndRun().Forget();
  98. } else if (PackageConst.IsAssetStoreBuild) {
  99. SwitchToStage(RedeemStage.Redeem);
  100. } else {
  101. FinishRegistration(RegistrationOutcome.Indie);
  102. EditorCodePatcher.DownloadAndRun().Forget();
  103. }
  104. }
  105. private void RenderRedeem() {
  106. if (error != null) {
  107. EditorGUILayout.HelpBox(error, MessageType.Warning);
  108. } else {
  109. EditorGUILayout.HelpBox("To enable us to verify your purchase, please enter your invoice number/order ID. Additionally, provide the email address that you intend to use for managing your credentials.", MessageType.Info);
  110. }
  111. EditorGUILayout.Space();
  112. EditorGUILayout.Space();
  113. EditorGUILayout.LabelField("Invoice number/Order ID");
  114. GUI.SetNextControlName("invoice_number");
  115. _pendingInvoiceNumber = EditorGUILayout.TextField(_pendingInvoiceNumber ?? HotReloadPrefs.RedeemLicenseInvoice)?.Trim();
  116. EditorGUILayout.Space();
  117. EditorGUILayout.LabelField("Email");
  118. GUI.SetNextControlName("email_redeem");
  119. _pendingRedeemEmail = EditorGUILayout.TextField(_pendingRedeemEmail ?? HotReloadPrefs.RedeemLicenseEmail);
  120. EditorGUILayout.Space();
  121. using (new EditorGUI.DisabledScope(requestingRedeem)) {
  122. if (GUILayout.Button("Redeem", HotReloadRunTab.bigButtonHeight)) {
  123. RedeemLicense(email: _pendingRedeemEmail, invoiceNumber: _pendingInvoiceNumber).Forget();
  124. }
  125. }
  126. EditorGUILayout.Space();
  127. EditorGUILayout.Space();
  128. using (new EditorGUILayout.HorizontalScope()) {
  129. GUILayout.FlexibleSpace();
  130. if (GUILayout.Button("Skip", secondaryButtonLayoutOptions)) {
  131. SwitchToStage(RedeemStage.Login);
  132. }
  133. GUILayout.FlexibleSpace();
  134. }
  135. }
  136. async Task RedeemLicense(string email, string invoiceNumber) {
  137. string validationError;
  138. if (string.IsNullOrEmpty(invoiceNumber)) {
  139. validationError = "Please enter invoice number / order ID.";
  140. } else {
  141. validationError = HotReloadRunTab.ValidateEmail(email);
  142. }
  143. if (validationError != null) {
  144. error = validationError;
  145. return;
  146. }
  147. var resp = await RequestRedeem(email: email, invoiceNumber: invoiceNumber);
  148. status = resp?.status;
  149. if (status != null) {
  150. if (status != statusSuccess && status != statusAlreadyClaimed) {
  151. Log.Error("Redeeming license failed: unknown status received");
  152. error = unknownError;
  153. } else {
  154. HotReloadPrefs.RedeemLicenseEmail = email;
  155. HotReloadPrefs.RedeemLicenseInvoice = invoiceNumber;
  156. // prepare data for login screen
  157. HotReloadPrefs.LicenseEmail = email;
  158. HotReloadPrefs.LicensePassword = null;
  159. SwitchToStage(RedeemStage.Login);
  160. }
  161. } else if (resp?.error != null) {
  162. Log.Warning($"Redeeming a license failed with error: {resp.error}");
  163. error = GetPrettyError(resp);
  164. } else {
  165. Log.Warning("Redeeming a license failed: uknown error encountered");
  166. error = unknownError;
  167. }
  168. }
  169. string GetPrettyError(RedeemResponse response) {
  170. var err = response?.error;
  171. if (err == null) {
  172. return unknownError;
  173. }
  174. if (err.Contains("Invalid email")) {
  175. return "Please enter a valid email address.";
  176. } else if (err.Contains("License invoice already redeemed")) {
  177. return "The invoice number/order ID you're trying to use has already been applied to redeem a license. Please enter a different invoice number/order ID. If you have already redeemed a license for another email, you may proceed to the next step.";
  178. } else if (err.Contains("Different license already redeemed by given email")) {
  179. return "The provided email has already been used to redeem a license. If you have previously redeemed a license, you can proceed to the next step and use your existing credentials. If not, please input a different email address.";
  180. } else if (err.Contains("Invoice not found")) {
  181. return "The invoice was not found. Please ensure that you've entered the correct invoice number/order ID.";
  182. } else if (err.Contains("Invoice refunded")) {
  183. return "The purchase has been refunded. Please enter a different invoice number/order ID.";
  184. } else {
  185. return unknownError;
  186. }
  187. }
  188. async Task<RedeemResponse> RequestRedeem(string email, string invoiceNumber) {
  189. requestingRedeem = true;
  190. await ThreadUtility.SwitchToThreadPool();
  191. try {
  192. redeemClient = redeemClient ?? (redeemClient = HttpClientUtils.CreateHttpClient());
  193. var input = new Dictionary<string, string> {
  194. { "email", email },
  195. { "invoice", invoiceNumber }
  196. };
  197. var content = new StringContent(JsonConvert.SerializeObject(input), Encoding.UTF8, "application/json");
  198. using (var resp = await redeemClient.PostAsync(redeemUrl, content, HotReloadWindow.Current.cancelToken).ConfigureAwait(false)) {
  199. if (resp.StatusCode != HttpStatusCode.OK) {
  200. return new RedeemResponse(null, $"Redeem request failed. Status code: {(int)resp.StatusCode}, reason: {resp.ReasonPhrase}");
  201. }
  202. var str = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);
  203. try {
  204. return JsonConvert.DeserializeObject<RedeemResponse>(str);
  205. } catch (Exception ex) {
  206. return new RedeemResponse(null, $"Failed deserializing redeem response with exception: {ex.GetType().Name}: {ex.Message}");
  207. }
  208. }
  209. } catch (WebException ex) {
  210. return new RedeemResponse(null, $"Redeeming license failed: WebException encountered {ex.Message}");
  211. } finally {
  212. requestingRedeem = false;
  213. }
  214. }
  215. private class RedeemResponse {
  216. public string status;
  217. public string error;
  218. public RedeemResponse(string status, string error) {
  219. this.status = status;
  220. this.error = error;
  221. }
  222. }
  223. private void RenderLogin(HotReloadRunTabState state) {
  224. if (status == statusSuccess) {
  225. EditorGUILayout.HelpBox("Success! You will receive an email containing your license password shortly. Once you receive it, please enter the received password in the designated field below to complete your registration.", MessageType.Info);
  226. } else if (status == statusAlreadyClaimed) {
  227. EditorGUILayout.HelpBox("Your license has already been redeemed. Please enter your existing password below.", MessageType.Info);
  228. }
  229. EditorGUILayout.Space();
  230. EditorGUILayout.Space();
  231. HotReloadRunTab.RenderLicenseInnerPanel(state, renderLogout: false);
  232. EditorGUILayout.Space();
  233. EditorGUILayout.Space();
  234. using (new EditorGUILayout.HorizontalScope()) {
  235. GUILayout.FlexibleSpace();
  236. if (GUILayout.Button("Go Back", secondaryButtonLayoutOptions)) {
  237. SwitchToStage(RedeemStage.Redeem);
  238. }
  239. GUILayout.FlexibleSpace();
  240. }
  241. }
  242. public void StartRegistration() {
  243. // ReSharper disable once AssignNullToNotNullAttribute
  244. Directory.CreateDirectory(Path.GetDirectoryName(registerFlagPath));
  245. using (File.Create(registerFlagPath)) {
  246. }
  247. RedeemStage = RedeemStage.Registration;
  248. RegistrationOutcome = RegistrationOutcome.None;
  249. }
  250. public void FinishRegistration(RegistrationOutcome outcome) {
  251. // ReSharper disable once AssignNullToNotNullAttribute
  252. Directory.CreateDirectory(Path.GetDirectoryName(registerFlagPath));
  253. File.WriteAllText(registerOutcomePath, outcome.ToString());
  254. File.Delete(registerFlagPath);
  255. RegistrationOutcome = outcome;
  256. SwitchToStage(RedeemStage.None);
  257. Cleanup();
  258. }
  259. void SwitchToStage(RedeemStage stage) {
  260. // remove focus so that the input field re-renders
  261. GUI.FocusControl(null);
  262. RedeemStage = stage;
  263. }
  264. void Cleanup() {
  265. redeemClient?.Dispose();
  266. redeemClient = null;
  267. _pendingCompanySize = null;
  268. _pendingInvoiceNumber = null;
  269. _pendingRedeemEmail = null;
  270. status = null;
  271. error = null;
  272. }
  273. }
  274. }