using System.Collections.ObjectModel; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using RblxTool.Models; using RblxTool.Services; namespace RblxTool.Views; public sealed partial class AccountsPage : Page { private readonly ObservableCollection _accounts = new(); private string? _pendingLoginId; private string? _oauthVerifier; public AccountsPage() { InitializeComponent(); AccountList.ItemsSource = _accounts; Loaded += (_, _) => Reload(); } private void Reload() { _accounts.Clear(); foreach (var account in AppState.Current.Accounts()) { _accounts.Add(account); } var active = AppState.Current.Active; if (active is not null) { AccountList.SelectedItem = _accounts.FirstOrDefault(a => a.Id == active.Id); } } private void Status(string message) => StatusText.Text = message; private async void OnAddCookie(object sender, RoutedEventArgs e) { var cookie = CookieBox.Password.Trim(); if (cookie.Length == 0) { Status("paste a cookie first"); return; } var label = CookieLabelBox.Text.Trim(); Status("validating cookie..."); try { var summary = await Task.Run(() => AppState.Current.Engine.AddCookie(cookie, label.Length == 0 ? null : label)); CookieBox.Password = string.Empty; Status($"added {summary.Username} ({summary.UserId})"); AppState.Current.NotifyAccountsChanged(); Reload(); } catch (Exception error) { Status(error.Message); } } private async void OnLogin(object sender, RoutedEventArgs e) { var identifier = UsernameBox.Text.Trim(); var password = PasswordBox.Password; if (identifier.Length == 0 || password.Length == 0) { Status("username and password are required"); return; } Status("signing in..."); var result = await Task.Run(() => AppState.Current.Engine.Login(identifier, password, null)); if (!result.Ok) { Status(result.Error ?? "login failed"); return; } switch (result.State) { case "authenticated": PasswordBox.Password = string.Empty; Status($"signed in as {result.Account?.Username}"); AppState.Current.NotifyAccountsChanged(); Reload(); break; case "two_step": _pendingLoginId = result.PendingId; TwoStepPanel.Visibility = Visibility.Visible; TwoStepHint.Text = $"enter the code sent via {result.MediaType}"; Status("two step verification required"); break; case "challenge": Status($"roblox raised a {result.ChallengeType} captcha challenge; solve it in the browser window"); await ShowChallengeAsync(result); break; default: Status("unexpected login state"); break; } } private async Task ShowChallengeAsync(LoginResult result) { var dialog = new ContentDialog { XamlRoot = XamlRoot, Title = "Captcha challenge", CloseButtonText = "Close", Content = new TextBlock { TextWrapping = TextWrapping.Wrap, Text = "Roblox requires a FunCaptcha before this login can continue.\n\n" + $"challenge id: {result.ChallengeId}\n" + $"arkose key: {result.ArkoseKey}\n\n" + "Sign in once in a browser on this machine, then paste the resulting .ROBLOSECURITY cookie " + "into the cookie card above. Cookie sessions never hit this challenge again.", }, }; await dialog.ShowAsync(); } private async void OnSubmitCode(object sender, RoutedEventArgs e) { if (_pendingLoginId is null) { return; } var code = CodeBox.Text.Trim(); try { var summary = await Task.Run(() => AppState.Current.Engine.SubmitTwoStep(_pendingLoginId, code)); _pendingLoginId = null; TwoStepPanel.Visibility = Visibility.Collapsed; PasswordBox.Password = string.Empty; CodeBox.Text = string.Empty; Status($"signed in as {summary.Username}"); AppState.Current.NotifyAccountsChanged(); Reload(); } catch (Exception error) { Status(error.Message); } } private OauthRequest BuildOauthRequest() => new() { ClientId = ClientIdBox.Text.Trim(), RedirectUri = RedirectBox.Text.Trim(), }; private async void OnOauthUrl(object sender, RoutedEventArgs e) { var request = BuildOauthRequest(); if (request.ClientId.Length == 0) { Status("client id is required"); return; } var result = AppState.Current.Engine.OauthUrl(request); if (!result.Ok || result.Url is null) { Status(result.Error ?? "could not build the authorize url"); return; } _oauthVerifier = result.Verifier; await Windows.System.Launcher.LaunchUriAsync(new Uri(result.Url)); Status("approve in the browser, then paste the returned code"); } private async void OnOauthExchange(object sender, RoutedEventArgs e) { if (_oauthVerifier is null) { Status("open the authorize page first"); return; } var code = OauthCodeBox.Text.Trim(); var request = BuildOauthRequest(); try { var summary = await Task.Run(() => AppState.Current.Engine.OauthExchange(request, code, _oauthVerifier)); Status($"linked {summary.Username} (read only)"); AppState.Current.NotifyAccountsChanged(); Reload(); } catch (Exception error) { Status(error.Message); } } private void OnAccountSelected(object sender, SelectionChangedEventArgs e) { if (AccountList.SelectedItem is AccountSummary account) { AppState.Current.SetActive(account); } } private void OnRemove(object sender, RoutedEventArgs e) { if (AccountList.SelectedItem is not AccountSummary account) { return; } AppState.Current.Engine.Remove(account.Id); AppState.Current.NotifyAccountsChanged(); Reload(); Status($"removed {account.Username}"); } private void OnRefresh(object sender, RoutedEventArgs e) => Reload(); }