feat(ui): Merge Microsoft Auth types into 1 popup
fix(ui): Microsoft Auth notification is no longer persistent on cancel
This commit is contained in:
parent
eeaecd3de4
commit
d0c97b4177
21 changed files with 606 additions and 630 deletions
|
|
@ -147,6 +147,10 @@ impl CoreBridgeRuntime {
|
|||
apply_game_failed(snapshots, cluster_id, message);
|
||||
immediate = true;
|
||||
}
|
||||
Notification::MicrosoftLoginStatus(status) => {
|
||||
snapshots.microsoft_login = status;
|
||||
immediate = true;
|
||||
}
|
||||
other => {
|
||||
let (notif, _timers, prompt) = engine.dispatch(inbox, other);
|
||||
snapshots.notifications = notif;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use oneclient_core::notification::LaunchStage;
|
||||
use oneclient_core::notification::{LaunchStage, MicrosoftLoginStatus};
|
||||
use oneclient_core::settings::LauncherSettings;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
|
|
@ -99,4 +99,5 @@ pub struct BridgeSnapshot {
|
|||
pub notifications: crate::notifications::NotificationSnapshot,
|
||||
pub game: GameSnapshot,
|
||||
pub account_switcher_open: bool,
|
||||
pub microsoft_login: Option<MicrosoftLoginStatus>,
|
||||
}
|
||||
|
|
|
|||
334
packages/oneclient_app/src/components/microsoft_login.rs
Normal file
334
packages/oneclient_app/src/components/microsoft_login.rs
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
use freya::prelude::*;
|
||||
use freya::query::{MutationCapability, MutationStateData, UseMutation};
|
||||
use freya::text_edit::Clipboard;
|
||||
use oneclient_core::auth::MicrosoftLoginSession;
|
||||
use oneclient_core::notification::MicrosoftLoginStatus;
|
||||
|
||||
use crate::components::{Button, Icon, IconType, OverlayPopup};
|
||||
use crate::hooks::{
|
||||
CancelMicrosoftLoginKeys, login_code_already_handled, reset_login_code_dedup,
|
||||
use_begin_microsoft_login, use_cancel_microsoft_login, use_finish_microsoft_login,
|
||||
use_microsoft_login_status,
|
||||
};
|
||||
use crate::platform;
|
||||
use crate::theme::colors;
|
||||
use crate::ui::border_all_color;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MicrosoftLogin {
|
||||
begin: UseMutation<crate::hooks::BeginMicrosoftLoginMutation>,
|
||||
cancel: UseMutation<crate::hooks::CancelMicrosoftLoginMutation>,
|
||||
pending_login: State<Option<MicrosoftLoginSession>>,
|
||||
cancelled: State<bool>,
|
||||
status: Option<MicrosoftLoginStatus>,
|
||||
locked: bool,
|
||||
pub pending: bool,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl MicrosoftLogin {
|
||||
pub fn start(&self) {
|
||||
let mut cancelled = self.cancelled;
|
||||
cancelled.set(false);
|
||||
self.begin.mutate(());
|
||||
}
|
||||
|
||||
fn cancel(&self) {
|
||||
if self.locked {
|
||||
return;
|
||||
}
|
||||
if let Some(session) = self.pending_login.peek().clone() {
|
||||
self.cancel.mutate(CancelMicrosoftLoginKeys {
|
||||
state_token: session.dedupe_key().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
reset_login_code_dedup();
|
||||
|
||||
let mut cancelled = self.cancelled;
|
||||
let mut pending_login = self.pending_login;
|
||||
cancelled.set(true);
|
||||
pending_login.set(None);
|
||||
}
|
||||
|
||||
pub fn popup(&self) -> Option<impl IntoElement> {
|
||||
let handle = self.clone();
|
||||
self.pending_login.read().clone().map(move |login| {
|
||||
microsoft_dialog(
|
||||
login,
|
||||
handle.clone(),
|
||||
handle.status.clone(),
|
||||
handle.error.clone(),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn use_microsoft_login() -> MicrosoftLogin {
|
||||
let begin = use_begin_microsoft_login();
|
||||
let finish = use_finish_microsoft_login();
|
||||
let cancel = use_cancel_microsoft_login();
|
||||
let mut pending_login = use_state(|| None::<MicrosoftLoginSession>);
|
||||
let cancelled = use_state(|| false);
|
||||
let status = use_microsoft_login_status();
|
||||
|
||||
use_side_effect(move || {
|
||||
let session = match &*begin.read().state() {
|
||||
MutationStateData::Settled {
|
||||
res: Ok(session), ..
|
||||
} => Some(session.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let Some(session) = session else { return };
|
||||
if login_code_already_handled(session.dedupe_key()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// open the browser immediately
|
||||
platform::open_url(session.auth_url());
|
||||
finish.mutate(session.clone());
|
||||
pending_login.set(Some(session));
|
||||
});
|
||||
|
||||
use_side_effect(move || {
|
||||
if matches!(
|
||||
&*finish.read().state(),
|
||||
MutationStateData::Settled { res: Ok(_), .. }
|
||||
) && pending_login.peek().is_some()
|
||||
{
|
||||
pending_login.set(None);
|
||||
}
|
||||
});
|
||||
|
||||
// is true once the flow has some status updates (just to ensure the user has seen the popup) and the flow is still in progress
|
||||
let locked = status.as_ref().is_some_and(|s| s.current > 0);
|
||||
let in_flight = begin.read().state().is_loading() || finish.read().state().is_loading();
|
||||
let pending = in_flight && !*cancelled.read();
|
||||
let error = mutation_err_text(&finish).or_else(|| mutation_err_text(&begin));
|
||||
|
||||
MicrosoftLogin {
|
||||
begin,
|
||||
cancel,
|
||||
pending_login,
|
||||
cancelled,
|
||||
status,
|
||||
locked,
|
||||
pending,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
fn mutation_err_text<M>(mutation: &UseMutation<M>) -> Option<String>
|
||||
where
|
||||
M: MutationCapability,
|
||||
M::Err: std::fmt::Display,
|
||||
{
|
||||
match &*mutation.read().state() {
|
||||
MutationStateData::Settled { res: Err(err), .. } => Some(err.to_string()),
|
||||
MutationStateData::Loading {
|
||||
res: Some(Err(err)),
|
||||
} => Some(err.to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn microsoft_dialog(
|
||||
login: MicrosoftLoginSession,
|
||||
handle: MicrosoftLogin,
|
||||
status: Option<MicrosoftLoginStatus>,
|
||||
error: Option<String>,
|
||||
) -> impl IntoElement {
|
||||
let locked = handle.locked;
|
||||
let close_handle = handle.clone();
|
||||
let cancel_handle = handle;
|
||||
OverlayPopup::new()
|
||||
// Backdrop / Esc only dismiss while the flow is still cancellable, if its locked the popup can't be closed and cancelling aborts everything.
|
||||
.on_close(move |()| close_handle.cancel())
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::window_percent(100.))
|
||||
.height(Size::window_percent(100.))
|
||||
.center()
|
||||
.child(
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::px(420.))
|
||||
.max_width(Size::window_percent(90.))
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(18.)
|
||||
.padding(Gaps::new_all(28.))
|
||||
.corner_radius(CornerRadius::new_all(16.))
|
||||
.background(colors::page_elevated())
|
||||
.border(border_all_color(1., colors::component_border()))
|
||||
.child(
|
||||
label()
|
||||
.text("Sign in to Microsoft")
|
||||
.font_size(18.)
|
||||
.font_weight(FontWeight::SEMI_BOLD)
|
||||
.color(colors::fg_primary()),
|
||||
)
|
||||
.child(browser_dialog_body(login.auth_url().to_string()))
|
||||
.child(dialog_divider())
|
||||
.child(device_code_dialog_body(
|
||||
login.user_code().to_string(),
|
||||
login.verification_uri().to_string(),
|
||||
))
|
||||
.child(status_row(status, error))
|
||||
.maybe_child((!locked).then(|| {
|
||||
Button::new()
|
||||
.ghost()
|
||||
.on_press(move |_| cancel_handle.cancel())
|
||||
.text("Cancel")
|
||||
.into_element()
|
||||
})),
|
||||
),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
|
||||
fn dialog_divider() -> impl IntoElement {
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.cross_align(Alignment::Center)
|
||||
.content(Content::Flex)
|
||||
.spacing(10.)
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::flex(1.0))
|
||||
.height(Size::px(1.))
|
||||
.background(colors::component_border()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text("or")
|
||||
.font_size(11.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::flex(1.0))
|
||||
.height(Size::px(1.))
|
||||
.background(colors::component_border()),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
|
||||
fn status_row(status: Option<MicrosoftLoginStatus>, error: Option<String>) -> impl IntoElement {
|
||||
if let Some(error) = error {
|
||||
return rect()
|
||||
.horizontal()
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(6.)
|
||||
.child(
|
||||
Icon::new(IconType::AlertTriangle)
|
||||
.size(13.)
|
||||
.color(colors::danger()),
|
||||
)
|
||||
.child(label().text(error).font_size(12.).color(colors::danger()))
|
||||
.into_element();
|
||||
}
|
||||
|
||||
let text = match &status {
|
||||
Some(status) if status.total > 0 && status.current > 0 => {
|
||||
format!("{} ({}/{})", status.label, status.current, status.total)
|
||||
}
|
||||
_ => "Waiting for you to finish signing in...".to_string(),
|
||||
};
|
||||
|
||||
rect()
|
||||
.horizontal()
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(6.)
|
||||
.child(
|
||||
Icon::new(IconType::Loading02)
|
||||
.size(13.)
|
||||
.color(colors::brand()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(text)
|
||||
.font_size(12.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
|
||||
fn browser_dialog_body(auth_url: String) -> impl IntoElement {
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::fill())
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(12.)
|
||||
.child(
|
||||
label()
|
||||
.text("We opened the Microsoft sign-in page in your browser. Finish there and you'll be brought back automatically.")
|
||||
.font_size(13.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.child(
|
||||
Button::new()
|
||||
.primary()
|
||||
.on_press(move |_| platform::open_url(&auth_url))
|
||||
.child(Icon::new(IconType::LinkExternal01).size(16.))
|
||||
.text("Open in browser again"),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
|
||||
fn device_code_dialog_body(code: String, verification_uri: String) -> impl IntoElement {
|
||||
let copy_code = code.clone();
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::fill())
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(12.)
|
||||
.child(
|
||||
label()
|
||||
.text("Or enter this code at the Microsoft sign-in page:")
|
||||
.font_size(13.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.center()
|
||||
.padding(Gaps::new_symmetric(20., 16.))
|
||||
.corner_radius(CornerRadius::new_all(14.))
|
||||
.background(colors::component_bg())
|
||||
.border(border_all_color(1., colors::brand()))
|
||||
.child(
|
||||
label()
|
||||
.text(code)
|
||||
.font_size(48.)
|
||||
.font_weight(FontWeight::BOLD)
|
||||
.color(colors::fg_primary()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.main_align(Alignment::Center)
|
||||
.spacing(8.)
|
||||
.child(
|
||||
Button::new()
|
||||
.secondary()
|
||||
.on_press(move |_| {
|
||||
if let Err(err) = Clipboard::set(copy_code.clone()) {
|
||||
tracing::warn!("clipboard copy failed: {err:?}");
|
||||
}
|
||||
})
|
||||
.child(Icon::new(IconType::Copy01).size(16.))
|
||||
.text("Copy code"),
|
||||
)
|
||||
.child(
|
||||
Button::new()
|
||||
.primary()
|
||||
.on_press(move |_| platform::open_url(&verification_uri))
|
||||
.child(Icon::new(IconType::LinkExternal01).size(16.))
|
||||
.text("Open in browser"),
|
||||
),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ mod link_confirm;
|
|||
mod local_image;
|
||||
mod log_viewer;
|
||||
mod major_version_card;
|
||||
mod microsoft_login;
|
||||
mod navbar;
|
||||
mod notifications;
|
||||
mod overlay_popup;
|
||||
|
|
@ -43,8 +44,9 @@ pub use link_confirm::ConfirmLinkOverlay;
|
|||
pub use local_image::LocalImage;
|
||||
pub use log_viewer::LogViewer;
|
||||
pub use major_version_card::MajorVersionCard;
|
||||
pub use navbar::{AppNavbar, OnboardingNavbar};
|
||||
pub use microsoft_login::use_microsoft_login;
|
||||
pub(crate) use navbar::window_controls;
|
||||
pub use navbar::{AppNavbar, OnboardingNavbar};
|
||||
pub use notifications::NotificationCenter;
|
||||
pub use overlay_popup::OverlayPopup;
|
||||
pub use package_row::{CardLayout, PackageEntry, PackageRow};
|
||||
|
|
@ -52,8 +54,8 @@ pub use pagination::Pagination;
|
|||
pub use player_model::PlayerModel;
|
||||
pub use recents_row::RecentsRow;
|
||||
pub use screenshot_viewer::ScreenshotViewer;
|
||||
pub use scrollview::{ScrollArea, ScrollAreaCtx};
|
||||
pub(crate) use scrollview::corrected_scroll;
|
||||
pub use scrollview::{ScrollArea, ScrollAreaCtx};
|
||||
pub use segmented_control::{Segment, SegmentedControl};
|
||||
pub use status_bar::StatusBar;
|
||||
pub use tab_bar::{TabBar, TabItem};
|
||||
|
|
|
|||
|
|
@ -15,38 +15,43 @@ pub use active_cluster::{
|
|||
|
||||
pub use dispatch::BridgeDispatch;
|
||||
pub use queries::{
|
||||
AddOfflineAccountKeys, BROWSE_PAGE_SIZE, CachedImageQuery, ClusterAction, ClusterBundles,
|
||||
ClusterLogsQuery, LogAction, LogContentQuery, MigrationQuery, OnboardingBundlesQuery,
|
||||
RefreshAccountKeys, RemoveAccountKeys, ScreenshotAction, SetDefaultAccountKeys, UploadLogKeys,
|
||||
UploadLogMutation, UseLogAction, UseRefreshAccount, UseRemoveAccount, UseScreenshotAction,
|
||||
UseSetDefaultAccount, UseUploadLog, VERSIONS_PAGE_SIZE, accounts_have_microsoft,
|
||||
bundle_overrides_map, bundles_with_status_items, category_list, changelog_error,
|
||||
changelog_groups, changelog_is_loading, cluster_content_items, content_type_for_slug,
|
||||
has_migration_data, invalidate_cluster_queries, invalidate_java_queries,
|
||||
AddOfflineAccountKeys, BROWSE_PAGE_SIZE, BeginMicrosoftLoginMutation, CachedImageQuery,
|
||||
CancelMicrosoftLoginKeys, CancelMicrosoftLoginMutation, ClusterAction, ClusterBundles,
|
||||
ClusterLogsQuery, FinishMicrosoftLoginMutation, LogAction, LogContentQuery, MigrationQuery,
|
||||
OnboardingBundlesQuery, RefreshAccountKeys, RemoveAccountKeys, ScreenshotAction,
|
||||
SetDefaultAccountKeys, UploadLogKeys, UploadLogMutation, UseLogAction, UseRefreshAccount,
|
||||
UseRemoveAccount, UseScreenshotAction, UseSetDefaultAccount, UseUploadLog, VERSIONS_PAGE_SIZE,
|
||||
accounts_have_microsoft, bundle_overrides_map, bundles_with_status_items, category_list,
|
||||
changelog_error, changelog_groups, changelog_is_loading, cluster_content_items,
|
||||
content_type_for_slug, has_migration_data, invalidate_cluster_queries, invalidate_java_queries,
|
||||
invalidate_logs_queries, invalidate_profile_queries, invalidate_screenshots_queries,
|
||||
java_runtimes, loader_versions, login_code_already_handled, migration_detection,
|
||||
mutation_error, mutation_is_pending, onboarding_bundles_items, package_meta_batch,
|
||||
pick_version_metadata, project_detail, provider_versions, search_items, search_pending,
|
||||
search_total, try_account, try_accounts, try_cluster_analytics, try_cluster_logs,
|
||||
try_cluster_screenshots, try_default_account, try_game_profile, try_global_analytics,
|
||||
try_log_content, use_account, use_accounts, use_add_microsoft_account, use_add_offline_account,
|
||||
use_begin_microsoft_login, use_bundle_overrides, use_bundle_updates, use_bundles_with_status,
|
||||
use_cached_image, use_changelog, use_cluster_analytics, use_cluster_content, use_cluster_logs,
|
||||
use_cluster_mutation, use_cluster_profile, use_cluster_screenshots, use_cluster_settings,
|
||||
use_clusters, use_current_account, use_default_account, use_finish_microsoft_login,
|
||||
use_game_profile, use_global_analytics, use_java_runtimes, use_loader_versions,
|
||||
use_local_image, use_log_action, use_log_content, use_migration, use_named_profiles,
|
||||
use_onboarding_bundles, use_package_categories, use_package_meta_batch, use_package_project,
|
||||
use_package_search, use_package_versions, use_player_profile, use_player_skin,
|
||||
use_provider_versions, use_refresh_account, use_refresh_all_accounts, use_remove_account,
|
||||
use_screenshot_action, use_set_default_account, use_upload_log, use_version_metadata,
|
||||
use_versions, version_list, versions_total,
|
||||
pick_version_metadata, project_detail, provider_versions, reset_login_code_dedup, search_items,
|
||||
search_pending, search_total, try_account, try_accounts, try_cluster_analytics,
|
||||
try_cluster_logs, try_cluster_screenshots, try_default_account, try_game_profile,
|
||||
try_global_analytics, try_log_content, use_account, use_accounts, use_add_microsoft_account,
|
||||
use_add_offline_account, use_begin_microsoft_login, use_bundle_overrides, use_bundle_updates,
|
||||
use_bundles_with_status, use_cached_image, use_cancel_microsoft_login, use_changelog,
|
||||
use_cluster_analytics, use_cluster_content, use_cluster_logs, use_cluster_mutation,
|
||||
use_cluster_profile, use_cluster_screenshots, use_cluster_settings, use_clusters,
|
||||
use_current_account, use_default_account, use_finish_microsoft_login, use_game_profile,
|
||||
use_global_analytics, use_java_runtimes, use_loader_versions, use_local_image, use_log_action,
|
||||
use_log_content, use_migration, use_named_profiles, use_onboarding_bundles,
|
||||
use_package_categories, use_package_meta_batch, use_package_project, use_package_search,
|
||||
use_package_versions, use_player_profile, use_player_skin, use_provider_versions,
|
||||
use_refresh_account, use_refresh_all_accounts, use_remove_account, use_screenshot_action,
|
||||
use_set_default_account, use_upload_log, use_version_metadata, use_versions, version_list,
|
||||
versions_total,
|
||||
};
|
||||
|
||||
use crate::{bridge::{
|
||||
BridgeSnapshot, ClustersSnapshot, GameSnapshot, JavaSnapshot, LauncherInit, OneClientBridge,
|
||||
ProfilesSnapshot, SettingsSnapshot, use_bridge_snapshot,
|
||||
}, notifications::NotificationSnapshot};
|
||||
use crate::{
|
||||
bridge::{
|
||||
BridgeSnapshot, ClustersSnapshot, GameSnapshot, JavaSnapshot, LauncherInit,
|
||||
OneClientBridge, ProfilesSnapshot, SettingsSnapshot, use_bridge_snapshot,
|
||||
},
|
||||
notifications::NotificationSnapshot,
|
||||
};
|
||||
use freya::prelude::*;
|
||||
|
||||
pub fn use_provide_bridge(bridge: &OneClientBridge) {
|
||||
|
|
@ -96,6 +101,10 @@ pub fn use_game_snapshot() -> GameSnapshot {
|
|||
use_snapshots().game
|
||||
}
|
||||
|
||||
pub fn use_microsoft_login_status() -> Option<oneclient_core::notification::MicrosoftLoginStatus> {
|
||||
use_snapshots().microsoft_login
|
||||
}
|
||||
|
||||
pub fn use_java_snapshot() -> JavaSnapshot {
|
||||
use_snapshots().java
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use freya::query::{
|
|||
QueryStateData, UseMutation, UseQuery, use_mutation, use_query,
|
||||
};
|
||||
use oneclient_core::LauncherError;
|
||||
use oneclient_core::auth::{self, AccountKind, MinecraftAccount, MinecraftLogin};
|
||||
use oneclient_core::auth::{self, AccountKind, MicrosoftLoginSession, MinecraftAccount};
|
||||
use uuid::Uuid;
|
||||
|
||||
static HANDLED_LOGIN_CODE: Mutex<Option<String>> = Mutex::new(None);
|
||||
|
|
@ -20,6 +20,10 @@ pub fn login_code_already_handled(user_code: &str) -> bool {
|
|||
false
|
||||
}
|
||||
|
||||
pub fn reset_login_code_dedup() {
|
||||
*HANDLED_LOGIN_CODE.lock().unwrap() = None;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ListAccountsKeys;
|
||||
|
||||
|
|
@ -141,7 +145,7 @@ async fn invalidate_auth_queries(account_id: Option<Uuid>) {
|
|||
pub struct BeginMicrosoftLoginMutation;
|
||||
|
||||
impl MutationCapability for BeginMicrosoftLoginMutation {
|
||||
type Ok = MinecraftLogin;
|
||||
type Ok = MicrosoftLoginSession;
|
||||
type Err = LauncherError;
|
||||
type Keys = ();
|
||||
|
||||
|
|
@ -156,7 +160,7 @@ pub struct FinishMicrosoftLoginMutation;
|
|||
impl MutationCapability for FinishMicrosoftLoginMutation {
|
||||
type Ok = MinecraftAccount;
|
||||
type Err = LauncherError;
|
||||
type Keys = MinecraftLogin;
|
||||
type Keys = MicrosoftLoginSession;
|
||||
|
||||
async fn run(&self, flow: &Self::Keys) -> Result<Self::Ok, Self::Err> {
|
||||
auth::finish_microsoft_login(flow.clone()).await
|
||||
|
|
@ -169,6 +173,27 @@ impl MutationCapability for FinishMicrosoftLoginMutation {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct CancelMicrosoftLoginMutation;
|
||||
|
||||
/// Drops a pending browser login server-side when the user cancels before the
|
||||
/// flow reaches the point of no return. Keyed by the session's CSRF state token.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct CancelMicrosoftLoginKeys {
|
||||
pub state_token: String,
|
||||
}
|
||||
|
||||
impl MutationCapability for CancelMicrosoftLoginMutation {
|
||||
type Ok = ();
|
||||
type Err = LauncherError;
|
||||
type Keys = CancelMicrosoftLoginKeys;
|
||||
|
||||
async fn run(&self, keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
|
||||
auth::cancel_microsoft_login(&keys.state_token).await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct AddMicrosoftAccountMutation;
|
||||
|
||||
|
|
@ -316,6 +341,10 @@ pub fn use_finish_microsoft_login() -> UseMutation<FinishMicrosoftLoginMutation>
|
|||
use_mutation(Mutation::new(FinishMicrosoftLoginMutation))
|
||||
}
|
||||
|
||||
pub fn use_cancel_microsoft_login() -> UseMutation<CancelMicrosoftLoginMutation> {
|
||||
use_mutation(Mutation::new(CancelMicrosoftLoginMutation))
|
||||
}
|
||||
|
||||
pub fn use_add_microsoft_account() -> UseMutation<AddMicrosoftAccountMutation> {
|
||||
use_mutation(Mutation::new(AddMicrosoftAccountMutation))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,13 +21,15 @@ pub use analytics::{
|
|||
try_cluster_analytics, try_global_analytics, use_cluster_analytics, use_global_analytics,
|
||||
};
|
||||
pub use auth::{
|
||||
AddOfflineAccountKeys, RefreshAccountKeys, RemoveAccountKeys, SetDefaultAccountKeys,
|
||||
UseRefreshAccount, UseRemoveAccount, UseSetDefaultAccount, accounts_have_microsoft,
|
||||
login_code_already_handled, mutation_error, mutation_is_pending, try_account, try_accounts,
|
||||
try_default_account, use_account, use_accounts, use_add_microsoft_account,
|
||||
use_add_offline_account, use_begin_microsoft_login, use_current_account, use_default_account,
|
||||
use_finish_microsoft_login, use_refresh_account, use_refresh_all_accounts, use_remove_account,
|
||||
use_set_default_account,
|
||||
AddOfflineAccountKeys, BeginMicrosoftLoginMutation, CancelMicrosoftLoginKeys,
|
||||
CancelMicrosoftLoginMutation, FinishMicrosoftLoginMutation, RefreshAccountKeys,
|
||||
RemoveAccountKeys, SetDefaultAccountKeys, UseRefreshAccount, UseRemoveAccount,
|
||||
UseSetDefaultAccount, accounts_have_microsoft, login_code_already_handled, mutation_error,
|
||||
mutation_is_pending, reset_login_code_dedup, try_account, try_accounts, try_default_account,
|
||||
use_account, use_accounts, use_add_microsoft_account, use_add_offline_account,
|
||||
use_begin_microsoft_login, use_cancel_microsoft_login, use_current_account,
|
||||
use_default_account, use_finish_microsoft_login, use_refresh_account, use_refresh_all_accounts,
|
||||
use_remove_account, use_set_default_account,
|
||||
};
|
||||
pub use bundles::{
|
||||
ClusterBundles, OnboardingBundlesQuery, bundle_overrides_map, bundles_with_status_items,
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ impl NotificationState {
|
|||
Notification::InvalidateClusters => {}
|
||||
Notification::InvalidateJava => {}
|
||||
Notification::SyncComplete => {}
|
||||
Notification::MicrosoftLoginStatus(_) => {}
|
||||
Notification::GameStage { .. }
|
||||
| Notification::GameLog { .. }
|
||||
| Notification::GameFailed { .. } => {}
|
||||
|
|
@ -268,7 +269,8 @@ impl NotificationState {
|
|||
}
|
||||
|
||||
pub fn expire_toast(&mut self, _inbox: &[InboxEntry], entry_id: u64) {
|
||||
self.active_toasts.retain(|toast| toast.entry_id != entry_id);
|
||||
self.active_toasts
|
||||
.retain(|toast| toast.entry_id != entry_id);
|
||||
}
|
||||
|
||||
pub fn mark_read(&mut self, inbox: &mut [InboxEntry], entry_id: u64) {
|
||||
|
|
|
|||
|
|
@ -1,23 +1,21 @@
|
|||
use chrono::Utc;
|
||||
use freya::prelude::*;
|
||||
use freya::query::{MutationCapability, MutationStateData, UseMutation};
|
||||
use freya::text_edit::Clipboard;
|
||||
use oneclient_core::auth::{AccountKind, MinecraftAccount, MinecraftLogin};
|
||||
use oneclient_core::auth::{AccountKind, MinecraftAccount};
|
||||
|
||||
use crate::components::{
|
||||
Avatar, Button, Icon, IconType, OverlayPopup, PlayerModel, ScrollArea, TextInput,
|
||||
use_microsoft_login,
|
||||
};
|
||||
use crate::hooks::{
|
||||
AddOfflineAccountKeys, RefreshAccountKeys, RemoveAccountKeys, SetDefaultAccountKeys,
|
||||
accounts_have_microsoft, login_code_already_handled, try_accounts, try_default_account,
|
||||
use_accounts, use_add_offline_account, use_begin_microsoft_login, use_current_account,
|
||||
use_finish_microsoft_login, use_refresh_account, use_remove_account, use_set_default_account,
|
||||
accounts_have_microsoft, try_accounts, try_default_account, use_accounts,
|
||||
use_add_offline_account, use_current_account, use_refresh_account, use_remove_account,
|
||||
use_set_default_account,
|
||||
};
|
||||
use crate::platform;
|
||||
use crate::theme::colors;
|
||||
use crate::ui::border_all_color;
|
||||
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct Accounts;
|
||||
|
||||
|
|
@ -26,44 +24,16 @@ impl Component for Accounts {
|
|||
let accounts_query = use_accounts();
|
||||
let default_query = use_current_account();
|
||||
|
||||
let begin = use_begin_microsoft_login();
|
||||
let finish = use_finish_microsoft_login();
|
||||
let msa = use_microsoft_login();
|
||||
let add_offline = use_add_offline_account();
|
||||
let set_default = use_set_default_account();
|
||||
let remove = use_remove_account();
|
||||
let refresh = use_refresh_account();
|
||||
|
||||
let mut username = use_state(String::new);
|
||||
let mut pending_login = use_state(|| None::<MinecraftLogin>);
|
||||
let mut handled_code = use_state(|| None::<String>);
|
||||
let mut show_offline = use_state(|| false);
|
||||
let mut closing_offline = use_state(|| false);
|
||||
|
||||
use_side_effect(move || {
|
||||
let login = match &*begin.read().state() {
|
||||
MutationStateData::Settled { res: Ok(login), .. } => Some(login.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let Some(login) = login else { return };
|
||||
if login_code_already_handled(login.dedupe_key()) {
|
||||
return;
|
||||
}
|
||||
handled_code.set(Some(login.dedupe_key().to_string()));
|
||||
if let Some(url) = login.browser_url() {
|
||||
platform::open_url(url);
|
||||
}
|
||||
finish.mutate(login.clone());
|
||||
pending_login.set(Some(login));
|
||||
});
|
||||
|
||||
use_side_effect(move || {
|
||||
if matches!(&*finish.read().state(), MutationStateData::Settled { .. })
|
||||
&& pending_login.peek().is_some()
|
||||
{
|
||||
pending_login.set(None);
|
||||
}
|
||||
});
|
||||
|
||||
use_side_effect(move || {
|
||||
if !*closing_offline.read() {
|
||||
return;
|
||||
|
|
@ -84,15 +54,13 @@ impl Component for Accounts {
|
|||
let accounts = try_accounts(&accounts_query).unwrap_or_default();
|
||||
let default_account = try_default_account(&default_query);
|
||||
let default_id = default_account.as_ref().map(|a| a.id);
|
||||
let microsoft_pending =
|
||||
begin.read().state().is_loading() || finish.read().state().is_loading();
|
||||
let has_microsoft = accounts_have_microsoft(&accounts);
|
||||
|
||||
let offline_name = username.read().trim().to_string();
|
||||
let offline_uuid = (!offline_name.is_empty())
|
||||
.then(|| oneclient_core::auth::offline_uuid(&offline_name).to_string());
|
||||
|
||||
let offline_error = mutation_err_text(&add_offline);
|
||||
let microsoft_error = mutation_err_text(&finish).or_else(|| mutation_err_text(&begin));
|
||||
|
||||
let on_confirm_offline = move |_| {
|
||||
let name = username.peek().trim().to_string();
|
||||
|
|
@ -144,10 +112,13 @@ impl Component for Accounts {
|
|||
)
|
||||
.child(add_bar(
|
||||
has_microsoft,
|
||||
microsoft_pending,
|
||||
microsoft_error,
|
||||
msa.pending,
|
||||
msa.error.clone(),
|
||||
move |_| show_offline.set(true),
|
||||
move |_| begin.mutate(()),
|
||||
{
|
||||
let msa = msa.clone();
|
||||
move |_| msa.start()
|
||||
},
|
||||
))
|
||||
.child(
|
||||
ScrollArea::new()
|
||||
|
|
@ -166,12 +137,7 @@ impl Component for Accounts {
|
|||
show_offline,
|
||||
)
|
||||
}))
|
||||
.maybe_child(
|
||||
pending_login
|
||||
.read()
|
||||
.clone()
|
||||
.map(|login| microsoft_dialog(login, pending_login, handled_code)),
|
||||
)
|
||||
.maybe_child(msa.popup())
|
||||
.into_element()
|
||||
}
|
||||
}
|
||||
|
|
@ -187,33 +153,6 @@ fn render_panel(account: Option<MinecraftAccount>) -> impl IntoElement {
|
|||
.corner_radius(CornerRadius::new_all(16.))
|
||||
.background(colors::page_elevated())
|
||||
.border(border_all_color(1., colors::component_border()))
|
||||
// .maybe_child(account.as_ref().map(|a| {
|
||||
// Avatar::new(a.id.to_string())
|
||||
// .width(Size::px(120.))
|
||||
// .height(Size::px(120.))
|
||||
// .into_element()
|
||||
// }))
|
||||
// .maybe_child(account.as_ref().map(|a| {
|
||||
// label()
|
||||
// .text(a.username.clone())
|
||||
// .font_size(18.)
|
||||
// .font_weight(FontWeight::SEMI_BOLD)
|
||||
// .color(colors::fg_primary())
|
||||
// .into_element()
|
||||
// }))
|
||||
// .maybe_child(account.is_none().then(|| {
|
||||
// label()
|
||||
// .text("No account selected")
|
||||
// .font_size(14.)
|
||||
// .color(colors::fg_secondary())
|
||||
// .into_element()
|
||||
// }))
|
||||
// .child(
|
||||
// label()
|
||||
// .text("3D preview coming soon")
|
||||
// .font_size(11.)
|
||||
// .color(colors::fg_secondary()),
|
||||
// )
|
||||
.maybe_child(account.as_ref().map(|account| {
|
||||
PlayerModel::new(account.id)
|
||||
.yaw(-0.5)
|
||||
|
|
@ -509,166 +448,6 @@ fn account_row(
|
|||
.into_element()
|
||||
}
|
||||
|
||||
fn microsoft_dialog(
|
||||
login: MinecraftLogin,
|
||||
mut pending_login: State<Option<MinecraftLogin>>,
|
||||
mut handled_code: State<Option<String>>,
|
||||
) -> impl IntoElement {
|
||||
let body = match &login {
|
||||
MinecraftLogin::DeviceCode(flow) => {
|
||||
device_code_dialog_body(flow.user_code.clone(), flow.verification_uri.clone())
|
||||
.into_element()
|
||||
}
|
||||
MinecraftLogin::Browser(flow) => {
|
||||
browser_dialog_body(flow.auth_url.clone()).into_element()
|
||||
}
|
||||
};
|
||||
|
||||
let mut close_pending = pending_login;
|
||||
let mut close_handled = handled_code;
|
||||
|
||||
OverlayPopup::new()
|
||||
.on_close(move |()| {
|
||||
pending_login.set(None);
|
||||
handled_code.set(None);
|
||||
})
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::window_percent(100.))
|
||||
.height(Size::window_percent(100.))
|
||||
.center()
|
||||
.child(
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::px(420.))
|
||||
.max_width(Size::window_percent(90.))
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(18.)
|
||||
.padding(Gaps::new_all(28.))
|
||||
.corner_radius(CornerRadius::new_all(16.))
|
||||
.background(colors::page_elevated())
|
||||
.border(border_all_color(1., colors::component_border()))
|
||||
.child(
|
||||
label()
|
||||
.text("Sign in to Microsoft")
|
||||
.font_size(18.)
|
||||
.font_weight(FontWeight::SEMI_BOLD)
|
||||
.color(colors::fg_primary()),
|
||||
)
|
||||
.child(body)
|
||||
.child(waiting_row())
|
||||
.child(
|
||||
Button::new()
|
||||
.ghost()
|
||||
.on_press(move |_| {
|
||||
close_pending.set(None);
|
||||
close_handled.set(None);
|
||||
})
|
||||
.text("Cancel"),
|
||||
),
|
||||
),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
|
||||
fn waiting_row() -> impl IntoElement {
|
||||
rect()
|
||||
.horizontal()
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(6.)
|
||||
.child(
|
||||
Icon::new(IconType::Loading02)
|
||||
.size(13.)
|
||||
.color(colors::brand()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text("Waiting for you to finish signing in...")
|
||||
.font_size(12.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
|
||||
fn browser_dialog_body(auth_url: String) -> impl IntoElement {
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::fill())
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(18.)
|
||||
.child(
|
||||
label()
|
||||
.text("Your browser has opened the Microsoft sign-in page. Complete sign-in there and you'll be brought back automatically.")
|
||||
.font_size(13.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.child(
|
||||
Button::new()
|
||||
.primary()
|
||||
.on_press(move |_| platform::open_url(&auth_url))
|
||||
.child(Icon::new(IconType::LinkExternal01).size(16.))
|
||||
.text("Open in browser again"),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
|
||||
fn device_code_dialog_body(code: String, verification_uri: String) -> impl IntoElement {
|
||||
let copy_code = code.clone();
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::fill())
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(18.)
|
||||
.child(
|
||||
label()
|
||||
.text("Enter this code on the Microsoft sign-in page:")
|
||||
.font_size(13.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.center()
|
||||
.padding(Gaps::new_symmetric(20., 16.))
|
||||
.corner_radius(CornerRadius::new_all(14.))
|
||||
.background(colors::component_bg())
|
||||
.border(border_all_color(1., colors::brand()))
|
||||
.child(
|
||||
label()
|
||||
.text(code)
|
||||
.font_size(48.)
|
||||
.font_weight(FontWeight::BOLD)
|
||||
.color(colors::fg_primary()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.main_align(Alignment::Center)
|
||||
.spacing(8.)
|
||||
.child(
|
||||
Button::new()
|
||||
.secondary()
|
||||
.on_press(move |_| {
|
||||
if let Err(err) = Clipboard::set(copy_code.clone()) {
|
||||
tracing::warn!("clipboard copy failed: {err:?}");
|
||||
}
|
||||
})
|
||||
.child(Icon::new(IconType::Copy01).size(16.))
|
||||
.text("Copy code"),
|
||||
)
|
||||
.child(
|
||||
Button::new()
|
||||
.primary()
|
||||
.on_press(move |_| platform::open_url(&verification_uri))
|
||||
.child(Icon::new(IconType::LinkExternal01).size(16.))
|
||||
.text("Open in browser"),
|
||||
),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
|
||||
fn kind_badge(kind: AccountKind) -> impl IntoElement {
|
||||
let (icon, text) = match kind {
|
||||
AccountKind::Microsoft => (IconType::Globe01, "Microsoft"),
|
||||
|
|
|
|||
|
|
@ -19,24 +19,18 @@ impl Component for SettingsLauncher {
|
|||
let v = settings.discord_enabled;
|
||||
move || v
|
||||
});
|
||||
let browser_login = use_state({
|
||||
let v = settings.microsoft_login_use_browser;
|
||||
move || v
|
||||
});
|
||||
|
||||
let mut first = use_state(|| true);
|
||||
{
|
||||
let settings = settings.clone();
|
||||
use_side_effect(move || {
|
||||
let discord = *discord_rpc.read();
|
||||
let browser = *browser_login.read();
|
||||
if *first.peek() {
|
||||
first.set(false);
|
||||
return;
|
||||
}
|
||||
let mut next = settings.clone();
|
||||
next.discord_enabled = discord;
|
||||
next.microsoft_login_use_browser = browser;
|
||||
dispatch.set_settings(next);
|
||||
});
|
||||
}
|
||||
|
|
@ -52,12 +46,6 @@ impl Component for SettingsLauncher {
|
|||
"Enable Discord Rich Presence.",
|
||||
toggle(discord_rpc),
|
||||
))
|
||||
.child(settings_row(
|
||||
IconType::Globe01,
|
||||
"Browser sign-in",
|
||||
"Sign in to Microsoft by opening your browser instead of entering a code.",
|
||||
toggle(browser_login),
|
||||
))
|
||||
.child(section_header("FOLDERS AND FILES"))
|
||||
.child(settings_row(
|
||||
IconType::Folder,
|
||||
|
|
|
|||
|
|
@ -1,17 +1,10 @@
|
|||
use freya::prelude::*;
|
||||
use freya::query::{MutationCapability, MutationStateData, UseMutation};
|
||||
use freya::text_edit::Clipboard;
|
||||
use oneclient_core::auth::{MinecraftAccount, MinecraftLogin};
|
||||
use oneclient_core::auth::MinecraftAccount;
|
||||
|
||||
use crate::components::{Avatar, Button, Icon, IconType, OverlayPopup};
|
||||
use crate::hooks::{
|
||||
login_code_already_handled, try_default_account, use_begin_microsoft_login,
|
||||
use_current_account, use_finish_microsoft_login,
|
||||
};
|
||||
use crate::platform;
|
||||
use crate::components::{Avatar, Button, Icon, IconType, use_microsoft_login};
|
||||
use crate::hooks::{try_default_account, use_current_account};
|
||||
use crate::routes::Route;
|
||||
use crate::theme::colors;
|
||||
use crate::ui::border_all_color;
|
||||
use crate::view::onboarding::{
|
||||
onboarding_illustration, onboarding_nav, onboarding_page, step_heading,
|
||||
};
|
||||
|
|
@ -22,42 +15,10 @@ pub struct OnboardingAccount;
|
|||
impl Component for OnboardingAccount {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let account_query = use_current_account();
|
||||
|
||||
let begin = use_begin_microsoft_login();
|
||||
let finish = use_finish_microsoft_login();
|
||||
|
||||
let mut pending_login = use_state(|| None::<MinecraftLogin>);
|
||||
|
||||
use_side_effect(move || {
|
||||
let login = match &*begin.read().state() {
|
||||
MutationStateData::Settled { res: Ok(login), .. } => Some(login.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let Some(login) = login else { return };
|
||||
if login_code_already_handled(login.dedupe_key()) {
|
||||
return;
|
||||
}
|
||||
// Browser flow: open the system browser straight away so the user
|
||||
// lands on the Microsoft sign-in page without copying a code.
|
||||
if let Some(url) = login.browser_url() {
|
||||
platform::open_url(url);
|
||||
}
|
||||
finish.mutate(login.clone());
|
||||
pending_login.set(Some(login));
|
||||
});
|
||||
|
||||
use_side_effect(move || {
|
||||
if matches!(&*finish.read().state(), MutationStateData::Settled { .. })
|
||||
&& pending_login.peek().is_some()
|
||||
{
|
||||
pending_login.set(None);
|
||||
}
|
||||
});
|
||||
let msa = use_microsoft_login();
|
||||
|
||||
let account = try_default_account(&account_query);
|
||||
let has_account = account.is_some();
|
||||
let pending = begin.read().state().is_loading() || finish.read().state().is_loading();
|
||||
let error = mutation_err_text(&finish).or_else(|| mutation_err_text(&begin));
|
||||
|
||||
let content = rect()
|
||||
.vertical()
|
||||
|
|
@ -69,7 +30,11 @@ impl Component for OnboardingAccount {
|
|||
))
|
||||
.child(match &account {
|
||||
Some(account) => account_preview(account).into_element(),
|
||||
None => sign_in_card(pending, error, move |_| begin.mutate(())).into_element(),
|
||||
None => {
|
||||
let start = msa.clone();
|
||||
sign_in_card(msa.pending, msa.error.clone(), move |_| start.start())
|
||||
.into_element()
|
||||
}
|
||||
})
|
||||
.into_element();
|
||||
|
||||
|
|
@ -87,26 +52,7 @@ impl Component for OnboardingAccount {
|
|||
.width(Size::fill())
|
||||
.height(Size::fill())
|
||||
.child(page)
|
||||
.maybe_child(
|
||||
pending_login
|
||||
.read()
|
||||
.clone()
|
||||
.map(|login| microsoft_dialog(login, pending_login)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn mutation_err_text<M>(mutation: &UseMutation<M>) -> Option<String>
|
||||
where
|
||||
M: MutationCapability,
|
||||
M::Err: std::fmt::Display,
|
||||
{
|
||||
match &*mutation.read().state() {
|
||||
MutationStateData::Settled { res: Err(err), .. } => Some(err.to_string()),
|
||||
MutationStateData::Loading {
|
||||
res: Some(Err(err)),
|
||||
} => Some(err.to_string()),
|
||||
_ => None,
|
||||
.maybe_child(msa.popup())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -114,23 +60,7 @@ fn account_preview(account: &MinecraftAccount) -> impl IntoElement {
|
|||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
// .height(Size::px(260.))
|
||||
.spacing(24.)
|
||||
// .cross_align(Alignment::Center)
|
||||
// .child(
|
||||
// rect()
|
||||
// .width(Size::px(200.))
|
||||
// .height(Size::fill())
|
||||
// .corner_radius(CornerRadius::new_all(16.))
|
||||
// .background(colors::page_elevated())
|
||||
// .border(border_all_color(1., colors::component_border()))
|
||||
// .child(
|
||||
// PlayerModel::new(account.id)
|
||||
// .yaw(-0.5)
|
||||
// .width(Size::fill())
|
||||
// .height(Size::fill()),
|
||||
// ),
|
||||
// )
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
|
|
@ -205,153 +135,3 @@ fn sign_in_card(
|
|||
}))
|
||||
.into_element()
|
||||
}
|
||||
|
||||
fn microsoft_dialog(
|
||||
login: MinecraftLogin,
|
||||
mut pending_login: State<Option<MinecraftLogin>>,
|
||||
) -> impl IntoElement {
|
||||
let body = match &login {
|
||||
MinecraftLogin::DeviceCode(flow) => {
|
||||
device_code_dialog_body(flow.user_code.clone(), flow.verification_uri.clone())
|
||||
.into_element()
|
||||
}
|
||||
MinecraftLogin::Browser(flow) => {
|
||||
browser_dialog_body(flow.auth_url.clone()).into_element()
|
||||
}
|
||||
};
|
||||
|
||||
OverlayPopup::new()
|
||||
.on_close(move |()| pending_login.set(None))
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::window_percent(100.))
|
||||
.height(Size::window_percent(100.))
|
||||
.center()
|
||||
.child(
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::px(420.))
|
||||
.max_width(Size::window_percent(90.))
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(18.)
|
||||
.padding(Gaps::new_all(28.))
|
||||
.corner_radius(CornerRadius::new_all(16.))
|
||||
.background(colors::page_elevated())
|
||||
.border(border_all_color(1., colors::component_border()))
|
||||
.child(
|
||||
label()
|
||||
.text("Sign in to Microsoft")
|
||||
.font_size(18.)
|
||||
.font_weight(FontWeight::SEMI_BOLD)
|
||||
.color(colors::fg_primary()),
|
||||
)
|
||||
.child(body)
|
||||
.child(waiting_row())
|
||||
.child(
|
||||
Button::new()
|
||||
.ghost()
|
||||
.on_press(move |_| pending_login.set(None))
|
||||
.text("Cancel"),
|
||||
),
|
||||
),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
|
||||
fn waiting_row() -> impl IntoElement {
|
||||
rect()
|
||||
.horizontal()
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(6.)
|
||||
.child(
|
||||
Icon::new(IconType::Loading02)
|
||||
.size(13.)
|
||||
.color(colors::brand()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text("Waiting for you to finish signing in...")
|
||||
.font_size(12.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
|
||||
fn browser_dialog_body(auth_url: String) -> impl IntoElement {
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::fill())
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(18.)
|
||||
.child(
|
||||
label()
|
||||
.text("Your browser has opened the Microsoft sign-in page. Complete sign-in there and you'll be brought back automatically.")
|
||||
.font_size(13.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.child(
|
||||
Button::new()
|
||||
.primary()
|
||||
.on_press(move |_| platform::open_url(&auth_url))
|
||||
.child(Icon::new(IconType::LinkExternal01).size(16.))
|
||||
.text("Open in browser again"),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
|
||||
fn device_code_dialog_body(code: String, verification_uri: String) -> impl IntoElement {
|
||||
let copy_code = code.clone();
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::fill())
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(18.)
|
||||
.child(
|
||||
label()
|
||||
.text("Enter this code on the Microsoft sign-in page:")
|
||||
.font_size(13.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.center()
|
||||
.padding(Gaps::new_symmetric(20., 16.))
|
||||
.corner_radius(CornerRadius::new_all(14.))
|
||||
.background(colors::component_bg())
|
||||
.border(border_all_color(1., colors::brand()))
|
||||
.child(
|
||||
label()
|
||||
.text(code)
|
||||
.font_size(48.)
|
||||
.font_weight(FontWeight::BOLD)
|
||||
.color(colors::fg_primary()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.main_align(Alignment::Center)
|
||||
.spacing(8.)
|
||||
.child(
|
||||
Button::new()
|
||||
.secondary()
|
||||
.on_press(move |_| {
|
||||
if let Err(err) = Clipboard::set(copy_code.clone()) {
|
||||
tracing::warn!("clipboard copy failed: {err:?}");
|
||||
}
|
||||
})
|
||||
.child(Icon::new(IconType::Copy01).size(16.))
|
||||
.text("Copy code"),
|
||||
)
|
||||
.child(
|
||||
Button::new()
|
||||
.primary()
|
||||
.on_press(move |_| platform::open_url(&verification_uri))
|
||||
.child(Icon::new(IconType::LinkExternal01).size(16.))
|
||||
.text("Open in browser"),
|
||||
),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
use std::process::Command;
|
||||
|
||||
use oneclient_core::auth::{self, MinecraftLogin};
|
||||
use oneclient_core::auth::{self};
|
||||
use oneclient_core::{dev, logger, LauncherResult};
|
||||
|
||||
#[tokio::main]
|
||||
|
|
@ -9,23 +9,18 @@ async fn main() -> LauncherResult<()> {
|
|||
logger::init_debug()?;
|
||||
dev::initialize().await?;
|
||||
|
||||
let flow = auth::begin_microsoft_login().await?;
|
||||
let session = auth::begin_microsoft_login().await?;
|
||||
|
||||
match &flow {
|
||||
MinecraftLogin::DeviceCode(device) => {
|
||||
println!("{}\n", device.message);
|
||||
println!("Verification URL: {}", device.verification_uri);
|
||||
println!("Enter this code: {}\n", device.user_code);
|
||||
try_open_browser(&device.verification_uri);
|
||||
}
|
||||
MinecraftLogin::Browser(browser) => {
|
||||
println!("Opening browser to sign in...\n");
|
||||
try_open_browser(&browser.auth_url);
|
||||
}
|
||||
}
|
||||
println!("Sign in with EITHER method:\n");
|
||||
println!(" Browser: opening {}", session.browser.auth_url);
|
||||
try_open_browser(&session.browser.auth_url);
|
||||
println!(
|
||||
" Device code: go to {} and enter {}\n",
|
||||
session.device.verification_uri, session.device.user_code
|
||||
);
|
||||
|
||||
println!("Waiting for you to sign in...");
|
||||
let account = auth::finish_microsoft_login(flow).await?;
|
||||
let account = auth::finish_microsoft_login(session).await?;
|
||||
|
||||
println!(
|
||||
"Signed in as {} ({})\n token expires: {}",
|
||||
|
|
|
|||
|
|
@ -52,23 +52,25 @@ pub struct BrowserLogin {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum MinecraftLogin {
|
||||
DeviceCode(DeviceCodeLogin),
|
||||
Browser(BrowserLogin),
|
||||
pub struct MicrosoftLoginSession {
|
||||
pub browser: BrowserLogin,
|
||||
pub device: DeviceCodeLogin,
|
||||
}
|
||||
|
||||
impl MinecraftLogin {
|
||||
impl MicrosoftLoginSession {
|
||||
pub fn dedupe_key(&self) -> &str {
|
||||
match self {
|
||||
MinecraftLogin::DeviceCode(flow) => &flow.user_code,
|
||||
MinecraftLogin::Browser(flow) => &flow.state,
|
||||
}
|
||||
&self.browser.state
|
||||
}
|
||||
|
||||
pub fn browser_url(&self) -> Option<&str> {
|
||||
match self {
|
||||
MinecraftLogin::Browser(flow) => Some(&flow.auth_url),
|
||||
MinecraftLogin::DeviceCode(_) => None,
|
||||
}
|
||||
pub fn auth_url(&self) -> &str {
|
||||
&self.browser.auth_url
|
||||
}
|
||||
|
||||
pub fn user_code(&self) -> &str {
|
||||
&self.device.user_code
|
||||
}
|
||||
|
||||
pub fn verification_uri(&self) -> &str {
|
||||
&self.device.verification_uri
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,64 +7,70 @@ mod data;
|
|||
pub use error::{AuthError, MinecraftAuthError, MinecraftAuthStep};
|
||||
pub use offline::{offline_account, offline_uuid, validate_offline_username};
|
||||
pub use store::CredentialsStore;
|
||||
pub use data::{AccountKind, BrowserLogin, DeviceCodeLogin, MinecraftAccount, MinecraftLogin};
|
||||
pub use data::{
|
||||
AccountKind, BrowserLogin, DeviceCodeLogin, MicrosoftLoginSession, MinecraftAccount,
|
||||
};
|
||||
pub use msa::PendingBrowserLogin;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::LauncherResult;
|
||||
use crate::notification::MicrosoftLoginStatus;
|
||||
use crate::state::LauncherState;
|
||||
|
||||
pub async fn begin_microsoft_login() -> LauncherResult<MinecraftLogin> {
|
||||
pub async fn begin_microsoft_login() -> LauncherResult<MicrosoftLoginSession> {
|
||||
let state = LauncherState::get()?;
|
||||
let use_browser = state.settings.read().microsoft_login_use_browser;
|
||||
let client = state.services.requester.http();
|
||||
|
||||
if use_browser {
|
||||
let (login, pending) = msa::begin_browser_login().await.map_err(AuthError::from)?;
|
||||
state
|
||||
.microsoft_logins
|
||||
.lock()
|
||||
.await
|
||||
.insert(login.state.clone(), pending);
|
||||
Ok(MinecraftLogin::Browser(login))
|
||||
} else {
|
||||
let client = state.services.requester.http();
|
||||
let flow = msa::begin_device_login(client).await.map_err(AuthError::from)?;
|
||||
Ok(MinecraftLogin::DeviceCode(flow))
|
||||
}
|
||||
let (browser, pending) = msa::begin_browser_login().await.map_err(AuthError::from)?;
|
||||
let device = msa::begin_device_login(client).await.map_err(AuthError::from)?;
|
||||
|
||||
state
|
||||
.microsoft_logins
|
||||
.lock()
|
||||
.await
|
||||
.insert(browser.state.clone(), pending);
|
||||
|
||||
Ok(MicrosoftLoginSession { browser, device })
|
||||
}
|
||||
|
||||
pub async fn finish_microsoft_login(flow: MinecraftLogin) -> LauncherResult<MinecraftAccount> {
|
||||
pub async fn finish_microsoft_login(
|
||||
session: MicrosoftLoginSession,
|
||||
) -> LauncherResult<MinecraftAccount> {
|
||||
let state = LauncherState::get()?;
|
||||
|
||||
match flow {
|
||||
MinecraftLogin::DeviceCode(flow) => {
|
||||
let mut auth = state.auth.lock().await;
|
||||
auth.finish_device_login(&flow, &state.services).await
|
||||
}
|
||||
MinecraftLogin::Browser(login) => {
|
||||
let pending = state
|
||||
.microsoft_logins
|
||||
.lock()
|
||||
.await
|
||||
.remove(&login.state)
|
||||
.ok_or(AuthError::Minecraft(
|
||||
MinecraftAuthError::BrowserLoginNotFound,
|
||||
))?;
|
||||
let pending = state
|
||||
.microsoft_logins
|
||||
.lock()
|
||||
.await
|
||||
.remove(&session.browser.state)
|
||||
.ok_or(AuthError::Minecraft(
|
||||
MinecraftAuthError::BrowserLoginNotFound,
|
||||
))?;
|
||||
|
||||
let client = state.services.requester.http();
|
||||
let progress_id = Uuid::new_v4();
|
||||
let notifier = state.services.notifier.clone();
|
||||
let account = msa::finish_browser_login(client, pending, |label, current, total| {
|
||||
notifier.send_progress(&progress_id, label, current, total);
|
||||
})
|
||||
.await
|
||||
.map_err(AuthError::from)?;
|
||||
let client = state.services.requester.http();
|
||||
let notifier = state.services.notifier.clone();
|
||||
|
||||
let mut auth = state.auth.lock().await;
|
||||
auth.commit_account(account, &state.services).await
|
||||
}
|
||||
}
|
||||
let result = msa::finish_dual_login(
|
||||
client,
|
||||
pending,
|
||||
&session.device,
|
||||
|label, current, total| {
|
||||
notifier.microsoft_login_status(Some(MicrosoftLoginStatus {
|
||||
label: label.to_string(),
|
||||
current,
|
||||
total,
|
||||
}));
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(AuthError::from);
|
||||
|
||||
state.services.notifier.microsoft_login_status(None);
|
||||
|
||||
let account = result?;
|
||||
let mut auth = state.auth.lock().await;
|
||||
auth.commit_account(account, &state.services).await
|
||||
}
|
||||
|
||||
pub async fn cancel_microsoft_login(state_token: &str) {
|
||||
|
|
|
|||
|
|
@ -29,23 +29,67 @@ const TOKEN_URL: &str = "https://login.microsoftonline.com/consumers/oauth2/v2.0
|
|||
|
||||
const AUTH_STEPS: u64 = 5;
|
||||
|
||||
const BROWSER_LOGIN_TIMEOUT: Duration = Duration::from_secs(5 * 60);
|
||||
|
||||
|
||||
pub async fn begin_device_login(client: &Client) -> Result<DeviceCodeLogin, MinecraftAuthError> {
|
||||
request_device_code(client).await
|
||||
}
|
||||
|
||||
pub async fn finish_device_login(
|
||||
pub async fn finish_dual_login(
|
||||
client: &Client,
|
||||
flow: &DeviceCodeLogin,
|
||||
pending: PendingBrowserLogin,
|
||||
device: &DeviceCodeLogin,
|
||||
on_progress: impl Fn(&str, u64, u64),
|
||||
) -> Result<MinecraftAccount, MinecraftAuthError> {
|
||||
on_progress("Waiting for sign-in", 0, AUTH_STEPS);
|
||||
let msa = poll_device_token(client, flow).await?;
|
||||
|
||||
let deadline = Duration::from_secs(device.expires_in);
|
||||
let msa = tokio::time::timeout(deadline, race_msa_token(client, &pending, device))
|
||||
.await
|
||||
.map_err(|_| MinecraftAuthError::DeviceAuthorizationExpired)??;
|
||||
|
||||
account_from_msa_token(client, msa, on_progress).await
|
||||
}
|
||||
|
||||
async fn race_msa_token(
|
||||
client: &Client,
|
||||
pending: &PendingBrowserLogin,
|
||||
device: &DeviceCodeLogin,
|
||||
) -> Result<MsaToken, MinecraftAuthError> {
|
||||
let browser = browser_msa_token(client, pending);
|
||||
let device = poll_device_token(client, device);
|
||||
tokio::pin!(browser, device);
|
||||
|
||||
let mut browser_done = false;
|
||||
let mut device_done = false;
|
||||
let mut last_err: Option<MinecraftAuthError> = None;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
res = &mut browser, if !browser_done => match res {
|
||||
Ok(token) => return Ok(token),
|
||||
Err(err) => {
|
||||
browser_done = true;
|
||||
if device_done {
|
||||
return Err(err);
|
||||
}
|
||||
last_err = Some(err);
|
||||
}
|
||||
},
|
||||
res = &mut device, if !device_done => match res {
|
||||
Ok(token) => return Ok(token),
|
||||
Err(err) => {
|
||||
device_done = true;
|
||||
if browser_done {
|
||||
return Err(err);
|
||||
}
|
||||
last_err = Some(err);
|
||||
}
|
||||
},
|
||||
else => return Err(last_err.unwrap_or(MinecraftAuthError::DeviceAuthorizationExpired)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PendingBrowserLogin {
|
||||
listener: TcpListener,
|
||||
pkce_verifier: String,
|
||||
|
|
@ -95,18 +139,12 @@ pub async fn begin_browser_login() -> Result<(BrowserLogin, PendingBrowserLogin)
|
|||
Ok((login, pending))
|
||||
}
|
||||
|
||||
pub async fn finish_browser_login(
|
||||
async fn browser_msa_token(
|
||||
client: &Client,
|
||||
pending: PendingBrowserLogin,
|
||||
on_progress: impl Fn(&str, u64, u64),
|
||||
) -> Result<MinecraftAccount, MinecraftAuthError> {
|
||||
on_progress("Waiting for sign-in", 0, AUTH_STEPS);
|
||||
let code = tokio::time::timeout(BROWSER_LOGIN_TIMEOUT, wait_for_redirect(&pending))
|
||||
.await
|
||||
.map_err(|_| MinecraftAuthError::BrowserAuthorizationExpired)??;
|
||||
|
||||
let msa = exchange_auth_code(client, &code, &pending).await?;
|
||||
account_from_msa_token(client, msa, on_progress).await
|
||||
pending: &PendingBrowserLogin,
|
||||
) -> Result<MsaToken, MinecraftAuthError> {
|
||||
let code = wait_for_redirect(pending).await?;
|
||||
exchange_auth_code(client, &code, pending).await
|
||||
}
|
||||
|
||||
async fn wait_for_redirect(pending: &PendingBrowserLogin) -> Result<String, MinecraftAuthError> {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use uuid::Uuid;
|
|||
|
||||
use super::error::AuthError;
|
||||
use super::offline::{offline_account, validate_offline_username};
|
||||
use super::data::{AccountKind, DeviceCodeLogin, MinecraftAccount};
|
||||
use super::data::{AccountKind, MinecraftAccount};
|
||||
use crate::paths;
|
||||
use crate::state::LauncherServices;
|
||||
use crate::LauncherResult;
|
||||
|
|
@ -57,23 +57,6 @@ impl CredentialsStore {
|
|||
self.users.get(&id)
|
||||
}
|
||||
|
||||
pub async fn finish_device_login(
|
||||
&mut self,
|
||||
flow: &DeviceCodeLogin,
|
||||
services: &LauncherServices,
|
||||
) -> LauncherResult<MinecraftAccount> {
|
||||
let client = services.requester.http();
|
||||
let progress_id = Uuid::new_v4();
|
||||
let notifier = services.notifier.clone();
|
||||
let account = super::msa::finish_device_login(client, flow, |label, current, total| {
|
||||
notifier.send_progress(&progress_id, label, current, total);
|
||||
})
|
||||
.await
|
||||
.map_err(AuthError::from)?;
|
||||
|
||||
self.commit_account(account, services).await
|
||||
}
|
||||
|
||||
pub async fn commit_account(
|
||||
&mut self,
|
||||
account: MinecraftAccount,
|
||||
|
|
|
|||
|
|
@ -160,6 +160,9 @@ fn spawn_notification_handler(mut rx: mpsc::UnboundedReceiver<Notification>) {
|
|||
Notification::InvalidateClusters => {}
|
||||
Notification::InvalidateJava => {}
|
||||
Notification::SyncComplete => {}
|
||||
Notification::MicrosoftLoginStatus(status) => {
|
||||
mp.suspend(|| tracing::info!(?status, "microsoft login status"));
|
||||
}
|
||||
Notification::GameStage { cluster_id, stage } => {
|
||||
mp.suspend(|| tracing::info!(cluster_id, ?stage, "game stage"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,17 @@ pub enum Notification {
|
|||
InvalidateJava,
|
||||
|
||||
SyncComplete,
|
||||
|
||||
/// Live status of Microsoft Auth
|
||||
/// `None` clears the status
|
||||
MicrosoftLoginStatus(Option<MicrosoftLoginStatus>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MicrosoftLoginStatus {
|
||||
pub label: String,
|
||||
pub current: u64,
|
||||
pub total: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
pub use data::{
|
||||
GroupedProgressEvent, LaunchStage, Notification, NotificationLevel, PromptKind, TaskPhase,
|
||||
UserChoice,
|
||||
GroupedProgressEvent, LaunchStage, MicrosoftLoginStatus, Notification, NotificationLevel,
|
||||
PromptKind, TaskPhase, UserChoice,
|
||||
};
|
||||
pub use grouped::{GroupedProgressChild, GroupedProgressSession};
|
||||
pub use service::NotificationService;
|
||||
|
|
@ -14,5 +14,5 @@ pub enum NotificationError {
|
|||
#[error("Notification service is down")]
|
||||
ServiceDown,
|
||||
#[error("Notification prompt timed out")]
|
||||
PromptTimedOut
|
||||
}
|
||||
PromptTimedOut,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ use tokio::sync::{mpsc, oneshot};
|
|||
use uuid::Uuid;
|
||||
|
||||
use crate::notification::{
|
||||
LaunchStage, Notification, NotificationError, NotificationLevel, PromptKind, UserChoice,
|
||||
LaunchStage, MicrosoftLoginStatus, Notification, NotificationError, NotificationLevel,
|
||||
PromptKind, UserChoice,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -80,6 +81,15 @@ impl NotificationService {
|
|||
reply_rx.await.map_err(|_| NotificationError::ServiceDown)
|
||||
}
|
||||
|
||||
pub fn microsoft_login_status(&self, status: Option<MicrosoftLoginStatus>) {
|
||||
if let Err(err) = self
|
||||
.channel
|
||||
.send(Notification::MicrosoftLoginStatus(status))
|
||||
{
|
||||
tracing::error!("{err}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_info(&self, title: &str, body: &str) {
|
||||
self.send_message(title, body, NotificationLevel::Info);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ pub struct LauncherSettings {
|
|||
pub auto_update: bool,
|
||||
pub enable_gamemode: bool,
|
||||
pub discord_enabled: bool,
|
||||
pub microsoft_login_use_browser: bool,
|
||||
pub max_concurrent_requests: usize,
|
||||
pub global_game_settings: GameSettingsProfile,
|
||||
pub allow_parallel_running_clusters: bool,
|
||||
|
|
@ -59,7 +58,6 @@ impl Default for LauncherSettings {
|
|||
log_debug: false,
|
||||
auto_update: true,
|
||||
discord_enabled: true,
|
||||
microsoft_login_use_browser: true,
|
||||
enable_gamemode: false,
|
||||
max_concurrent_requests: 25,
|
||||
global_game_settings: GameSettingsProfile::default_global_profile(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue