1390 lines
74 KiB
C#
1390 lines
74 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using SingularityGroup.HotReload.DTO;
|
|
using SingularityGroup.HotReload.EditorDependencies;
|
|
using UnityEditor;
|
|
using UnityEditor.Compilation;
|
|
using UnityEngine;
|
|
using Color = UnityEngine.Color;
|
|
using Task = System.Threading.Tasks.Task;
|
|
#if UNITY_2019_4_OR_NEWER
|
|
using Unity.CodeEditor;
|
|
#endif
|
|
|
|
namespace SingularityGroup.HotReload.Editor {
|
|
internal class ErrorData {
|
|
public string fileName;
|
|
public string error;
|
|
public TextAsset file;
|
|
public int lineNumber;
|
|
public string stacktrace;
|
|
public string linkString;
|
|
private static string[] supportedPaths = new[] { Path.GetFullPath("Assets"), Path.GetFullPath("Plugins") };
|
|
|
|
public static ErrorData GetErrorData(string errorString) {
|
|
// Get the relevant file name
|
|
string stackTrace = errorString;
|
|
string fileName = null;
|
|
try {
|
|
int csIndex = 0;
|
|
int attempt = 0;
|
|
do {
|
|
csIndex = errorString.IndexOf(".cs", csIndex + 1, StringComparison.Ordinal);
|
|
if (csIndex == -1) {
|
|
break;
|
|
}
|
|
int fileNameStartIndex = csIndex - 1;
|
|
for (; fileNameStartIndex >= 0; fileNameStartIndex--) {
|
|
if (!char.IsLetter(errorString[fileNameStartIndex])) {
|
|
if (errorString.Contains("error CS")) {
|
|
fileName = errorString.Substring(fileNameStartIndex + 1,
|
|
csIndex - fileNameStartIndex + ".cs".Length - 1);
|
|
} else {
|
|
fileName = errorString.Substring(fileNameStartIndex,
|
|
csIndex - fileNameStartIndex + ".cs".Length);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
} while (attempt++ < 100 && fileName == null);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
fileName = fileName ?? "Tap to show stacktrace";
|
|
|
|
// Get the error
|
|
string error = (errorString.Contains("error CS")
|
|
? "Compile error, "
|
|
: "Unsupported change detected, ") + "tap here to see more.";
|
|
int endOfError = errorString.IndexOf(". in ", StringComparison.Ordinal);
|
|
string specialChars = "\"'/\\";
|
|
char[] characters = specialChars.ToCharArray();
|
|
int specialChar = errorString.IndexOfAny(characters);
|
|
try {
|
|
if (errorString.Contains("error CS") ) {
|
|
error = errorString.Substring(errorString.IndexOf("error CS", StringComparison.Ordinal), errorString.Length - errorString.IndexOf("error CS", StringComparison.Ordinal)).Trim();
|
|
using (StringReader reader = new StringReader(error)) {
|
|
string line;
|
|
while ((line = reader.ReadLine()) != null) {
|
|
error = line;
|
|
break;
|
|
}
|
|
}
|
|
} else if (errorString.StartsWith("errors:", StringComparison.Ordinal) && endOfError > 0) {
|
|
error = errorString.Substring("errors: ".Length, endOfError - "errors: ".Length).Trim();
|
|
} else if (errorString.StartsWith("errors:", StringComparison.Ordinal) && specialChar > 0) {
|
|
error = errorString.Substring("errors: ".Length, specialChar - "errors: ".Length).Trim();
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
|
|
// Get relative path
|
|
TextAsset file = null;
|
|
try {
|
|
foreach (var path in supportedPaths) {
|
|
int lastprojectIndex = 0;
|
|
int attempt = 0;
|
|
while (attempt++ < 100 && !file) {
|
|
lastprojectIndex = errorString.IndexOf(path, lastprojectIndex + 1, StringComparison.Ordinal);
|
|
if (lastprojectIndex == -1) {
|
|
break;
|
|
}
|
|
var fullCsIndex = errorString.IndexOf(".cs", lastprojectIndex, StringComparison.Ordinal);
|
|
var l = fullCsIndex - lastprojectIndex + ".cs".Length;
|
|
if (l <= 0) {
|
|
continue;
|
|
}
|
|
var candidateAbsolutePath = errorString.Substring(lastprojectIndex, fullCsIndex - lastprojectIndex + ".cs".Length);
|
|
var candidateRelativePath = EditorCodePatcher.GetRelativePath(filespec: candidateAbsolutePath, folder: path);
|
|
file = AssetDatabase.LoadAssetAtPath<TextAsset>(candidateRelativePath);
|
|
}
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
|
|
// Get the line number
|
|
int lineNumber = 0;
|
|
try {
|
|
int lastIndex = 0;
|
|
int attempt = 0;
|
|
do {
|
|
lastIndex = errorString.IndexOf(fileName, lastIndex + 1, StringComparison.Ordinal);
|
|
if (lastIndex == -1) {
|
|
break;
|
|
}
|
|
var part = errorString.Substring(lastIndex + fileName.Length);
|
|
if (!part.StartsWith(errorString.Contains("error CS") ? "(" : ":", StringComparison.Ordinal)
|
|
|| part.Length == 1
|
|
|| !char.IsDigit(part[1])
|
|
) {
|
|
continue;
|
|
}
|
|
int y = 1;
|
|
for (; y < part.Length; y++) {
|
|
if (!char.IsDigit(part[y])) {
|
|
break;
|
|
}
|
|
}
|
|
if (int.TryParse(part.Substring(1, errorString.Contains("error CS") ? y - 1 : y), out lineNumber)) {
|
|
break;
|
|
}
|
|
} while (attempt++ < 100);
|
|
} catch {
|
|
//ignore
|
|
}
|
|
|
|
return new ErrorData() {
|
|
fileName = fileName,
|
|
error = error,
|
|
file = file,
|
|
lineNumber = lineNumber,
|
|
stacktrace = stackTrace,
|
|
linkString = lineNumber > 0 ? fileName + ":" + lineNumber : fileName
|
|
};
|
|
}
|
|
|
|
}
|
|
|
|
internal struct HotReloadRunTabState {
|
|
public readonly bool spinnerActive;
|
|
public readonly string indicationIconPath;
|
|
public readonly bool requestingDownloadAndRun;
|
|
public readonly bool starting;
|
|
public readonly bool stopping;
|
|
public readonly bool running;
|
|
public readonly Tuple<float, string> startupProgress;
|
|
public readonly string indicationStatusText;
|
|
public readonly LoginStatusResponse loginStatus;
|
|
public readonly bool downloadRequired;
|
|
public readonly bool downloadStarted;
|
|
public readonly bool requestingLoginInfo;
|
|
public readonly RedeemStage redeemStage;
|
|
public readonly int suggestionCount;
|
|
|
|
public HotReloadRunTabState(
|
|
bool spinnerActive,
|
|
string indicationIconPath,
|
|
bool requestingDownloadAndRun,
|
|
bool starting,
|
|
bool stopping,
|
|
bool running,
|
|
Tuple<float, string> startupProgress,
|
|
string indicationStatusText,
|
|
LoginStatusResponse loginStatus,
|
|
bool downloadRequired,
|
|
bool downloadStarted,
|
|
bool requestingLoginInfo,
|
|
RedeemStage redeemStage,
|
|
int suggestionCount
|
|
) {
|
|
this.spinnerActive = spinnerActive;
|
|
this.indicationIconPath = indicationIconPath;
|
|
this.requestingDownloadAndRun = requestingDownloadAndRun;
|
|
this.starting = starting;
|
|
this.stopping = stopping;
|
|
this.running = running;
|
|
this.startupProgress = startupProgress;
|
|
this.indicationStatusText = indicationStatusText;
|
|
this.loginStatus = loginStatus;
|
|
this.downloadRequired = downloadRequired;
|
|
this.downloadStarted = downloadStarted;
|
|
this.requestingLoginInfo = requestingLoginInfo;
|
|
this.redeemStage = redeemStage;
|
|
this.suggestionCount = suggestionCount;
|
|
}
|
|
|
|
public static HotReloadRunTabState Current => new HotReloadRunTabState(
|
|
spinnerActive: EditorIndicationState.SpinnerActive,
|
|
indicationIconPath: EditorIndicationState.IndicationIconPath,
|
|
requestingDownloadAndRun: EditorCodePatcher.RequestingDownloadAndRun,
|
|
starting: EditorCodePatcher.Starting,
|
|
stopping: EditorCodePatcher.Stopping,
|
|
running: EditorCodePatcher.Running,
|
|
startupProgress: EditorCodePatcher.StartupProgress,
|
|
indicationStatusText: EditorIndicationState.IndicationStatusText,
|
|
loginStatus: EditorCodePatcher.Status,
|
|
downloadRequired: EditorCodePatcher.DownloadRequired,
|
|
downloadStarted: EditorCodePatcher.DownloadStarted,
|
|
requestingLoginInfo: EditorCodePatcher.RequestingLoginInfo,
|
|
redeemStage: RedeemLicenseHelper.I.RedeemStage,
|
|
suggestionCount: HotReloadTimelineHelper.Suggestions.Count
|
|
);
|
|
}
|
|
|
|
internal struct LicenseErrorData {
|
|
public readonly string description;
|
|
public bool showBuyButton;
|
|
public string buyButtonText;
|
|
public readonly bool showLoginButton;
|
|
public readonly string loginButtonText;
|
|
public readonly bool showSupportButton;
|
|
public readonly string supportButtonText;
|
|
public readonly bool showManageLicenseButton;
|
|
public readonly string manageLicenseButtonText;
|
|
|
|
public LicenseErrorData(string description, bool showManageLicenseButton = false, string manageLicenseButtonText = "", string loginButtonText = "", bool showSupportButton = false, string supportButtonText = "", bool showBuyButton = false, string buyButtonText = "", bool showLoginButton = false) {
|
|
this.description = description;
|
|
this.showManageLicenseButton = showManageLicenseButton;
|
|
this.manageLicenseButtonText = manageLicenseButtonText;
|
|
this.loginButtonText = loginButtonText;
|
|
this.showSupportButton = showSupportButton;
|
|
this.supportButtonText = supportButtonText;
|
|
this.showBuyButton = showBuyButton;
|
|
this.buyButtonText = buyButtonText;
|
|
this.showLoginButton = showLoginButton;
|
|
}
|
|
}
|
|
|
|
internal class HotReloadRunTab : HotReloadTabBase {
|
|
private static string _pendingEmail;
|
|
private static string _pendingPassword;
|
|
private string _pendingPromoCode;
|
|
private bool _requestingActivatePromoCode;
|
|
|
|
private static Tuple<string, MessageType> _activateInfoMessage;
|
|
|
|
private HotReloadRunTabState currentState => _window.RunTabState;
|
|
// Has Indie or Pro license (even if not currenctly active)
|
|
public bool HasPayedLicense => currentState.loginStatus != null && (currentState.loginStatus.isIndieLicense || currentState.loginStatus.isBusinessLicense);
|
|
public bool TrialLicense => currentState.loginStatus != null && (currentState.loginStatus?.isTrial == true);
|
|
|
|
private Vector2 _patchedMethodsScrollPos;
|
|
private Vector2 _runTabScrollPos;
|
|
|
|
private string promoCodeError;
|
|
private MessageType promoCodeErrorType;
|
|
private bool promoCodeActivatedThisSession;
|
|
|
|
public HotReloadRunTab(HotReloadWindow window) : base(window, "Run", "forward", "Run and monitor the current Hot Reload session.") { }
|
|
|
|
public override void OnGUI() {
|
|
using(new EditorGUILayout.VerticalScope()) {
|
|
OnGUICore();
|
|
}
|
|
}
|
|
|
|
internal static bool ShouldRenderConsumption(HotReloadRunTabState currentState) => (currentState.running && !currentState.starting && !currentState.stopping && currentState.loginStatus?.isLicensed != true && currentState.loginStatus?.isFree != true && !EditorCodePatcher.LoginNotRequired) && !(currentState.loginStatus == null || currentState.loginStatus.isFree);
|
|
|
|
void OnGUICore() {
|
|
using (var scope = new EditorGUILayout.ScrollViewScope(_runTabScrollPos, GUI.skin.horizontalScrollbar, GUI.skin.verticalScrollbar, GUILayout.MaxHeight(Math.Max(HotReloadWindowStyles.windowScreenHeight, 800)), GUILayout.MaxWidth(Math.Max(HotReloadWindowStyles.windowScreenWidth, 800)))) {
|
|
_runTabScrollPos.x = scope.scrollPosition.x;
|
|
_runTabScrollPos.y = scope.scrollPosition.y;
|
|
using (new EditorGUILayout.VerticalScope(HotReloadWindowStyles.DynamiSection)) {
|
|
if (HotReloadWindowStyles.windowScreenWidth > Constants.UpgradeLicenseNoteHideWidth
|
|
&& HotReloadWindowStyles.windowScreenHeight > Constants.UpgradeLicenseNoteHideHeight
|
|
) {
|
|
RenderUpgradeLicenseNote(currentState, HotReloadWindowStyles.UpgradeLicenseButtonStyle);
|
|
}
|
|
|
|
RenderIndicationPanel();
|
|
|
|
if (CanRenderBars(currentState)) {
|
|
RenderBars(currentState);
|
|
// clear red dot next time button shows
|
|
HotReloadState.ShowingRedDot = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// At the end to not fuck up rendering https://answers.unity.com/questions/400454/argumentexception-getting-control-0s-position-in-a-1.html
|
|
var renderStart = !EditorCodePatcher.Running && !EditorCodePatcher.Starting && !currentState.requestingDownloadAndRun && currentState.redeemStage == RedeemStage.None;
|
|
var e = Event.current;
|
|
if (renderStart && e.type == EventType.KeyUp
|
|
&& (e.keyCode == KeyCode.Return
|
|
|| e.keyCode == KeyCode.KeypadEnter)
|
|
) {
|
|
EditorCodePatcher.DownloadAndRun().Forget();
|
|
}
|
|
}
|
|
|
|
internal static void RenderUpgradeLicenseNote(HotReloadRunTabState currentState, GUIStyle style) {
|
|
var isIndie = RedeemLicenseHelper.I.RegistrationOutcome == RegistrationOutcome.Indie
|
|
|| EditorCodePatcher.licenseType == UnityLicenseType.UnityPersonalPlus;
|
|
|
|
if (RedeemLicenseHelper.I.RegistrationOutcome == RegistrationOutcome.Business
|
|
&& currentState.loginStatus?.isBusinessLicense != true
|
|
&& EditorCodePatcher.Running
|
|
&& (PackageConst.IsAssetStoreBuild || HotReloadPrefs.RateAppShown)
|
|
) {
|
|
// Warn asset store users they need to buy a business license
|
|
// Website users get reminded after using Hot Reload for 5+ days
|
|
RenderBusinessLicenseInfo(style);
|
|
} else if (isIndie
|
|
&& HotReloadPrefs.RateAppShown
|
|
&& !PackageConst.IsAssetStoreBuild
|
|
&& EditorCodePatcher.Running
|
|
&& currentState.loginStatus?.isBusinessLicense != true
|
|
&& currentState.loginStatus?.isIndieLicense != true
|
|
) {
|
|
// Reminder users they need to buy an indie license
|
|
RenderIndieLicenseInfo(style);
|
|
}
|
|
}
|
|
|
|
internal static bool CanRenderBars(HotReloadRunTabState currentState) {
|
|
return HotReloadWindowStyles.windowScreenHeight > Constants.EventsListHideHeight
|
|
&& HotReloadWindowStyles.windowScreenWidth > Constants.EventsListHideWidth
|
|
&& !currentState.starting
|
|
&& !currentState.stopping
|
|
&& !currentState.requestingDownloadAndRun
|
|
;
|
|
}
|
|
|
|
static Texture2D GetFoldoutIcon(AlertEntry alertEntry) {
|
|
InvertibleIcon alertIcon = InvertibleIcon.FoldoutClosed;
|
|
if (HotReloadTimelineHelper.expandedEntries.Contains(alertEntry)) {
|
|
alertIcon = InvertibleIcon.FoldoutOpen;
|
|
}
|
|
return GUIHelper.GetInvertibleIcon(alertIcon);
|
|
}
|
|
|
|
static void ToggleEntry(AlertEntry alertEntry) {
|
|
if (HotReloadTimelineHelper.expandedEntries.Contains(alertEntry)) {
|
|
HotReloadTimelineHelper.expandedEntries.Remove(alertEntry);
|
|
} else {
|
|
HotReloadTimelineHelper.expandedEntries.Add(alertEntry);
|
|
}
|
|
}
|
|
|
|
static void RenderEntries(TimelineType timelineType) {
|
|
List<AlertEntry> alertEntries;
|
|
|
|
alertEntries = timelineType == TimelineType.Suggestions ? HotReloadTimelineHelper.Suggestions : HotReloadTimelineHelper.EventsTimeline;
|
|
|
|
bool skipChildren = false;
|
|
for (int i = 0; i < alertEntries.Count; i++) {
|
|
var alertEntry = alertEntries[i];
|
|
if (i > HotReloadTimelineHelper.maxVisibleEntries && alertEntry.entryType != EntryType.Child) {
|
|
break;
|
|
}
|
|
if (timelineType != TimelineType.Suggestions) {
|
|
if (alertEntry.entryType != EntryType.Child
|
|
&& !enabledFilters.Contains(alertEntry.alertType)
|
|
) {
|
|
skipChildren = true;
|
|
continue;
|
|
} else if (alertEntry.entryType == EntryType.Child && skipChildren) {
|
|
continue;
|
|
} else {
|
|
skipChildren = false;
|
|
}
|
|
}
|
|
|
|
EntryType entryType = alertEntry.entryType;
|
|
|
|
string title = $" {alertEntry.title}{(!string.IsNullOrEmpty(alertEntry.shortDescription) ? $": {alertEntry.shortDescription}": "")}";
|
|
Texture2D icon = null;
|
|
GUIStyle style;
|
|
if (entryType != EntryType.Child) {
|
|
icon = GUIHelper.GetLocalIcon(HotReloadTimelineHelper.alertIconString[alertEntry.iconType]);
|
|
}
|
|
if (entryType == EntryType.Child) {
|
|
style = HotReloadWindowStyles.ChildBarStyle;
|
|
} else if (entryType == EntryType.Foldout) {
|
|
style = HotReloadWindowStyles.FoldoutBarStyle;
|
|
} else {
|
|
style = HotReloadWindowStyles.BarStyle;
|
|
}
|
|
|
|
Rect startRect;
|
|
using (new EditorGUILayout.HorizontalScope()) {
|
|
GUILayout.Space(0);
|
|
Rect spaceRect = GUILayoutUtility.GetLastRect();
|
|
// entry header foldout arrow
|
|
if (entryType == EntryType.Foldout) {
|
|
GUI.Label(new Rect(spaceRect.x + 3, spaceRect.y, 20, 20), new GUIContent(GetFoldoutIcon(alertEntry)));
|
|
} else if (entryType == EntryType.Child) {
|
|
GUI.Label(new Rect(spaceRect.x + 26, spaceRect.y + 2, 20, 20), new GUIContent(GetFoldoutIcon(alertEntry)));
|
|
}
|
|
// a workaround to limit the width of the label
|
|
GUILayout.Label(new GUIContent(""), style);
|
|
startRect = GUILayoutUtility.GetLastRect();
|
|
GUI.Label(startRect, new GUIContent(title, icon), style);
|
|
}
|
|
|
|
bool clickableDescription = (alertEntry.title == "Unsupported change" || alertEntry.title == "Compile error" || alertEntry.title == "Failed applying patch to method") && alertEntry.alertData.alertEntryType != AlertEntryType.InlinedMethod;
|
|
|
|
if (HotReloadTimelineHelper.expandedEntries.Contains(alertEntry) || alertEntry.alertType == AlertType.CompileError) {
|
|
using (new EditorGUILayout.VerticalScope()) {
|
|
using (new EditorGUILayout.HorizontalScope()) {
|
|
using (new EditorGUILayout.VerticalScope(entryType == EntryType.Child ? HotReloadWindowStyles.ChildEntryBoxStyle : HotReloadWindowStyles.EntryBoxStyle)) {
|
|
if (alertEntry.alertType == AlertType.Suggestion || !clickableDescription) {
|
|
GUILayout.Label(alertEntry.description, HotReloadWindowStyles.LabelStyle);
|
|
}
|
|
if (alertEntry.actionData != null) {
|
|
alertEntry.actionData.Invoke();
|
|
}
|
|
GUILayout.Space(5f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// remove button
|
|
if (timelineType == TimelineType.Suggestions && alertEntry.hasExitButton) {
|
|
var isClick = GUI.Button(new Rect(startRect.x + startRect.width - 20, startRect.y + 2, 20, 20), new GUIContent(GUIHelper.GetInvertibleIcon(InvertibleIcon.Close)), HotReloadWindowStyles.RemoveIconStyle);
|
|
if (isClick) {
|
|
HotReloadTimelineHelper.EventsTimeline.Remove(alertEntry);
|
|
var kind = HotReloadSuggestionsHelper.FindSuggestionKind(alertEntry);
|
|
if (kind != null) {
|
|
HotReloadSuggestionsHelper.SetSuggestionInactive((HotReloadSuggestionKind)kind);
|
|
}
|
|
_instantRepaint = true;
|
|
}
|
|
}
|
|
|
|
// Extend background to whole entry
|
|
var endRect = GUILayoutUtility.GetLastRect();
|
|
if (GUI.Button(new Rect(startRect) { height = endRect.y - startRect.y + endRect.height}, new GUIContent(""), HotReloadWindowStyles.BarBackgroundStyle) && (entryType == EntryType.Child || entryType == EntryType.Foldout)) {
|
|
ToggleEntry(alertEntry);
|
|
}
|
|
|
|
if (alertEntry.alertType != AlertType.Suggestion && HotReloadWindowStyles.windowScreenWidth > 400 && entryType != EntryType.Child) {
|
|
using (new EditorGUILayout.HorizontalScope()) {
|
|
var ago = (DateTime.Now - alertEntry.timestamp);
|
|
GUI.Label(new Rect(startRect.x + startRect.width - 60, startRect.y, 80, 20), ago.TotalMinutes < 1 ? "now" : $"{(ago.TotalHours > 1 ? $"{Math.Floor(ago.TotalHours)} h " : string.Empty)}{ago.Minutes} min", HotReloadWindowStyles.TimestampStyle);
|
|
}
|
|
}
|
|
|
|
GUILayout.Space(1f);
|
|
}
|
|
if (timelineType != TimelineType.Suggestions && HotReloadTimelineHelper.GetRunTabTimelineEventCount() > 40) {
|
|
GUILayout.Space(3f);
|
|
GUILayout.Label(Constants.Only40EntriesShown, HotReloadWindowStyles.EmptyListText);
|
|
}
|
|
}
|
|
|
|
private static List<AlertType> _enabledFilters;
|
|
private static List<AlertType> enabledFilters {
|
|
get {
|
|
if (_enabledFilters == null) {
|
|
_enabledFilters = new List<AlertType>();
|
|
}
|
|
|
|
if (HotReloadPrefs.RunTabUnsupportedChangesFilter && !_enabledFilters.Contains(AlertType.UnsupportedChange))
|
|
_enabledFilters.Add(AlertType.UnsupportedChange);
|
|
if (!HotReloadPrefs.RunTabUnsupportedChangesFilter && _enabledFilters.Contains(AlertType.UnsupportedChange))
|
|
_enabledFilters.Remove(AlertType.UnsupportedChange);
|
|
|
|
if (HotReloadPrefs.RunTabCompileErrorFilter && !_enabledFilters.Contains(AlertType.CompileError))
|
|
_enabledFilters.Add(AlertType.CompileError);
|
|
if (!HotReloadPrefs.RunTabCompileErrorFilter && _enabledFilters.Contains(AlertType.CompileError))
|
|
_enabledFilters.Remove(AlertType.CompileError);
|
|
|
|
if (HotReloadPrefs.RunTabPartiallyAppliedPatchesFilter && !_enabledFilters.Contains(AlertType.PartiallySupportedChange))
|
|
_enabledFilters.Add(AlertType.PartiallySupportedChange);
|
|
if (!HotReloadPrefs.RunTabPartiallyAppliedPatchesFilter && _enabledFilters.Contains(AlertType.PartiallySupportedChange))
|
|
_enabledFilters.Remove(AlertType.PartiallySupportedChange);
|
|
|
|
if (HotReloadPrefs.RunTabUndetectedPatchesFilter && !_enabledFilters.Contains(AlertType.UndetectedChange))
|
|
_enabledFilters.Add(AlertType.UndetectedChange);
|
|
if (!HotReloadPrefs.RunTabUndetectedPatchesFilter && _enabledFilters.Contains(AlertType.UndetectedChange))
|
|
_enabledFilters.Remove(AlertType.UndetectedChange);
|
|
|
|
if (HotReloadPrefs.RunTabAppliedPatchesFilter && !_enabledFilters.Contains(AlertType.AppliedChange))
|
|
_enabledFilters.Add(AlertType.AppliedChange);
|
|
if (!HotReloadPrefs.RunTabAppliedPatchesFilter && _enabledFilters.Contains(AlertType.AppliedChange))
|
|
_enabledFilters.Remove(AlertType.AppliedChange);
|
|
|
|
return _enabledFilters;
|
|
}
|
|
}
|
|
|
|
private Vector2 suggestionsScroll;
|
|
static GUILayoutOption[] timelineButtonOptions = new[] { GUILayout.Height(27), GUILayout.Width(100) };
|
|
|
|
internal static void RenderBars(HotReloadRunTabState currentState) {
|
|
if (currentState.suggestionCount > 0) {
|
|
GUILayout.Space(5f);
|
|
|
|
using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.Section)) {
|
|
using (new EditorGUILayout.VerticalScope()) {
|
|
HotReloadPrefs.RunTabEventsSuggestionsFoldout = EditorGUILayout.Foldout(HotReloadPrefs.RunTabEventsSuggestionsFoldout, "", true, HotReloadWindowStyles.CustomFoldoutStyle);
|
|
GUILayout.Space(-23);
|
|
if (GUILayout.Button($"Suggestions ({currentState.suggestionCount.ToString()})", HotReloadWindowStyles.ClickableLabelBoldStyle, GUILayout.Height(27))) {
|
|
HotReloadPrefs.RunTabEventsSuggestionsFoldout = !HotReloadPrefs.RunTabEventsSuggestionsFoldout;
|
|
}
|
|
if (HotReloadPrefs.RunTabEventsSuggestionsFoldout) {
|
|
using (new EditorGUILayout.VerticalScope(HotReloadWindowStyles.Scroll)) {
|
|
RenderEntries(TimelineType.Suggestions);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GUILayout.Space(5f);
|
|
|
|
using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.Section)) {
|
|
using (new EditorGUILayout.VerticalScope()) {
|
|
HotReloadPrefs.RunTabEventsTimelineFoldout = EditorGUILayout.Foldout(HotReloadPrefs.RunTabEventsTimelineFoldout, "", true, HotReloadWindowStyles.CustomFoldoutStyle);
|
|
GUILayout.Space(-23);
|
|
if (GUILayout.Button("Timeline", HotReloadWindowStyles.ClickableLabelBoldStyle, timelineButtonOptions)) {
|
|
HotReloadPrefs.RunTabEventsTimelineFoldout = !HotReloadPrefs.RunTabEventsTimelineFoldout;
|
|
}
|
|
if (HotReloadPrefs.RunTabEventsTimelineFoldout) {
|
|
GUILayout.Space(-10);
|
|
var noteShown = HotReloadTimelineHelper.GetRunTabTimelineEventCount() == 0 || !currentState.running;
|
|
using (new EditorGUILayout.HorizontalScope()) {
|
|
if (noteShown) {
|
|
GUILayout.Space(2f);
|
|
using (new EditorGUILayout.VerticalScope()) {
|
|
GUILayout.Space(2f);
|
|
string text;
|
|
if (currentState.redeemStage != RedeemStage.None) {
|
|
text = "Complete registration before using Hot Reload";
|
|
} else if (!currentState.running) {
|
|
text = "Use the Start button to activate Hot Reload";
|
|
} else if (enabledFilters.Count < 4 && HotReloadTimelineHelper.EventsTimeline.Count != 0) {
|
|
text = "Enable filters to see events";
|
|
} else {
|
|
text = "Make code changes to see events";
|
|
}
|
|
GUILayout.Label(text, HotReloadWindowStyles.EmptyListText);
|
|
}
|
|
GUILayout.FlexibleSpace();
|
|
} else {
|
|
GUILayout.FlexibleSpace();
|
|
if (HotReloadTimelineHelper.EventsTimeline.Count > 0 && GUILayout.Button("Clear")) {
|
|
HotReloadTimelineHelper.ClearEntries();
|
|
if (HotReloadWindow.Current) {
|
|
HotReloadWindow.Current.Repaint();
|
|
}
|
|
}
|
|
GUILayout.Space(3);
|
|
}
|
|
}
|
|
if (!noteShown) {
|
|
GUILayout.Space(2f);
|
|
using (new EditorGUILayout.VerticalScope()) {
|
|
RenderEntries(TimelineType.Timeline);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static void RenderConsumption(LoginStatusResponse loginStatus) {
|
|
if (loginStatus == null) {
|
|
return;
|
|
}
|
|
EditorGUILayout.Space();
|
|
|
|
EditorGUILayout.LabelField($"Hot Reload Limited", HotReloadWindowStyles.H3CenteredTitleStyle);
|
|
EditorGUILayout.Space();
|
|
if (loginStatus.consumptionsUnavailableReason == ConsumptionsUnavailableReason.NetworkUnreachable) {
|
|
EditorGUILayout.HelpBox("Something went wrong. Please check your internet connection.", MessageType.Warning);
|
|
} else if (loginStatus.consumptionsUnavailableReason == ConsumptionsUnavailableReason.UnrecoverableError) {
|
|
EditorGUILayout.HelpBox("Something went wrong. Please contact support if the issue persists.", MessageType.Error);
|
|
} else if (loginStatus.freeSessionFinished) {
|
|
var now = DateTime.UtcNow;
|
|
var sessionRefreshesAt = (now.AddDays(1).Date - now).Add(TimeSpan.FromMinutes(5));
|
|
var sessionRefreshString = $"Next Session: {(sessionRefreshesAt.Hours > 0 ? $"{sessionRefreshesAt.Hours}h " : "")}{sessionRefreshesAt.Minutes}min";
|
|
HotReloadGUIHelper.HelpBox(sessionRefreshString, MessageType.Warning, fontSize: 11);
|
|
} else if (loginStatus.freeSessionRunning && loginStatus.freeSessionEndTime != null) {
|
|
var sessionEndsAt = loginStatus.freeSessionEndTime.Value - DateTime.Now;
|
|
var sessionString = $"Daily Session: {(sessionEndsAt.Hours > 0 ? $"{sessionEndsAt.Hours}h " : "")}{sessionEndsAt.Minutes}min Left";
|
|
HotReloadGUIHelper.HelpBox(sessionString, MessageType.Info, fontSize: 11);
|
|
} else if (loginStatus.freeSessionEndTime == null) {
|
|
HotReloadGUIHelper.HelpBox("Daily Session: Make code changes to start", MessageType.Info, fontSize: 11);
|
|
}
|
|
}
|
|
|
|
static bool _repaint;
|
|
static bool _instantRepaint;
|
|
static DateTime _lastRepaint;
|
|
private EditorIndicationState.IndicationStatus _lastStatus;
|
|
public override void Update() {
|
|
if (EditorIndicationState.SpinnerActive) {
|
|
_repaint = true;
|
|
}
|
|
if (EditorCodePatcher.DownloadRequired) {
|
|
_repaint = true;
|
|
}
|
|
if (EditorIndicationState.IndicationIconPath == Spinner.SpinnerIconPath) {
|
|
_repaint = true;
|
|
}
|
|
try {
|
|
// workaround: hovering over non-buttons doesn't repain by default
|
|
if (EditorWindow.mouseOverWindow == HotReloadWindow.Current) {
|
|
_repaint = true;
|
|
}
|
|
if (EditorWindow.mouseOverWindow
|
|
&& EditorWindow.mouseOverWindow?.GetType() == typeof(PopupWindow)
|
|
&& HotReloadEventPopup.I.open
|
|
) {
|
|
_repaint = true;
|
|
}
|
|
} catch (NullReferenceException) {
|
|
// Unity randomly throws nullrefs when EditorWindow.mouseOverWindow gets accessed
|
|
}
|
|
if (_repaint && DateTime.UtcNow - _lastRepaint > TimeSpan.FromMilliseconds(33)) {
|
|
_repaint = false;
|
|
_instantRepaint = true;
|
|
}
|
|
// repaint on status change
|
|
var status = EditorIndicationState.CurrentIndicationStatus;
|
|
if (_lastStatus != status) {
|
|
_lastStatus = status;
|
|
_instantRepaint = true;
|
|
}
|
|
if (_instantRepaint) {
|
|
Repaint();
|
|
HotReloadEventPopup.I.Repaint();
|
|
_instantRepaint = false;
|
|
_repaint = false;
|
|
_lastRepaint = DateTime.UtcNow;
|
|
}
|
|
}
|
|
|
|
public static void RepaintInstant() {
|
|
_instantRepaint = true;
|
|
}
|
|
|
|
private void RenderRecompileButton() {
|
|
string recompileText = HotReloadWindowStyles.windowScreenWidth > Constants.RecompileButtonTextHideWidth ? " Recompile" : "";
|
|
var recompileButton = new GUIContent(recompileText, GUIHelper.GetInvertibleIcon(InvertibleIcon.Recompile));
|
|
if (!GUILayout.Button(recompileButton, HotReloadWindowStyles.RecompileButton)) {
|
|
return;
|
|
}
|
|
RecompileWithChecks();
|
|
}
|
|
|
|
public static void RecompileWithChecks() {
|
|
var firstDialoguePass = HotReloadPrefs.RecompileDialogueShown
|
|
|| EditorUtility.DisplayDialog(
|
|
title: "Hot Reload auto-applies changes",
|
|
message: "Using the Recompile button is only necessary when Hot Reload fails to apply your changes. \n\nDo you wish to proceed?",
|
|
ok: "Recompile",
|
|
cancel: "Not now");
|
|
HotReloadPrefs.RecompileDialogueShown = true;
|
|
if (!firstDialoguePass) {
|
|
return;
|
|
}
|
|
if (!ConfirmExitPlaymode("Using the Recompile button will stop Play Mode.\n\nDo you wish to proceed?")) {
|
|
return;
|
|
}
|
|
Recompile();
|
|
}
|
|
|
|
#if UNITY_2020_1_OR_NEWER
|
|
public static void SwitchToDebugMode() {
|
|
CompilationPipeline.codeOptimization = CodeOptimization.Debug;
|
|
HotReloadRunTab.Recompile();
|
|
HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.SwitchToDebugModeForInlinedMethods);
|
|
}
|
|
#endif
|
|
|
|
public static bool ConfirmExitPlaymode(string message) {
|
|
return !Application.isPlaying
|
|
|| EditorUtility.DisplayDialog(
|
|
title: "Stop Play Mode and Recompile?",
|
|
message: message,
|
|
ok: "Stop and Recompile",
|
|
cancel: "Cancel");
|
|
}
|
|
|
|
public static bool recompiling;
|
|
public static void Recompile() {
|
|
recompiling = true;
|
|
EditorApplication.isPlaying = false;
|
|
|
|
CompileMethodDetourer.Reset();
|
|
AssetDatabase.Refresh();
|
|
// This forces the recompilation if no changes were made.
|
|
// This is better UX because otherwise the recompile button is unresponsive
|
|
// which can be extra annoying if there are compile error entries in the list
|
|
if (!EditorApplication.isCompiling) {
|
|
CompilationPipeline.RequestScriptCompilation();
|
|
}
|
|
}
|
|
|
|
private void RenderIndicationButtons() {
|
|
if (currentState.requestingDownloadAndRun || currentState.starting || currentState.stopping || currentState.redeemStage != RedeemStage.None) {
|
|
return;
|
|
}
|
|
|
|
if (!currentState.running && (currentState.startupProgress?.Item1 ?? 0) == 0) {
|
|
string startText = HotReloadWindowStyles.windowScreenWidth > Constants.StartButtonTextHideWidth ? " Start" : "";
|
|
if (GUILayout.Button(new GUIContent(startText, GUIHelper.GetInvertibleIcon(InvertibleIcon.Start)), HotReloadWindowStyles.StartButton)) {
|
|
EditorCodePatcher.DownloadAndRun().Forget();
|
|
}
|
|
} else if (currentState.running && !currentState.starting) {
|
|
if (HotReloadWindowStyles.windowScreenWidth > 150) {
|
|
RenderRecompileButton();
|
|
}
|
|
string stopText = HotReloadWindowStyles.windowScreenWidth > Constants.StartButtonTextHideWidth ? " Stop" : "";
|
|
if (GUILayout.Button(new GUIContent(stopText, GUIHelper.GetInvertibleIcon(InvertibleIcon.Stop)), HotReloadWindowStyles.StopButton)) {
|
|
if (!EditorCodePatcher.StoppedServerRecently()) {
|
|
EditorCodePatcher.StopCodePatcher().Forget();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void RenderIndicationPanel() {
|
|
using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.SectionInnerBox)) {
|
|
RenderIndication();
|
|
if (HotReloadWindowStyles.windowScreenWidth > Constants.IndicationTextHideWidth) {
|
|
GUILayout.FlexibleSpace();
|
|
}
|
|
RenderIndicationButtons();
|
|
if (HotReloadWindowStyles.windowScreenWidth <= Constants.IndicationTextHideWidth) {
|
|
GUILayout.FlexibleSpace();
|
|
}
|
|
}
|
|
if (currentState.requestingDownloadAndRun || currentState.starting) {
|
|
RenderProgressBar();
|
|
}
|
|
if (HotReloadWindowStyles.windowScreenWidth > Constants.ConsumptionsHideWidth
|
|
&& HotReloadWindowStyles.windowScreenHeight > Constants.ConsumptionsHideHeight
|
|
) {
|
|
RenderLicenseInfo(currentState);
|
|
}
|
|
}
|
|
|
|
internal static void RenderLicenseInfo(HotReloadRunTabState currentState) {
|
|
var showRedeem = currentState.redeemStage != RedeemStage.None;
|
|
var showConsumptions = ShouldRenderConsumption(currentState);
|
|
if (!showConsumptions && !showRedeem) {
|
|
return;
|
|
}
|
|
using (new EditorGUILayout.VerticalScope()) {
|
|
// space needed only for consumptions because of Stop/Start button's margin
|
|
if (showConsumptions) {
|
|
GUILayout.Space(6);
|
|
}
|
|
using (new EditorGUILayout.VerticalScope(HotReloadWindowStyles.Section)) {
|
|
if (showRedeem) {
|
|
RedeemLicenseHelper.I.RenderStage(currentState);
|
|
} else {
|
|
RenderConsumption(currentState.loginStatus);
|
|
GUILayout.Space(10);
|
|
RenderLicenseInfo(currentState, currentState.loginStatus);
|
|
RenderLicenseButtons(currentState);
|
|
GUILayout.Space(10);
|
|
}
|
|
}
|
|
GUILayout.Space(6);
|
|
}
|
|
}
|
|
|
|
private Spinner _spinner = new Spinner(85);
|
|
private void RenderIndication() {
|
|
using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.IndicationBox)) {
|
|
// icon box
|
|
if (HotReloadWindowStyles.windowScreenWidth <= Constants.IndicationTextHideWidth) {
|
|
GUILayout.FlexibleSpace();
|
|
}
|
|
|
|
using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.IndicationHelpBox)) {
|
|
var text = HotReloadWindowStyles.windowScreenWidth > Constants.IndicationTextHideWidth ? $" {currentState.indicationStatusText}" : "";
|
|
if (currentState.indicationIconPath == Spinner.SpinnerIconPath) {
|
|
GUILayout.Label(new GUIContent(text, _spinner.GetIcon()), style: HotReloadWindowStyles.IndicationIcon);
|
|
} else if (currentState.indicationIconPath != null) {
|
|
var style = HotReloadWindowStyles.IndicationIcon;
|
|
if (HotReloadTimelineHelper.alertIconString.ContainsValue(currentState.indicationIconPath)) {
|
|
style = HotReloadWindowStyles.IndicationAlertIcon;
|
|
}
|
|
GUILayout.Label(new GUIContent(text, GUIHelper.GetLocalIcon(currentState.indicationIconPath)), style);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static GUIStyle _openSettingsStyle;
|
|
static GUIStyle openSettingsStyle => _openSettingsStyle ?? (_openSettingsStyle = new GUIStyle(GUI.skin.button) {
|
|
fontStyle = FontStyle.Normal,
|
|
fixedHeight = 25,
|
|
});
|
|
|
|
static GUILayoutOption[] _bigButtonHeight;
|
|
public static GUILayoutOption[] bigButtonHeight => _bigButtonHeight ?? (_bigButtonHeight = new [] {GUILayout.Height(25)});
|
|
|
|
private static GUIContent indieLicenseContent;
|
|
private static GUIContent businessLicenseContent;
|
|
|
|
internal static void RenderLicenseStatusInfo(HotReloadRunTabState currentState, LoginStatusResponse loginStatus, bool allowHide = true, bool verbose = false) {
|
|
string message = null;
|
|
MessageType messageType = default(MessageType);
|
|
Action customGUI = null;
|
|
GUIContent content = null;
|
|
if (loginStatus == null) {
|
|
// no info
|
|
} else if (loginStatus.lastLicenseError != null) {
|
|
messageType = !loginStatus.freeSessionFinished ? MessageType.Warning : MessageType.Error;
|
|
message = GetMessageFromError(currentState, loginStatus.lastLicenseError);
|
|
} else if (loginStatus.isTrial && !PackageConst.IsAssetStoreBuild) {
|
|
message = $"Using Trial license, valid until {loginStatus.licenseExpiresAt.ToShortDateString()}";
|
|
messageType = MessageType.Info;
|
|
} else if (loginStatus.isIndieLicense) {
|
|
if (verbose) {
|
|
message = " Indie license active";
|
|
messageType = MessageType.Info;
|
|
customGUI = () => {
|
|
if (loginStatus.licenseExpiresAt.Date != DateTime.MaxValue.Date) {
|
|
EditorGUILayout.LabelField($"License will renew on {loginStatus.licenseExpiresAt.ToShortDateString()}.");
|
|
EditorGUILayout.Space();
|
|
}
|
|
using (new GUILayout.HorizontalScope()) {
|
|
HotReloadAboutTab.manageLicenseButton.OnGUI();
|
|
HotReloadAboutTab.manageAccountButton.OnGUI();
|
|
}
|
|
EditorGUILayout.Space();
|
|
};
|
|
if (indieLicenseContent == null) {
|
|
indieLicenseContent = new GUIContent(message, EditorGUIUtility.FindTexture("TestPassed"));
|
|
}
|
|
content = indieLicenseContent;
|
|
}
|
|
} else if (loginStatus.isBusinessLicense) {
|
|
if (verbose) {
|
|
message = " Business license active";
|
|
messageType = MessageType.Info;
|
|
if (businessLicenseContent == null) {
|
|
businessLicenseContent = new GUIContent(message, EditorGUIUtility.FindTexture("TestPassed"));
|
|
}
|
|
content = businessLicenseContent;
|
|
customGUI = () => {
|
|
using (new GUILayout.HorizontalScope()) {
|
|
HotReloadAboutTab.manageLicenseButton.OnGUI();
|
|
HotReloadAboutTab.manageAccountButton.OnGUI();
|
|
}
|
|
EditorGUILayout.Space();
|
|
};
|
|
}
|
|
}
|
|
|
|
if (messageType != MessageType.Info && HotReloadPrefs.ErrorHidden && allowHide) {
|
|
return;
|
|
}
|
|
if (message != null) {
|
|
if (messageType != MessageType.Info) {
|
|
using(new EditorGUILayout.HorizontalScope()) {
|
|
EditorGUILayout.HelpBox(message, messageType);
|
|
var style = HotReloadWindowStyles.HideButtonStyle;
|
|
if (Event.current.type == EventType.Repaint) {
|
|
style.fixedHeight = GUILayoutUtility.GetLastRect().height;
|
|
}
|
|
if (allowHide) {
|
|
if (GUILayout.Button("Hide", style)) {
|
|
HotReloadPrefs.ErrorHidden = true;
|
|
}
|
|
}
|
|
}
|
|
} else if (content != null) {
|
|
EditorGUILayout.LabelField(content);
|
|
EditorGUILayout.Space();
|
|
} else {
|
|
EditorGUILayout.LabelField(message);
|
|
EditorGUILayout.Space();
|
|
}
|
|
customGUI?.Invoke();
|
|
}
|
|
}
|
|
|
|
const string assetStoreProInfo = "Unity Pro/Enterprise users from company with your number of employees require a Business license. Please upgrade your license on our website.";
|
|
internal static void RenderBusinessLicenseInfo(GUIStyle style) {
|
|
GUILayout.Space(8);
|
|
using (new EditorGUILayout.HorizontalScope()) {
|
|
EditorGUILayout.HelpBox(assetStoreProInfo, MessageType.Info);
|
|
if (Event.current.type == EventType.Repaint) {
|
|
style.fixedHeight = GUILayoutUtility.GetLastRect().height;
|
|
}
|
|
if (GUILayout.Button("Upgrade", style)) {
|
|
Application.OpenURL(Constants.ProductPurchaseBusinessURL);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static void RenderIndieLicenseInfo(GUIStyle style) {
|
|
string message;
|
|
if (EditorCodePatcher.licenseType == UnityLicenseType.UnityPersonalPlus) {
|
|
message = "Unity Plus users require an Indie license. Please upgrade your license on our website.";
|
|
} else if (EditorCodePatcher.licenseType == UnityLicenseType.UnityPro) {
|
|
message = "Unity Pro/Enterprise users from company with your number of employees require an Indie license. Please upgrade your license on our website.";
|
|
} else {
|
|
return;
|
|
}
|
|
GUILayout.Space(8);
|
|
using (new EditorGUILayout.HorizontalScope()) {
|
|
EditorGUILayout.HelpBox(message, MessageType.Info);
|
|
if (Event.current.type == EventType.Repaint) {
|
|
style.fixedHeight = GUILayoutUtility.GetLastRect().height;
|
|
}
|
|
if (GUILayout.Button("Upgrade", style)) {
|
|
Application.OpenURL(Constants.ProductPurchaseURL);
|
|
}
|
|
}
|
|
}
|
|
|
|
const string GetLicense = "Get License";
|
|
const string ContactSupport = "Contact Support";
|
|
const string UpgradeLicense = "Upgrade License";
|
|
const string ManageLicense = "Manage License";
|
|
internal static Dictionary<string, LicenseErrorData> _licenseErrorData;
|
|
internal static Dictionary<string, LicenseErrorData> LicenseErrorData => _licenseErrorData ?? (_licenseErrorData = new Dictionary<string, LicenseErrorData> {
|
|
{ "DeviceNotLicensedException", new LicenseErrorData(description: "Another device is using your license. Please reach out to customer support for assistance.", showSupportButton: true, supportButtonText: ContactSupport) },
|
|
{ "DeviceBlacklistedException", new LicenseErrorData(description: "You device has been blacklisted.") },
|
|
{ "DateHeaderInvalidException", new LicenseErrorData(description: $"Your license is not working because your computer's clock is incorrect. Please set the clock to the correct time to restore your license.") },
|
|
{ "DateTimeCheatingException", new LicenseErrorData(description: $"Your license is not working because your computer's clock is incorrect. Please set the clock to the correct time to restore your license.") },
|
|
{ "LicenseActivationException", new LicenseErrorData(description: "An error has occured while activating your license. Please contact customer support for assistance.", showSupportButton: true, supportButtonText: ContactSupport) },
|
|
{ "LicenseDeletedException", new LicenseErrorData(description: $"Your license has been deleted. Please contact customer support for assistance.", showBuyButton: true, buyButtonText: GetLicense, showSupportButton: true, supportButtonText: ContactSupport) },
|
|
{ "LicenseDisabledException", new LicenseErrorData(description: $"Your license has been disabled. Please contact customer support for assistance.", showBuyButton: true, buyButtonText: GetLicense, showSupportButton: true, supportButtonText: ContactSupport) },
|
|
{ "LicenseExpiredException", new LicenseErrorData(description: $"Your license has expired. Please renew your license subscription using the 'Upgrade License' button below and login with your email/password to activate your license.", showBuyButton: true, buyButtonText: UpgradeLicense, showManageLicenseButton: true, manageLicenseButtonText: ManageLicense) },
|
|
{ "LicenseInactiveException", new LicenseErrorData(description: $"Your license is currenty inactive. Please login with your email/password to activate your license.") },
|
|
{ "LocalLicenseException", new LicenseErrorData(description: $"Your license file was damaged or corrupted. Please login with your email/password to refresh your license file.") },
|
|
// Note: obsolete
|
|
{ "MissingParametersException", new LicenseErrorData(description: "An account already exists for this device. Please login with your existing email/password.", showBuyButton: true, buyButtonText: GetLicense) },
|
|
{ "NetworkException", new LicenseErrorData(description: "There is an issue connecting to our servers. Please check your internet connection or contact customer support if the issue persists.", showSupportButton: true, supportButtonText: ContactSupport) },
|
|
{ "TrialLicenseExpiredException", new LicenseErrorData(description: $"Your trial has expired. Activate a license with unlimited usage or continue using the Free version. View available plans on our website.", showBuyButton: true, buyButtonText: UpgradeLicense) },
|
|
{ "InvalidCredentialException", new LicenseErrorData(description: "Incorrect email/password. You can find your initial password in the sign-up email.") },
|
|
// Note: activating free trial with email is not supported anymore. This error shouldn't happen which is why we should rather user the fallback
|
|
// { "LicenseNotFoundException", new LicenseErrorData(description: "The account you're trying to access doesn't seem to exist yet. Please enter your email address to create a new account and receive a trial license.", showLoginButton: true, loginButtonText: CreateAccount) },
|
|
{ "LicenseIncompatibleException", new LicenseErrorData(description: "Please upgrade your license to continue using hotreload with Unity Pro.", showManageLicenseButton: true, manageLicenseButtonText: ManageLicense) },
|
|
});
|
|
internal static LicenseErrorData defaultLicenseErrorData = new LicenseErrorData(description: "We apologize, an error happened while verifying your license. Please reach out to customer support for assistance.", showSupportButton: true, supportButtonText: ContactSupport);
|
|
|
|
internal static string GetMessageFromError(HotReloadRunTabState currentState, string error) {
|
|
if (PackageConst.IsAssetStoreBuild && error == "TrialLicenseExpiredException") {
|
|
return assetStoreProInfo;
|
|
}
|
|
return GetLicenseErrorDataOrDefault(currentState, error).description;
|
|
}
|
|
|
|
internal static LicenseErrorData GetLicenseErrorDataOrDefault(HotReloadRunTabState currentState, string error) {
|
|
if (currentState.loginStatus?.isFree == true) {
|
|
return default(LicenseErrorData);
|
|
}
|
|
if (currentState.loginStatus == null || string.IsNullOrEmpty(error) && (!currentState.loginStatus.isLicensed || currentState.loginStatus.isTrial)) {
|
|
return new LicenseErrorData(null, showBuyButton: true, buyButtonText: GetLicense);
|
|
}
|
|
if (string.IsNullOrEmpty(error)) {
|
|
return default(LicenseErrorData);
|
|
}
|
|
if (!LicenseErrorData.ContainsKey(error)) {
|
|
return defaultLicenseErrorData;
|
|
}
|
|
return LicenseErrorData[error];
|
|
}
|
|
|
|
internal static void RenderBuyLicenseButton(string buyLicenseButton) {
|
|
OpenURLButton.Render(buyLicenseButton, Constants.ProductPurchaseURL);
|
|
}
|
|
|
|
static void RenderLicenseActionButtons(HotReloadRunTabState currentState) {
|
|
var errInfo = GetLicenseErrorDataOrDefault(currentState, currentState.loginStatus?.lastLicenseError);
|
|
if (errInfo.showBuyButton || errInfo.showManageLicenseButton) {
|
|
using(new EditorGUILayout.HorizontalScope()) {
|
|
if (errInfo.showBuyButton) {
|
|
RenderBuyLicenseButton(errInfo.buyButtonText);
|
|
}
|
|
if (errInfo.showManageLicenseButton && !HotReloadPrefs.ErrorHidden) {
|
|
OpenURLButton.Render(errInfo.manageLicenseButtonText, Constants.ManageLicenseURL);
|
|
}
|
|
}
|
|
}
|
|
if (errInfo.showLoginButton && GUILayout.Button(errInfo.loginButtonText, openSettingsStyle)) {
|
|
// show license section
|
|
HotReloadWindow.Current.SelectTab(typeof(HotReloadSettingsTab));
|
|
HotReloadWindow.Current.SettingsTab.FocusLicenseFoldout();
|
|
}
|
|
if (errInfo.showSupportButton && !HotReloadPrefs.ErrorHidden) {
|
|
OpenURLButton.Render(errInfo.supportButtonText, Constants.ContactURL);
|
|
}
|
|
if (currentState.loginStatus?.lastLicenseError != null) {
|
|
HotReloadAboutTab.reportIssueButton.OnGUI();
|
|
}
|
|
}
|
|
|
|
internal static void RenderLicenseInfo(HotReloadRunTabState currentState, LoginStatusResponse loginStatus, bool verbose = false, bool allowHide = true, string overrideActionButton = null, bool showConsumptions = false) {
|
|
HotReloadPrefs.ShowLogin = EditorGUILayout.Foldout(HotReloadPrefs.ShowLogin, "Hot Reload License", true, HotReloadWindowStyles.FoldoutStyle);
|
|
if (HotReloadPrefs.ShowLogin) {
|
|
EditorGUILayout.Space();
|
|
if ((loginStatus?.isLicensed != true && showConsumptions) && !(loginStatus == null || loginStatus.isFree)) {
|
|
RenderConsumption(loginStatus);
|
|
}
|
|
RenderLicenseStatusInfo(currentState, loginStatus: loginStatus, allowHide: allowHide, verbose: verbose);
|
|
|
|
RenderLicenseInnerPanel(currentState, overrideActionButton: overrideActionButton);
|
|
|
|
EditorGUILayout.Space();
|
|
EditorGUILayout.Space();
|
|
}
|
|
}
|
|
|
|
internal void RenderPromoCodes() {
|
|
HotReloadPrefs.ShowPromoCodes = EditorGUILayout.Foldout(HotReloadPrefs.ShowPromoCodes, "Promo Codes", true, HotReloadWindowStyles.FoldoutStyle);
|
|
if (!HotReloadPrefs.ShowPromoCodes) {
|
|
return;
|
|
}
|
|
if (promoCodeActivatedThisSession) {
|
|
EditorGUILayout.HelpBox($"Your promo code has been successfully activated. Free trial has been extended by 3 months.", MessageType.Info);
|
|
} else {
|
|
if (promoCodeError != null && promoCodeErrorType != MessageType.None) {
|
|
EditorGUILayout.HelpBox(promoCodeError, promoCodeErrorType);
|
|
}
|
|
EditorGUILayout.LabelField("Promo code");
|
|
_pendingPromoCode = EditorGUILayout.TextField(_pendingPromoCode);
|
|
EditorGUILayout.Space();
|
|
|
|
using (new EditorGUI.DisabledScope(_requestingActivatePromoCode)) {
|
|
if (GUILayout.Button("Activate promo code", HotReloadRunTab.bigButtonHeight)) {
|
|
RequestActivatePromoCode().Forget();
|
|
}
|
|
}
|
|
}
|
|
|
|
EditorGUILayout.Space();
|
|
EditorGUILayout.Space();
|
|
}
|
|
|
|
private async Task RequestActivatePromoCode() {
|
|
_requestingActivatePromoCode = true;
|
|
try {
|
|
var resp = await RequestHelper.RequestActivatePromoCode(_pendingPromoCode);
|
|
if (resp != null && resp.error == null) {
|
|
promoCodeActivatedThisSession = true;
|
|
} else {
|
|
var requestError = resp?.error ?? "Network error";
|
|
var errorType = ToErrorType(requestError);
|
|
promoCodeError = ToPrettyErrorMessage(errorType);
|
|
promoCodeErrorType = ToMessageType(errorType);
|
|
}
|
|
} finally {
|
|
_requestingActivatePromoCode = false;
|
|
}
|
|
}
|
|
|
|
PromoCodeErrorType ToErrorType(string error) {
|
|
switch (error) {
|
|
case "Input is missing": return PromoCodeErrorType.MISSING_INPUT;
|
|
case "only POST is supported": return PromoCodeErrorType.INVALID_HTTP_METHOD;
|
|
case "body is not a valid json": return PromoCodeErrorType.BODY_INVALID;
|
|
case "Promo code is not found": return PromoCodeErrorType.PROMO_CODE_NOT_FOUND;
|
|
case "Promo code already claimed": return PromoCodeErrorType.PROMO_CODE_CLAIMED;
|
|
case "Promo code expired": return PromoCodeErrorType.PROMO_CODE_EXPIRED;
|
|
case "License not found": return PromoCodeErrorType.LICENSE_NOT_FOUND;
|
|
case "License is not a trial": return PromoCodeErrorType.LICENSE_NOT_TRIAL;
|
|
case "License already extended": return PromoCodeErrorType.LICENSE_ALREADY_EXTENDED;
|
|
case "conditionalCheckFailed": return PromoCodeErrorType.CONDITIONAL_CHECK_FAILED;
|
|
}
|
|
if (error.Contains("Updating License Failed with error")) {
|
|
return PromoCodeErrorType.UPDATING_LICENSE_FAILED;
|
|
} else if (error.Contains("Unknown exception")) {
|
|
return PromoCodeErrorType.UNKNOWN_EXCEPTION;
|
|
} else if (error.Contains("Unsupported path")) {
|
|
return PromoCodeErrorType.UNSUPPORTED_PATH;
|
|
}
|
|
return PromoCodeErrorType.NONE;
|
|
}
|
|
|
|
string ToPrettyErrorMessage(PromoCodeErrorType errorType) {
|
|
var defaultMsg = "We apologize, an error happened while activating your promo code. Please reach out to customer support for assistance.";
|
|
switch (errorType) {
|
|
case PromoCodeErrorType.MISSING_INPUT:
|
|
case PromoCodeErrorType.INVALID_HTTP_METHOD:
|
|
case PromoCodeErrorType.BODY_INVALID:
|
|
case PromoCodeErrorType.UNKNOWN_EXCEPTION:
|
|
case PromoCodeErrorType.UNSUPPORTED_PATH:
|
|
case PromoCodeErrorType.LICENSE_NOT_FOUND:
|
|
case PromoCodeErrorType.UPDATING_LICENSE_FAILED:
|
|
case PromoCodeErrorType.LICENSE_NOT_TRIAL:
|
|
return defaultMsg;
|
|
case PromoCodeErrorType.PROMO_CODE_NOT_FOUND: return "Your promo code is invalid. Please ensure that you have entered the correct promo code.";
|
|
case PromoCodeErrorType.PROMO_CODE_CLAIMED: return "Your promo code has already been used.";
|
|
case PromoCodeErrorType.PROMO_CODE_EXPIRED: return "Your promo code has expired.";
|
|
case PromoCodeErrorType.LICENSE_ALREADY_EXTENDED: return "Your license has already been activated with a promo code. Only one promo code activation per license is allowed.";
|
|
case PromoCodeErrorType.CONDITIONAL_CHECK_FAILED: return "We encountered an error while activating your promo code. Please try again. If the issue persists, please contact our customer support team for assistance.";
|
|
case PromoCodeErrorType.NONE: return "There is an issue connecting to our servers. Please check your internet connection or contact customer support if the issue persists.";
|
|
default: return defaultMsg;
|
|
}
|
|
}
|
|
|
|
MessageType ToMessageType(PromoCodeErrorType errorType) {
|
|
switch (errorType) {
|
|
case PromoCodeErrorType.MISSING_INPUT: return MessageType.Error;
|
|
case PromoCodeErrorType.INVALID_HTTP_METHOD: return MessageType.Error;
|
|
case PromoCodeErrorType.BODY_INVALID: return MessageType.Error;
|
|
case PromoCodeErrorType.PROMO_CODE_NOT_FOUND: return MessageType.Warning;
|
|
case PromoCodeErrorType.PROMO_CODE_CLAIMED: return MessageType.Warning;
|
|
case PromoCodeErrorType.PROMO_CODE_EXPIRED: return MessageType.Warning;
|
|
case PromoCodeErrorType.LICENSE_NOT_FOUND: return MessageType.Error;
|
|
case PromoCodeErrorType.LICENSE_NOT_TRIAL: return MessageType.Error;
|
|
case PromoCodeErrorType.LICENSE_ALREADY_EXTENDED: return MessageType.Warning;
|
|
case PromoCodeErrorType.UPDATING_LICENSE_FAILED: return MessageType.Error;
|
|
case PromoCodeErrorType.CONDITIONAL_CHECK_FAILED: return MessageType.Error;
|
|
case PromoCodeErrorType.UNKNOWN_EXCEPTION: return MessageType.Error;
|
|
case PromoCodeErrorType.UNSUPPORTED_PATH: return MessageType.Error;
|
|
case PromoCodeErrorType.NONE: return MessageType.Error;
|
|
default: return MessageType.Error;
|
|
}
|
|
}
|
|
|
|
public static void RenderLicenseButtons(HotReloadRunTabState currentState) {
|
|
RenderLicenseActionButtons(currentState);
|
|
}
|
|
|
|
internal static void RenderLicenseInnerPanel(HotReloadRunTabState currentState, string overrideActionButton = null, bool renderLogout = true) {
|
|
EditorGUILayout.LabelField("Email");
|
|
GUI.SetNextControlName("email");
|
|
_pendingEmail = EditorGUILayout.TextField(string.IsNullOrEmpty(_pendingEmail) ? HotReloadPrefs.LicenseEmail : _pendingEmail);
|
|
_pendingEmail = _pendingEmail.Trim();
|
|
|
|
EditorGUILayout.LabelField("Password");
|
|
GUI.SetNextControlName("password");
|
|
_pendingPassword = EditorGUILayout.PasswordField(string.IsNullOrEmpty(_pendingPassword) ? HotReloadPrefs.LicensePassword : _pendingPassword);
|
|
|
|
RenderSwitchAuthMode();
|
|
|
|
var e = Event.current;
|
|
using(new EditorGUI.DisabledScope(currentState.requestingLoginInfo)) {
|
|
var btnLabel = overrideActionButton;
|
|
if (String.IsNullOrEmpty(overrideActionButton)) {
|
|
btnLabel = "Login";
|
|
}
|
|
using (new EditorGUILayout.HorizontalScope()) {
|
|
var focusedControl = GUI.GetNameOfFocusedControl();
|
|
if (GUILayout.Button(btnLabel, bigButtonHeight)
|
|
|| (focusedControl == "email"
|
|
|| focusedControl == "password")
|
|
&& e.type == EventType.KeyUp
|
|
&& (e.keyCode == KeyCode.Return
|
|
|| e.keyCode == KeyCode.KeypadEnter)
|
|
) {
|
|
var error = ValidateEmail(_pendingEmail);
|
|
if (!string.IsNullOrEmpty(error)) {
|
|
_activateInfoMessage = new Tuple<string, MessageType>(error, MessageType.Warning);
|
|
} else if (string.IsNullOrEmpty(_pendingPassword)) {
|
|
_activateInfoMessage = new Tuple<string, MessageType>("Please enter your password.", MessageType.Warning);
|
|
} else {
|
|
HotReloadWindow.Current.SelectTab(typeof(HotReloadRunTab));
|
|
|
|
_activateInfoMessage = null;
|
|
if (RedeemLicenseHelper.I.RedeemStage == RedeemStage.Login) {
|
|
RedeemLicenseHelper.I.FinishRegistration(RegistrationOutcome.Indie);
|
|
}
|
|
if (!EditorCodePatcher.RequestingDownloadAndRun && !EditorCodePatcher.Running) {
|
|
LoginOnDownloadAndRun(new LoginData(email: _pendingEmail, password: _pendingPassword)).Forget();
|
|
} else {
|
|
EditorCodePatcher.RequestLogin(_pendingEmail, _pendingPassword).Forget();
|
|
}
|
|
}
|
|
}
|
|
if (renderLogout) {
|
|
RenderLogout(currentState);
|
|
}
|
|
}
|
|
}
|
|
if (_activateInfoMessage != null && (e.type == EventType.Layout || e.type == EventType.Repaint)) {
|
|
EditorGUILayout.HelpBox(_activateInfoMessage.Item1, _activateInfoMessage.Item2);
|
|
}
|
|
}
|
|
|
|
public static string ValidateEmail(string email) {
|
|
if (string.IsNullOrEmpty(email)) {
|
|
return "Please enter your email address.";
|
|
} else if (!EditorWindowHelper.IsValidEmailAddress(email)) {
|
|
return "Please enter a valid email address.";
|
|
} else if (email.Contains("+")) {
|
|
return "Mail extensions (in a form of 'username+suffix@example.com') are not supported yet. Please provide your original email address (such as 'username@example.com' without '+suffix' part) as we're working on resolving this issue.";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public static void RenderLogout(HotReloadRunTabState currentState) {
|
|
if (currentState.loginStatus?.isLicensed != true) {
|
|
return;
|
|
}
|
|
if (GUILayout.Button("Logout", bigButtonHeight)) {
|
|
HotReloadWindow.Current.SelectTab(typeof(HotReloadRunTab));
|
|
if (!EditorCodePatcher.RequestingDownloadAndRun && !EditorCodePatcher.Running) {
|
|
LogoutOnDownloadAndRun().Forget();
|
|
} else {
|
|
RequestLogout().Forget();
|
|
}
|
|
}
|
|
}
|
|
|
|
async static Task LoginOnDownloadAndRun(LoginData loginData = null) {
|
|
var ok = await EditorCodePatcher.DownloadAndRun(loginData);
|
|
if (ok && loginData != null) {
|
|
HotReloadPrefs.ErrorHidden = false;
|
|
HotReloadPrefs.LicenseEmail = loginData.email;
|
|
HotReloadPrefs.LicensePassword = loginData.password;
|
|
}
|
|
}
|
|
|
|
async static Task LogoutOnDownloadAndRun() {
|
|
var ok = await EditorCodePatcher.DownloadAndRun();
|
|
if (!ok) {
|
|
return;
|
|
}
|
|
await RequestLogout();
|
|
}
|
|
|
|
private async static Task RequestLogout() {
|
|
int i = 0;
|
|
while (!EditorCodePatcher.Running && i < 100) {
|
|
await Task.Delay(100);
|
|
i++;
|
|
}
|
|
var resp = await RequestHelper.RequestLogout();
|
|
if (!EditorCodePatcher.RequestingLoginInfo && resp != null) {
|
|
EditorCodePatcher.HandleStatus(resp);
|
|
}
|
|
}
|
|
|
|
private static void RenderSwitchAuthMode() {
|
|
var color = EditorGUIUtility.isProSkin ? new Color32(0x3F, 0x9F, 0xFF, 0xFF) : new Color32(0x0F, 0x52, 0xD7, 0xFF);
|
|
if (HotReloadGUIHelper.LinkLabel("Forgot password?", 12, FontStyle.Normal, TextAnchor.MiddleLeft, color)) {
|
|
if (EditorUtility.DisplayDialog("Recover password", "Use company code 'naughtycult' and the email you signed up with in order to recover your account.", "Open in browser", "Cancel")) {
|
|
Application.OpenURL(Constants.ForgotPasswordURL);
|
|
}
|
|
}
|
|
}
|
|
|
|
Texture2D _greenTextureLight;
|
|
Texture2D _greenTextureDark;
|
|
Texture2D GreenTexture => EditorGUIUtility.isProSkin
|
|
? _greenTextureDark ? _greenTextureDark : (_greenTextureDark = MakeTexture(0.5f))
|
|
: _greenTextureLight ? _greenTextureLight : (_greenTextureLight = MakeTexture(0.85f));
|
|
|
|
private void RenderProgressBar() {
|
|
if (currentState.downloadRequired && !currentState.downloadStarted) {
|
|
return;
|
|
}
|
|
|
|
using(var scope = new EditorGUILayout.VerticalScope(HotReloadWindowStyles.MiddleCenterStyle)) {
|
|
float progress;
|
|
var bg = HotReloadWindowStyles.ProgressBarBarStyle.normal.background;
|
|
try {
|
|
HotReloadWindowStyles.ProgressBarBarStyle.normal.background = GreenTexture;
|
|
var barRect = scope.rect;
|
|
|
|
barRect.height = 25;
|
|
if (currentState.downloadRequired) {
|
|
barRect.width = barRect.width - 65;
|
|
using (new EditorGUILayout.HorizontalScope()) {
|
|
progress = EditorCodePatcher.DownloadProgress;
|
|
EditorGUI.ProgressBar(barRect, Mathf.Clamp(progress, 0f, 1f), "");
|
|
if (GUI.Button(new Rect(barRect) { x = barRect.x + barRect.width + 5, height = barRect.height, width = 60 }, new GUIContent(" Info", GUIHelper.GetLocalIcon("alert_info")))) {
|
|
Application.OpenURL(Constants.AdditionalContentURL);
|
|
}
|
|
}
|
|
} else {
|
|
progress = EditorCodePatcher.Stopping ? 1 : Mathf.Clamp(EditorCodePatcher.StartupProgress?.Item1 ?? 0f, 0f, 1f);
|
|
EditorGUI.ProgressBar(barRect, progress, "");
|
|
}
|
|
GUILayout.Space(barRect.height);
|
|
} finally {
|
|
HotReloadWindowStyles.ProgressBarBarStyle.normal.background = bg;
|
|
}
|
|
}
|
|
}
|
|
|
|
private Texture2D MakeTexture(float maxHue) {
|
|
var width = 11;
|
|
var height = 11;
|
|
Color[] pix = new Color[width * height];
|
|
for (int y = 0; y < height; y++) {
|
|
var middle = Math.Ceiling(height / (double)2);
|
|
var maxGreen = maxHue;
|
|
var yCoord = y + 1;
|
|
var green = maxGreen - Math.Abs(yCoord - middle) * 0.02;
|
|
for (int x = 0; x < width; x++) {
|
|
pix[y * width + x] = new Color(0.1f, (float)green, 0.1f, 1.0f);
|
|
}
|
|
}
|
|
var result = new Texture2D(width, height);
|
|
result.SetPixels(pix);
|
|
result.Apply();
|
|
return result;
|
|
}
|
|
|
|
|
|
/*
|
|
[MenuItem("codepatcher/restart")]
|
|
public static void TestRestart() {
|
|
CodePatcherCLI.Restart(Application.dataPath, false);
|
|
}
|
|
*/
|
|
|
|
}
|
|
|
|
internal static class HotReloadGUIHelper {
|
|
public static bool LinkLabel(string labelText, int fontSize, FontStyle fontStyle, TextAnchor alignment, Color? color = null) {
|
|
var stl = EditorStyles.label;
|
|
|
|
// copy
|
|
var origSize = stl.fontSize;
|
|
var origStyle = stl.fontStyle;
|
|
var origAnchor = stl.alignment;
|
|
var origColor = stl.normal.textColor;
|
|
|
|
// temporarily modify the built-in style
|
|
stl.fontSize = fontSize;
|
|
stl.fontStyle = fontStyle;
|
|
stl.alignment = alignment;
|
|
stl.normal.textColor = color ?? origColor;
|
|
stl.active.textColor = color ?? origColor;
|
|
stl.focused.textColor = color ?? origColor;
|
|
stl.hover.textColor = color ?? origColor;
|
|
|
|
try {
|
|
return GUILayout.Button(labelText, stl);
|
|
} finally{
|
|
// set the editor style (stl) back to normal
|
|
stl.fontSize = origSize;
|
|
stl.fontStyle = origStyle;
|
|
stl.alignment = origAnchor;
|
|
stl.normal.textColor = origColor;
|
|
stl.active.textColor = origColor;
|
|
stl.focused.textColor = origColor;
|
|
stl.hover.textColor = origColor;
|
|
}
|
|
}
|
|
|
|
public static void HelpBox(string message, MessageType type, int fontSize) {
|
|
var _fontSize = EditorStyles.helpBox.fontSize;
|
|
try {
|
|
EditorStyles.helpBox.fontSize = fontSize;
|
|
EditorGUILayout.HelpBox(message, type);
|
|
} finally {
|
|
EditorStyles.helpBox.fontSize = _fontSize;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal enum PromoCodeErrorType {
|
|
NONE,
|
|
MISSING_INPUT,
|
|
INVALID_HTTP_METHOD,
|
|
BODY_INVALID,
|
|
PROMO_CODE_NOT_FOUND,
|
|
PROMO_CODE_CLAIMED,
|
|
PROMO_CODE_EXPIRED,
|
|
LICENSE_NOT_FOUND,
|
|
LICENSE_NOT_TRIAL,
|
|
LICENSE_ALREADY_EXTENDED,
|
|
UPDATING_LICENSE_FAILED,
|
|
CONDITIONAL_CHECK_FAILED,
|
|
UNKNOWN_EXCEPTION,
|
|
UNSUPPORTED_PATH,
|
|
}
|
|
|
|
internal class LoginData {
|
|
public readonly string email;
|
|
public readonly string password;
|
|
|
|
public LoginData(string email, string password) {
|
|
this.email = email;
|
|
this.password = password;
|
|
}
|
|
}
|
|
}
|
|
|