using Client.ConsoleForms; using Client.ConsoleForms.Events; using Client.ConsoleForms.Graphics; using Client.ConsoleForms.Parameters; using Client.Properties; using ConsoleForms; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Tofvesson.Collections; using Tofvesson.Common; using Tofvesson.Crypto; namespace Client { public sealed class SessionContext : Context { private readonly BankNetInteractor interactor; private bool scheduleDestroy; private Promise userDataGetter; private Promise accountsGetter; private List<string> accounts = null; private string username; private bool isAdministrator = false; // Transient data private int accountType = -1; private string acc1 = null, acc2 = null, user = null; // Stores personal accounts private readonly FixedQueue<Tuple<string, Account>> accountDataCache = new FixedQueue<Tuple<string, Account>>(64); // Stores remote account data private readonly FixedQueue<Tuple<string, string>> remoteUserCache = new FixedQueue<Tuple<string, string>>(8); private bool accountChange = false; // XML-generated views private ListView options; private ButtonView options_exit; private ButtonView options_view; private ButtonView options_delete; private ButtonView options_tx; private ButtonView options_update; private ButtonView options_add; private InputView password_update; private InputView transfer; private DialogView exit_prompt; private DialogView account_delete; private InputView account_create; // Synthetic private ListView accountTypes; // Deprecated private DialogView success; public SessionContext(ContextManager manager, BankNetInteractor interactor) : base(manager, "Session", "Common") { this.interactor = interactor; scheduleDestroy = !interactor.IsLoggedIn; if (!scheduleDestroy) { RegisterAutoHide("account_create", "account_info", "password_update", "exit_prompt", "account_show", "transfer"); // XML-generated views options = GetView<ListView>("menu_options"); options_exit = options.GetView<ButtonView>("exit"); options_view = options.GetView<ButtonView>("view"); options_delete = options.GetView<ButtonView>("delete"); options_tx = options.GetView<ButtonView>("tx"); options_update = options.GetView<ButtonView>("update"); options_add = options.GetView<ButtonView>("add"); exit_prompt = GetView<DialogView>("exit_prompt"); password_update = GetView<InputView>("password_update"); transfer = GetView<InputView>("transfer"); account_delete = GetView<DialogView>("account_delete"); account_create = GetView<InputView>("account_create"); success = GetView<DialogView>("Success"); // Synthetic views accountTypes = GenerateList( new string[] { GetIntlString("SE_acc_checking"), GetIntlString("SE_acc_saving") }, v => { accountType = accountTypes.SelectedView; account_create.Inputs[1].Text = (v as ButtonView).Text; CreateAccount(); }, true); // Run setup SetupBackEvents(); SetupHideEvents(); SetupInputEvents(); SetupSubmissionEvents(); SetupDefaultViewStates(); // We have a valid context! RefreshUserInfo(); // Get user info RefreshAccountList(); // Get account list for user } } private void SetupInputEvents() { transfer.InputListener = (v, i, s, t) => { if (t) return false; // Don't handle artificial events i.BackgroundColor = v.DefaultBackgroundColor; i.SelectBackgroundColor = v.DefaultSelectBackgroundColor; if (v.IndexOf(i) < 3) { // Trigger a keypress event for key [ENTER] v.TriggerKeyEvent(new ConsoleController.KeyEvent(new ConsoleKeyInfo('\n', ConsoleKey.Enter, false, false, false))); return false; // Don't update input } return true; }; account_create.InputListener = (v, c, i, t) => { c.BackgroundColor = v.DefaultBackgroundColor; c.SelectBackgroundColor = v.DefaultSelectBackgroundColor; if (v.IndexOf(c) == 1) { Show(accountTypes); return false; // Don't process key event } return true; }; // Set up a listener to reset color scheme password_update.InputListener = (v, c, i, t) => { c.BackgroundColor = v.DefaultBackgroundColor; c.SelectBackgroundColor = v.DefaultSelectBackgroundColor; return true; }; } private void ResetInputView(View v) => ResetInputView(v as InputView); private void ResetInputView(InputView i) { i.SelectedField = 0; foreach (var inputField in i.Inputs) { inputField.Text = ""; inputField.BackgroundColor = i.DefaultBackgroundColor; inputField.SelectBackgroundColor = i.DefaultSelectBackgroundColor; inputField.SelectIndex = 0; inputField.RenderStart = 0; } } private void ResetDialogView(View d) => ResetDialogView(d as DialogView); private void ResetDialogView(DialogView d) => d.Select = 0; private void ResetListView(View d) => ResetListView(d as ListView); private void ResetListView(ListView d) => d.SelectedView = 0; private void SetupHideEvents() { password_update.OnClose = ResetInputView; transfer.OnClose = _ => { ResetInputView(transfer); transfer.Inputs[0].Text = GetIntlString("SE_account_select"); transfer.Inputs[1].Text = GetIntlString("SE_user_select"); transfer.Inputs[2].Text = GetIntlString("SE_account_select"); acc1 = null; acc2 = null; user = null; }; options.OnClose = ResetListView; account_delete.OnClose = ResetDialogView; account_create.OnClose = _ => { ResetInputView(account_create); account_create.Inputs[1].Text = GetIntlString("SE_acc_sel"); accountType = -1; }; exit_prompt.OnClose = ResetDialogView; } private void SetupBackEvents() { options.OnBackEvent = v => Show("exit_prompt"); } private void SetupDefaultViewStates() { account_create.OnClose(account_create); transfer.OnClose(transfer); } private void SetupSubmissionEvents() { // Setup options menu events options_add.SetEvent(_ => Show(account_create)); options_update.SetEvent(v => Show(password_update)); options_tx.SetEvent(v => Show(transfer)); options_delete.SetEvent(v => Show(account_delete)); // Other events account_delete.RegisterSelectListener((v, i, s) => { Hide(v); if (i == 1) { Show("delete_stall"); Promise deletion = Promise.AwaitPromise(interactor.DeleteUser()); deletion.Subscribe = p => { Hide("delete_stall"); if (bool.Parse(p.Value)) controller.Popup(GetIntlString("SE_delete_success"), 2500, ConsoleColor.Green, () => manager.LoadContext(new WelcomeContext(manager, interactor))); else controller.Popup(GetIntlString("SE_delete_failure"), 1500, ConsoleColor.Red); }; } }); account_create.SubmissionsListener = inputView => { if (inputView.SelectedField == 1) Show(accountTypes); // Show account type selection menu else CreateAccount(); }; success.RegisterSelectListener((v, i, s) => HandleLogout()); options_exit.SetEvent(v => Show("exit_prompt")); options_view.SetEvent(v => { if (accountChange) RefreshAccountList(); if (!accountsGetter.HasValue) Show("data_fetch"); accountsGetter.Subscribe = p => { accountsGetter.Unsubscribe(); Hide("data_fetch"); Show(GenerateList(p.Value.Split('&').ForEach(Support.FromBase64String), ViewAccountListener)); }; }); password_update.SubmissionsListener = v => { bool hasError = v.Inputs[0].Text.Length == 0; if (hasError) { // Notify user, as well as mark the errant input field v.Inputs[0].SelectBackgroundColor = ConsoleColor.Red; v.Inputs[0].BackgroundColor = ConsoleColor.DarkRed; controller.Popup(GetIntlString("ERR_empty"), 3000, ConsoleColor.Red); } if (v.Inputs[1].Text.Length == 0) { v.Inputs[1].SelectBackgroundColor = ConsoleColor.Red; v.Inputs[1].BackgroundColor = ConsoleColor.DarkRed; if (!hasError) controller.Popup(GetIntlString("ERR_empty"), 3000, ConsoleColor.Red); return; // No need to continue, we have notified the user. There is no valid information to operate on past this point } if (!v.Inputs[0].Text.Equals(v.Inputs[1].Text)) { controller.Popup(GetIntlString("SU_mismatch"), 3000, ConsoleColor.Red); return; } Show("update_stall"); Task<Promise> t = interactor.UpdatePassword(v.Inputs[0].Text); Promise.AwaitPromise(t).Subscribe = p => { Hide("update_stall"); Hide("password_update"); v.Inputs[0].ClearText(); v.Inputs[1].ClearText(); v.SelectedField = 0; }; }; transfer.SubmissionsListener = v => { switch (v.SelectedField) { case 0: if (accountChange) accountsGetter = Promise.AwaitPromise(interactor.ListUserAccounts()); Show("data_fetch"); accountsGetter.Subscribe = p => { accountsGetter.Unsubscribe(); Hide("data_fetch"); Show(GenerateList(p.Value.Split('&').ForEach(Support.FromBase64String), sel => v.Inputs[0].Text = acc1 = (sel as ButtonView).Text, true)); }; break; case 1: Show("data_fetch"); Promise remoteUserGetter = Promise.AwaitPromise(interactor.ListUsers()); remoteUserGetter.Subscribe = p => { remoteUserGetter.Unsubscribe(); Hide("data_fetch"); Show(GenerateList(p.Value.Split('&').ForEach(Support.FromBase64String), sel => v.Inputs[1].Text = user = (sel as ButtonView).Text, true)); }; break; case 2: if (user == null) controller.Popup(GetIntlString("SE_user_noselect"), 2000, ConsoleColor.Red); else { Show("data_fetch"); Promise remoteAccountsGetter = Promise.AwaitPromise(interactor.ListAccounts(user)); remoteAccountsGetter.Subscribe = p => { remoteAccountsGetter.Unsubscribe(); Hide("data_fetch"); Show(GenerateList(p.Value.Split('&').ForEach(Support.FromBase64String), sel => v.Inputs[2].Text = acc2 = (sel as ButtonView).Text, true)); }; } break; case 3: case 4: Show("verify_stall"); bool error = false; if (acc1 == null) { controller.Popup(GetIntlString("SE_account_noselect"), 1500, ConsoleColor.Red); error = true; v.Inputs[0].BackgroundColor = ConsoleColor.Red; v.Inputs[0].SelectBackgroundColor = ConsoleColor.DarkRed; } if (acc2 == null) { if (!error) controller.Popup(GetIntlString("SE_account_noselect"), 1500, ConsoleColor.Red); error = true; v.Inputs[2].BackgroundColor = ConsoleColor.Red; v.Inputs[2].SelectBackgroundColor = ConsoleColor.DarkRed; } if (user == null) { if (!error) controller.Popup(GetIntlString("SE_account_nouser"), 1500, ConsoleColor.Red); error = true; v.Inputs[1].BackgroundColor = ConsoleColor.DarkRed; v.Inputs[1].SelectBackgroundColor = ConsoleColor.Red; } userDataGetter = Promise.AwaitPromise(interactor.UserInfo()); userDataGetter.Subscribe = p => { userDataGetter.Unsubscribe(); var account = AccountLookup("SE_balance_toohigh"); if (account == null) accountsGetter = Promise.AwaitPromise(interactor.AccountInfo(acc1)); accountsGetter.Subscribe = result => { accountsGetter.Unsubscribe(); var resultData = p.Value.Split('&'); Hide("verify_stall"); decimal d; if (result.Value.StartsWith("ERROR") || !Account.TryParse(result.Value, out var act)) controller.Popup(GetIntlString("GENERIC_error"), 1500, ConsoleColor.Red); else if ((d = decimal.Parse(v.Inputs[3].Text)) > act.balance && (!bool.Parse(resultData[1]) || !acc1.Equals(acc2))) controller.Popup(GetIntlString("SE_balance_toohigh").Replace("$0", act.balance.ToString()), 3000, ConsoleColor.Red); else { Promise txPromise = Promise.AwaitPromise(interactor.CreateTransaction(acc1, user, acc2, d, v.Inputs[4].Text.Length == 0 ? null : v.Inputs[4].Text)); accountChange = true; accountDataCache.Clear(); txPromise.Subscribe = txResult => { if (txResult.Value.StartsWith("ERROR")) controller.Popup(GetIntlString("GENERIC_error"), 1500, ConsoleColor.Red); else controller.Popup(GetIntlString("SE_tx_success"), 2000, ConsoleColor.Green, () => Hide("transfer")); }; } }; }; break; } }; exit_prompt.RegisterSelectListener((v, i, s) => { if (i == 0) Hide("exit_prompt"); else HandleLogout(); }); } private ListView GenerateList(string[] data, SubmissionEvent onclick, bool exitOnSubmit = false, bool hideOnBack = true) { //var list = GetView<ListView>("account_show"); var list = new ListView(new ViewData("ListView").SetAttribute("padding_left", 2).SetAttribute("padding_right", 2).SetAttribute("border", 8), LangManager.NO_LANG); //if (data.Length == 1 && data[0].Length == 0) data = new string[0]; Tuple<string, View>[] listData = new Tuple<string, View>[data.Length - ((data.Length == 1 && data[0].Length == 0) ? 1 : 0)]; for (int i = 0; i < listData.Length; ++i) { ButtonView t = new ButtonView(new ViewData("ButtonView").AddNestedSimple("Text", data[i]), LangManager.NO_LANG); // Don't do translations t.SetEvent(v => { onclick?.Invoke(v); if (exitOnSubmit) Hide(list); }); listData[i] = new Tuple<string, View>(t.Text, t); } if (listData.Length > 0) list.AddViews(0, listData); else { ButtonView close = new ButtonView(new ViewData("ButtonView").AddNestedSimple("Text", GetIntlString("GENERIC_dismiss")), LangManager.NO_LANG); close.SetEvent(_ => Hide(list)); list.AddView(close, "close"); } if (hideOnBack) list.OnBackEvent = v => Hide(v); return list; } private void RefreshAccountList() { accountsGetter = Promise.AwaitPromise(interactor.ListUserAccounts()); // Get accounts associated with this user accountsGetter.Subscribe = p => { var data = p.Value.Split('&'); accounts = new List<string>(); accounts.AddRange(data); }; } private void RefreshUserInfo() { userDataGetter = Promise.AwaitPromise(interactor.UserInfo()); // Get basic user info userDataGetter.Subscribe = p => { var data = p.Value.Split('&'); username = data[0].FromBase64String(); isAdministrator = bool.Parse(data[1]); }; } private void HandleLogout(bool automatic = false) { #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed interactor.Logout(); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed controller.Popup(GetIntlString($"SE_{(automatic ? "auto" : "")}lo"), 2500, ConsoleColor.DarkMagenta, () => manager.LoadContext(new WelcomeContext(manager, interactor))); } private Tuple<string, Account> AccountLookup(string name) { foreach (var cacheEntry in accountDataCache) if (cacheEntry.Item1.Equals(name)) return cacheEntry; return null; } public override void OnCreate() { if (scheduleDestroy) manager.LoadContext(new WelcomeContext(manager, interactor)); else Show("menu_options"); } public override void OnDestroy() { base.HideAll(); #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed interactor.CancelAll(); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed } void CreateAccount() { bool hasError = false; if (accountType == -1) { hasError = true; account_create.Inputs[1].SelectBackgroundColor = ConsoleColor.Red; account_create.Inputs[1].BackgroundColor = ConsoleColor.DarkRed; controller.Popup(GetIntlString("SE_acc_nosel"), 2500, ConsoleColor.Red); } if (account_create.Inputs[0].Text.Length == 0) { account_create.Inputs[0].SelectBackgroundColor = ConsoleColor.Red; account_create.Inputs[0].BackgroundColor = ConsoleColor.DarkRed; if (!hasError) controller.Popup(GetIntlString("ERR_empty"), 3000, ConsoleColor.Red); } else if(!hasError) { void AlreadyExists() => controller.Popup(GetIntlString("SE_account_exists").Replace("$0", account_create.Inputs[0].Text), 2500, ConsoleColor.Red, () => Hide(account_create)); var act = AccountLookup(account_create.Inputs[0].Text); if (act != null) AlreadyExists(); else { Show("account_stall"); Promise accountPromise = Promise.AwaitPromise(interactor.CreateAccount(account_create.Inputs[0].Text, accountType == 0)); accountPromise.Subscribe = p => { if (bool.Parse(p.Value)) { controller.Popup(GetIntlString("SE_account_success"), 750, ConsoleColor.Green, () => Hide(account_create)); accountChange = true; } else AlreadyExists(); Hide("account_stall"); }; } } } private void ViewAccountListener(View listener) { ButtonView view = listener as ButtonView; void ShowAccountData(string name, decimal balance, Account.AccountType type) { // Build dialog view manually var show = new DialogView( new ViewData("DialogView") // Layout parameters .SetAttribute("padding_left", 2) .SetAttribute("padding_right", 2) .SetAttribute("padding_top", 1) .SetAttribute("padding_bottom", 1) .SetAttribute("border", (int)ConsoleColor.DarkGreen) // Option buttons .AddNested(new ViewData("Options").AddNestedSimple("Option", GetIntlString("GENERIC_dismiss")).AddNestedSimple("Option", GetIntlString("SE_account_delete"))) // Message .AddNestedSimple("Text", GetIntlString("SE_info") .Replace("$0", name) .Replace("$1", GetIntlString(type == Account.AccountType.Savings ? "SE_acc_saving" : "SE_acc_checking")) .Replace("$2", balance.ToTruncatedString())), // No translation (it's already handled) LangManager.NO_LANG); show.RegisterSelectListener((_, s, l) => { if(s==0) Hide(show); else { var ynDialog = GetView<DialogView>("yn"); ynDialog.Text = GetIntlString("SE_account_delete_warn"); ynDialog.RegisterSelectListener((v, i, str) => { var stall = GetView<TextView>("stall"); stall.Text = GetIntlString("SE_account_delete_stall"); Show(stall); if (i == 1) { Promise p = Promise.AwaitPromise(interactor.CloseAccount(name)); p.Subscribe = deleteAwait => { if (bool.Parse(deleteAwait.Value)) { accountChange = true; controller.Popup(GetIntlString("SE_account_delete_success"), 1500, ConsoleColor.Green, () => { bool closed = false; controller.CloseIf(predV => closed = !closed && predV is ListView); Hide(show); }); } else controller.Popup(GetIntlString("SE_account_delete_fail"), 2000, ConsoleColor.Red); Hide(stall); }; } }); Show(ynDialog); } }); Show(show); } // TODO: Show account info var account = AccountLookup(view.Text); if (account == null) { // TODO: Get account data from server + cache data Show("data_fetch"); Promise info_promise = Promise.AwaitPromise(interactor.AccountInfo(view.Text)); info_promise.Subscribe = evt => { Hide("data_fetch"); if (evt.Value.StartsWith("ERROR") || !Account.TryParse(evt.Value, out var act)) controller.Popup(GetIntlString("GENERIC_error"), 3000, ConsoleColor.Red); else { // Cache result (don't cache savings accounts because their value updates pretty frequently) if (act.type != Account.AccountType.Savings) accountDataCache.Enqueue(new Tuple<string, Account>(view.Text, act)); ShowAccountData(view.Text, act.balance, act.type); } }; } else ShowAccountData(account.Item1, account.Item2.balance, account.Item2.type); } } }