Add readable MS errors
This commit is contained in:
parent
24d9bc3458
commit
85d1e09cee
8 changed files with 563 additions and 32 deletions
|
|
@ -1,7 +1,8 @@
|
|||
use freya::prelude::*;
|
||||
use freya::query::{MutationCapability, MutationStateData, UseMutation};
|
||||
use freya::text_edit::Clipboard;
|
||||
use oneclient_core::auth::MicrosoftLoginSession;
|
||||
use oneclient_core::LauncherError;
|
||||
use oneclient_core::auth::{AuthErrorGuidance, MicrosoftLoginSession};
|
||||
use oneclient_core::notification::MicrosoftLoginStatus;
|
||||
|
||||
use crate::components::{Button, Icon, IconType, OverlayPopup};
|
||||
|
|
@ -24,6 +25,7 @@ pub struct MicrosoftLogin {
|
|||
locked: bool,
|
||||
pub pending: bool,
|
||||
pub error: Option<String>,
|
||||
error_guidance: Option<AuthErrorGuidance>,
|
||||
}
|
||||
|
||||
impl MicrosoftLogin {
|
||||
|
|
@ -59,6 +61,7 @@ impl MicrosoftLogin {
|
|||
handle.clone(),
|
||||
handle.status.clone(),
|
||||
handle.error.clone(),
|
||||
handle.error_guidance.clone(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
@ -104,7 +107,11 @@ pub fn use_microsoft_login() -> MicrosoftLogin {
|
|||
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));
|
||||
let error_info = mutation_err_info(&finish).or_else(|| mutation_err_info(&begin));
|
||||
let (error, error_guidance) = match error_info {
|
||||
Some(info) => (Some(info.message), info.guidance),
|
||||
None => (None, None),
|
||||
};
|
||||
|
||||
MicrosoftLogin {
|
||||
begin,
|
||||
|
|
@ -115,19 +122,28 @@ pub fn use_microsoft_login() -> MicrosoftLogin {
|
|||
locked,
|
||||
pending,
|
||||
error,
|
||||
error_guidance,
|
||||
}
|
||||
}
|
||||
|
||||
fn mutation_err_text<M>(mutation: &UseMutation<M>) -> Option<String>
|
||||
struct AuthErrorInfo {
|
||||
message: String,
|
||||
guidance: Option<AuthErrorGuidance>,
|
||||
}
|
||||
|
||||
fn mutation_err_info<M>(mutation: &UseMutation<M>) -> Option<AuthErrorInfo>
|
||||
where
|
||||
M: MutationCapability,
|
||||
M::Err: std::fmt::Display,
|
||||
M: MutationCapability<Err = LauncherError>,
|
||||
{
|
||||
match &*mutation.read().state() {
|
||||
MutationStateData::Settled { res: Err(err), .. } => Some(err.to_string()),
|
||||
MutationStateData::Loading {
|
||||
let read = mutation.read();
|
||||
match &*read.state() {
|
||||
MutationStateData::Settled { res: Err(err), .. }
|
||||
| MutationStateData::Loading {
|
||||
res: Some(Err(err)),
|
||||
} => Some(err.to_string()),
|
||||
} => Some(AuthErrorInfo {
|
||||
message: err.to_string(),
|
||||
guidance: err.auth_guidance(),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -137,13 +153,36 @@ fn microsoft_dialog(
|
|||
handle: MicrosoftLogin,
|
||||
status: Option<MicrosoftLoginStatus>,
|
||||
error: Option<String>,
|
||||
guidance: Option<AuthErrorGuidance>,
|
||||
) -> impl IntoElement {
|
||||
let locked = handle.locked;
|
||||
let close_handle = handle.clone();
|
||||
let cancel_handle = handle;
|
||||
let close = handle.clone();
|
||||
login_dialog(
|
||||
login.auth_url().to_string(),
|
||||
login.user_code().to_string(),
|
||||
login.verification_uri().to_string(),
|
||||
handle.locked,
|
||||
status,
|
||||
error,
|
||||
guidance,
|
||||
move || close.cancel(),
|
||||
)
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub(crate) fn login_dialog(
|
||||
auth_url: String,
|
||||
user_code: String,
|
||||
verification_uri: String,
|
||||
locked: bool,
|
||||
status: Option<MicrosoftLoginStatus>,
|
||||
error: Option<String>,
|
||||
guidance: Option<AuthErrorGuidance>,
|
||||
on_close: impl FnMut() + Clone + 'static,
|
||||
) -> impl IntoElement {
|
||||
let mut close_scrim = on_close.clone();
|
||||
let mut close_cancel = on_close;
|
||||
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())
|
||||
.on_close(move |()| close_scrim())
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::window_percent(100.))
|
||||
|
|
@ -154,6 +193,7 @@ fn microsoft_dialog(
|
|||
.vertical()
|
||||
.width(Size::px(420.))
|
||||
.max_width(Size::window_percent(90.))
|
||||
.max_height(Size::window_percent(90.))
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(18.)
|
||||
.padding(Gaps::new_all(28.))
|
||||
|
|
@ -167,17 +207,31 @@ fn microsoft_dialog(
|
|||
.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))
|
||||
.child(
|
||||
ScrollView::new()
|
||||
.width(Size::fill())
|
||||
.height(Size::auto())
|
||||
.max_height(Size::window_percent(70.))
|
||||
.child(
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::fill())
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(18.)
|
||||
.child(browser_dialog_body(auth_url))
|
||||
.child(dialog_divider())
|
||||
.child(device_code_dialog_body(
|
||||
user_code,
|
||||
verification_uri,
|
||||
))
|
||||
.child(status_row(status, error))
|
||||
.maybe_child(guidance.map(guidance_block)),
|
||||
),
|
||||
)
|
||||
.maybe_child((!locked).then(|| {
|
||||
Button::new()
|
||||
.ghost()
|
||||
.on_press(move |_| cancel_handle.cancel())
|
||||
.on_press(move |_| close_cancel())
|
||||
.text("Cancel")
|
||||
.into_element()
|
||||
})),
|
||||
|
|
@ -254,6 +308,58 @@ fn status_row(status: Option<MicrosoftLoginStatus>, error: Option<String>) -> im
|
|||
.into_element()
|
||||
}
|
||||
|
||||
fn guidance_block(guidance: AuthErrorGuidance) -> impl IntoElement {
|
||||
let mut steps = rect()
|
||||
.vertical()
|
||||
.width(Size::fill())
|
||||
.spacing(6.)
|
||||
.child(
|
||||
label()
|
||||
.text("What you can do:")
|
||||
.font_size(12.)
|
||||
.font_weight(FontWeight::SEMI_BOLD)
|
||||
.color(colors::fg_primary()),
|
||||
);
|
||||
|
||||
for (index, step) in guidance.steps_to_fix.into_iter().enumerate() {
|
||||
steps = steps.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.spacing(6.)
|
||||
.child(
|
||||
label()
|
||||
.text(format!("{}.", index + 1))
|
||||
.font_size(12.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(step)
|
||||
.font_size(12.)
|
||||
.color(colors::fg_secondary()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::fill())
|
||||
.spacing(10.)
|
||||
.padding(Gaps::new_all(14.))
|
||||
.corner_radius(CornerRadius::new_all(12.))
|
||||
.background(colors::component_bg())
|
||||
.border(border_all_color(1., colors::component_border()))
|
||||
.child(
|
||||
label()
|
||||
.text(guidance.what_happened)
|
||||
.font_size(12.)
|
||||
.color(colors::fg_primary()),
|
||||
)
|
||||
.child(steps)
|
||||
.into_element()
|
||||
}
|
||||
|
||||
fn browser_dialog_body(auth_url: String) -> impl IntoElement {
|
||||
rect()
|
||||
.vertical()
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ pub use local_image::LocalImage;
|
|||
pub use log_viewer::LogViewer;
|
||||
pub use major_version_card::MajorVersionCard;
|
||||
pub use microsoft_login::use_microsoft_login;
|
||||
pub(crate) use microsoft_login::login_dialog;
|
||||
pub(crate) use navbar::window_controls;
|
||||
pub use navbar::{AppNavbar, OnboardingNavbar};
|
||||
pub use notifications::NotificationCenter;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ use freya::router::RouterContext;
|
|||
use oneclient_core::LauncherState;
|
||||
use oneclient_db::console::{ConsoleQueryResult, run_console_query};
|
||||
|
||||
use crate::components::{Button, Icon, IconType, TextInput, toggle};
|
||||
use oneclient_core::auth::preview_samples;
|
||||
|
||||
use crate::components::{Button, Icon, IconType, TextInput, login_dialog, toggle};
|
||||
use crate::hooks::use_dispatch;
|
||||
use crate::notifications::{ClusterUpdateSummary, NotificationAction, NotificationActionKind};
|
||||
use crate::routes::Route;
|
||||
|
|
@ -68,6 +70,11 @@ impl Component for Debug {
|
|||
vec![ClusterUpdateSimulator.into_element()],
|
||||
))
|
||||
.child(divider())
|
||||
.child(section(
|
||||
"Auth Error Guidance",
|
||||
vec![AuthGuidancePreview.into_element()],
|
||||
))
|
||||
.child(divider())
|
||||
.child(section("SQL Console", vec![SqlConsole.into_element()]))
|
||||
.child(divider())
|
||||
.child(section(
|
||||
|
|
@ -261,6 +268,71 @@ impl Component for ClusterUpdateSimulator {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct AuthGuidancePreview;
|
||||
|
||||
impl Component for AuthGuidancePreview {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let selected = use_state(|| None::<Option<usize>>);
|
||||
let samples = preview_samples();
|
||||
|
||||
let mut controls = rect().vertical().width(Size::fill()).spacing(10.).child(
|
||||
label()
|
||||
.text("Each button opens the real Microsoft sign-in popup exactly as users see it, in the chosen error state.")
|
||||
.font_size(13.)
|
||||
.color(colors::fg_secondary()),
|
||||
);
|
||||
|
||||
let mut labels: Vec<(&'static str, Option<usize>)> = vec![("No error (in progress)", None)];
|
||||
labels.extend(samples.iter().enumerate().map(|(i, s)| (s.label, Some(i))));
|
||||
|
||||
for chunk in labels.chunks(3) {
|
||||
let mut row = rect().horizontal().width(Size::fill()).spacing(12.);
|
||||
for (text, target) in chunk.iter().copied() {
|
||||
let mut sel = selected;
|
||||
let mut button = Button::new()
|
||||
.secondary()
|
||||
.text(text)
|
||||
.on_press(move |_| sel.set(Some(target)));
|
||||
if text.contains("No error") {
|
||||
button = button.child(Icon::new(IconType::CheckCircle).size(16.));
|
||||
} else {
|
||||
button = button.child(Icon::new(IconType::AlertTriangle).size(16.));
|
||||
}
|
||||
row = row.child(rect().width(Size::flex(1.0)).child(button));
|
||||
}
|
||||
for _ in chunk.len()..3 {
|
||||
row = row.child(rect().width(Size::flex(1.0)));
|
||||
}
|
||||
controls = controls.child(row);
|
||||
}
|
||||
|
||||
let current = *selected.read();
|
||||
let popup = current.map(|choice| {
|
||||
let (error, guidance) = match choice {
|
||||
None => (None, None),
|
||||
Some(i) => {
|
||||
let s = &samples[i];
|
||||
(Some(s.message.clone()), s.guidance.clone())
|
||||
}
|
||||
};
|
||||
let mut sel = selected;
|
||||
login_dialog(
|
||||
"https://login.live.com/oauth20_authorize (preview)".to_string(),
|
||||
"ABCD-EFGH".to_string(),
|
||||
"https://www.microsoft.com/link".to_string(),
|
||||
false,
|
||||
None,
|
||||
error,
|
||||
guidance,
|
||||
move || sel.set(None),
|
||||
)
|
||||
});
|
||||
|
||||
controls.maybe_child(popup).into_element()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct SqlConsole;
|
||||
|
||||
|
|
|
|||
299
packages/oneclient_core/src/auth/diagnostics.rs
Normal file
299
packages/oneclient_core/src/auth/diagnostics.rs
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
//! Adapted from Modrinth's `minecraft-auth-errors.ts`
|
||||
//! (GPL-3.0)
|
||||
|
||||
use reqwest::StatusCode;
|
||||
|
||||
use super::error::{friendly_xbox_error, MinecraftAuthError, MinecraftAuthStep};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct AuthErrorGuidance {
|
||||
pub what_happened: String,
|
||||
pub steps_to_fix: Vec<String>,
|
||||
}
|
||||
|
||||
impl AuthErrorGuidance {
|
||||
fn new(what_happened: &str, steps_to_fix: &[&str]) -> Self {
|
||||
Self {
|
||||
what_happened: what_happened.to_string(),
|
||||
steps_to_fix: steps_to_fix.iter().map(|s| (*s).to_string()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn diagnose_auth_error(err: &MinecraftAuthError) -> Option<AuthErrorGuidance> {
|
||||
match err {
|
||||
MinecraftAuthError::XboxError { error_code, .. } => guidance_for_xerr(*error_code),
|
||||
|
||||
MinecraftAuthError::ServiceError { step, status_code } => {
|
||||
guidance_for_service_error(*step, *status_code)
|
||||
}
|
||||
|
||||
MinecraftAuthError::DeserializeError { step, .. } => guidance_for_step(*step),
|
||||
|
||||
MinecraftAuthError::RequestError { .. } => Some(network_guidance()),
|
||||
|
||||
MinecraftAuthError::BrowserAuthorizationExpired
|
||||
| MinecraftAuthError::BrowserLoginNotFound => Some(AuthErrorGuidance::new(
|
||||
"The Microsoft sign-in window was closed or expired before sign-in finished.",
|
||||
&[
|
||||
"Start the sign-in again",
|
||||
"Complete the Microsoft sign-in in your browser without closing the window",
|
||||
"If your browser blocked the redirect back, allow it and try once more",
|
||||
],
|
||||
)),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn guidance_for_xerr(code: u64) -> Option<AuthErrorGuidance> {
|
||||
Some(match code {
|
||||
2_148_916_222 => AuthErrorGuidance::new(
|
||||
"This account requires age verification to comply with UK regulations before it can sign in.",
|
||||
&[
|
||||
"Go to the Minecraft login page and sign in (https://www.minecraft.net/en-us/login)",
|
||||
"Follow the instructions to verify your age",
|
||||
"Once verified, try signing in again",
|
||||
"For more help see UK age verification on Xbox (https://support.xbox.com/en-GB/help/family-online-safety/online-safety/UK-age-verification)",
|
||||
],
|
||||
),
|
||||
2_148_916_227 => AuthErrorGuidance::new(
|
||||
"This account was suspended for violating Xbox Community Standards.",
|
||||
&[
|
||||
"Visit Xbox Support and review the enforcement details (https://support.xbox.com)",
|
||||
"Submit an appeal if one is available",
|
||||
],
|
||||
),
|
||||
2_148_916_229 => AuthErrorGuidance::new(
|
||||
"This account is restricted and does not have permission to play online.",
|
||||
&[
|
||||
"Have a guardian sign in to Microsoft Family (https://account.microsoft.com/family/)",
|
||||
"Update the online play permissions",
|
||||
"Once finished, try signing in again",
|
||||
],
|
||||
),
|
||||
2_148_916_233 => AuthErrorGuidance::new(
|
||||
"This account does not have an Xbox profile set up, or does not own Minecraft.",
|
||||
&[
|
||||
"Make sure Minecraft is purchased on this account",
|
||||
"Visit the Minecraft login page and sign in (https://www.minecraft.net/en-us/login)",
|
||||
"Complete Xbox profile setup if prompted",
|
||||
"Once finished, try signing in again",
|
||||
],
|
||||
),
|
||||
2_148_916_234 => AuthErrorGuidance::new(
|
||||
"This account has not accepted Xbox's Terms of Service.",
|
||||
&[
|
||||
"Visit Xbox and sign in (https://www.xbox.com)",
|
||||
"Accept the Terms if prompted",
|
||||
"Once finished, try signing in again",
|
||||
],
|
||||
),
|
||||
2_148_916_235 => AuthErrorGuidance::new(
|
||||
"Xbox Live is not available in your region, so sign-in is blocked.",
|
||||
&[
|
||||
"Xbox services must be supported in your country before you can sign in",
|
||||
"Check Xbox availability for supported regions (https://www.xbox.com/en-US/regions)",
|
||||
],
|
||||
),
|
||||
2_148_916_236 | 2_148_916_237 => AuthErrorGuidance::new(
|
||||
"This account requires adult verification under South Korean regulations.",
|
||||
&[
|
||||
"Visit Xbox and sign in (https://www.xbox.com)",
|
||||
"Complete the identity verification process",
|
||||
"Once finished, try signing in again",
|
||||
],
|
||||
),
|
||||
2_148_916_238 => AuthErrorGuidance::new(
|
||||
"This account is underage and not linked to a Microsoft family group.",
|
||||
&[
|
||||
"Review the Minecraft Family Setup guide (https://help.minecraft.net/hc/en-us/articles/4408968616077)",
|
||||
"Join or create a family group as instructed",
|
||||
"Once finished, try signing in again",
|
||||
],
|
||||
),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn guidance_for_service_error(
|
||||
step: MinecraftAuthStep,
|
||||
status: StatusCode,
|
||||
) -> Option<AuthErrorGuidance> {
|
||||
if step == MinecraftAuthStep::MinecraftToken {
|
||||
if status == StatusCode::TOO_MANY_REQUESTS {
|
||||
return Some(AuthErrorGuidance::new(
|
||||
"Microsoft or Minecraft temporarily blocked the sign-in because there were too many recent attempts.",
|
||||
&[
|
||||
"Wait about an hour before trying again",
|
||||
"Restart OneClient after waiting",
|
||||
"Try signing in once more",
|
||||
"If it keeps happening, wait longer before retrying so the temporary limit can clear",
|
||||
],
|
||||
));
|
||||
}
|
||||
if status.is_server_error() {
|
||||
return Some(AuthErrorGuidance::new(
|
||||
"Minecraft's authentication service is returning a server error, so sign-in cannot finish right now.",
|
||||
&[
|
||||
"Wait a few minutes and try signing in again",
|
||||
"Check Xbox status for current service issues (https://support.xbox.com/xbox-live-status)",
|
||||
"Try the official Minecraft Launcher to confirm whether Minecraft sign-in is affected there too (https://www.minecraft.net/en-us/download)",
|
||||
"If the service is healthy and this keeps happening, contact support with the debug information",
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
guidance_for_step(step)
|
||||
}
|
||||
|
||||
fn guidance_for_step(step: MinecraftAuthStep) -> Option<AuthErrorGuidance> {
|
||||
Some(match step {
|
||||
MinecraftAuthStep::RefreshToken | MinecraftAuthStep::AuthCodeExchange => {
|
||||
AuthErrorGuidance::new(
|
||||
"Your saved Microsoft sign-in has expired or was revoked, so your Minecraft session could not be renewed.",
|
||||
&[
|
||||
"Sign out of the affected Minecraft account in OneClient",
|
||||
"Sign in to the account again",
|
||||
"Once the new sign-in finishes, try launching Minecraft again",
|
||||
],
|
||||
)
|
||||
}
|
||||
MinecraftAuthStep::XblAuthenticate => AuthErrorGuidance::new(
|
||||
"Xbox rejected the first sign-in step. This is most often caused by your system clock or time zone being out of sync, or by a temporary Xbox block.",
|
||||
&[
|
||||
"Open your system date and time settings",
|
||||
"Turn on automatic time zone and automatic time, if available",
|
||||
"Use the sync option in your settings to synchronise the clock",
|
||||
"Restart OneClient and try signing in again",
|
||||
"If it persists, check Xbox status (https://support.xbox.com/xbox-live-status)",
|
||||
],
|
||||
),
|
||||
MinecraftAuthStep::XstsAuthorize => AuthErrorGuidance::new(
|
||||
"Xbox rejected the request to authorize this account for Minecraft, but did not return a specific account restriction we recognise.",
|
||||
&[
|
||||
"Sign in with the official Minecraft Launcher (https://www.minecraft.net/en-us/download)",
|
||||
"Complete any prompts shown by Microsoft, Xbox, or Minecraft",
|
||||
"Try signing in to OneClient again",
|
||||
"If the official launcher also fails, follow the error shown there or contact Xbox Support",
|
||||
],
|
||||
),
|
||||
MinecraftAuthStep::MinecraftProfile => AuthErrorGuidance::new(
|
||||
"Minecraft services could not return a Java Edition profile for this account. This usually means the game was purchased recently, the Java profile is not finished being created, or the wrong Microsoft account is being used.",
|
||||
&[
|
||||
"Sign in with the official Minecraft Launcher and launch Java Edition once (https://www.minecraft.net/en-us/download)",
|
||||
"Wait up to an hour if the purchase or profile setup was recent",
|
||||
"Make sure you are using the Microsoft account that owns Minecraft",
|
||||
"Try signing in to OneClient again",
|
||||
],
|
||||
),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct AuthErrorSample {
|
||||
pub label: &'static str,
|
||||
pub message: String,
|
||||
pub guidance: Option<AuthErrorGuidance>,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn preview_samples() -> Vec<AuthErrorSample> {
|
||||
use MinecraftAuthStep::{
|
||||
MinecraftProfile, MinecraftToken, RefreshToken, XblAuthenticate, XstsAuthorize,
|
||||
};
|
||||
|
||||
let errors: Vec<(&'static str, MinecraftAuthError)> = vec![
|
||||
(
|
||||
"Xbox 403 (RUST-4)",
|
||||
MinecraftAuthError::ServiceError {
|
||||
step: XblAuthenticate,
|
||||
status_code: StatusCode::FORBIDDEN,
|
||||
},
|
||||
),
|
||||
("No Xbox profile", xerr(2_148_916_233)),
|
||||
("Region blocked", xerr(2_148_916_235)),
|
||||
("Child account", xerr(2_148_916_238)),
|
||||
("Account suspended", xerr(2_148_916_227)),
|
||||
("Restricted / no online play", xerr(2_148_916_229)),
|
||||
("ToS not accepted", xerr(2_148_916_234)),
|
||||
("UK age verification", xerr(2_148_916_222)),
|
||||
("KR adult verification", xerr(2_148_916_236)),
|
||||
(
|
||||
"Rate limited (429)",
|
||||
MinecraftAuthError::ServiceError {
|
||||
step: MinecraftToken,
|
||||
status_code: StatusCode::TOO_MANY_REQUESTS,
|
||||
},
|
||||
),
|
||||
(
|
||||
"MC service error (503)",
|
||||
MinecraftAuthError::ServiceError {
|
||||
step: MinecraftToken,
|
||||
status_code: StatusCode::SERVICE_UNAVAILABLE,
|
||||
},
|
||||
),
|
||||
(
|
||||
"Profile fetch failed",
|
||||
MinecraftAuthError::ServiceError {
|
||||
step: MinecraftProfile,
|
||||
status_code: StatusCode::NOT_FOUND,
|
||||
},
|
||||
),
|
||||
(
|
||||
"Session expired",
|
||||
MinecraftAuthError::ServiceError {
|
||||
step: RefreshToken,
|
||||
status_code: StatusCode::BAD_REQUEST,
|
||||
},
|
||||
),
|
||||
(
|
||||
"Generic Xbox reject",
|
||||
MinecraftAuthError::ServiceError {
|
||||
step: XstsAuthorize,
|
||||
status_code: StatusCode::UNAUTHORIZED,
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
let mut out: Vec<AuthErrorSample> = errors
|
||||
.into_iter()
|
||||
.map(|(label, err)| AuthErrorSample {
|
||||
label,
|
||||
message: err.to_string(),
|
||||
guidance: diagnose_auth_error(&err),
|
||||
})
|
||||
.collect();
|
||||
|
||||
out.push(AuthErrorSample {
|
||||
label: "Network unreachable",
|
||||
message: "error sending request for url (https://user.auth.xboxlive.com/user/authenticate): connection closed".to_string(),
|
||||
guidance: Some(network_guidance()),
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
fn xerr(code: u64) -> MinecraftAuthError {
|
||||
MinecraftAuthError::XboxError {
|
||||
step: MinecraftAuthStep::XstsAuthorize,
|
||||
error_code: code,
|
||||
message: friendly_xbox_error(code).unwrap_or("Xbox rejected sign-in.").to_string(),
|
||||
redirect: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn network_guidance() -> AuthErrorGuidance {
|
||||
AuthErrorGuidance::new(
|
||||
"OneClient could not connect to a Microsoft, Xbox, or Minecraft service needed for sign-in. This is usually a local network, DNS, proxy, firewall, hosts file, VPN, or antivirus issue.",
|
||||
&[
|
||||
"Restart OneClient and try signing in again",
|
||||
"Check that your internet connection is working",
|
||||
"Allow OneClient through your firewall, antivirus, proxy, VPN, and hosts file rules",
|
||||
"Try a different network, or temporarily disable VPN/proxy software if you use one",
|
||||
"If routing or DNS is the issue, a service like Cloudflare WARP can sometimes help",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ use serde::Serialize;
|
|||
|
||||
use crate::http::RequestError;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
pub enum MinecraftAuthStep {
|
||||
DeviceCodeRequest,
|
||||
DeviceCodePoll,
|
||||
|
|
@ -57,13 +57,34 @@ pub enum MinecraftAuthError {
|
|||
BrowserAuthorizationFailed { error: String },
|
||||
#[error("failed to read user hash from Xbox token")]
|
||||
HashError,
|
||||
#[error("Minecraft authentication error {error_code} during MSA step {step:?}: {message}")]
|
||||
#[error("{message}")]
|
||||
XboxError {
|
||||
step: MinecraftAuthStep,
|
||||
error_code: u64,
|
||||
message: String,
|
||||
redirect: Option<String>,
|
||||
},
|
||||
#[error(
|
||||
"The sign-in service returned an error (HTTP {status_code}) during step {step:?}. Please wait a moment and try again."
|
||||
)]
|
||||
ServiceError {
|
||||
step: MinecraftAuthStep,
|
||||
status_code: reqwest::StatusCode,
|
||||
},
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn friendly_xbox_error(code: u64) -> Option<&'static str> {
|
||||
Some(match code {
|
||||
2_148_916_227 => "This account has been banned or suspended from Xbox.",
|
||||
2_148_916_229 => "This account is a child account that must be added to a Family group by an adult before signing in.",
|
||||
2_148_916_233 => "This Microsoft account does not have an Xbox profile yet. Create one at xbox.com, then try again.",
|
||||
2_148_916_234 => "This account has not accepted the Xbox Terms of Service. Sign in at xbox.com to accept them first.",
|
||||
2_148_916_235 => "Xbox Live is not available in your country or region, so sign-in is blocked.",
|
||||
2_148_916_236 | 2_148_916_237 => "This account requires adult verification (South Korea) before it can sign in.",
|
||||
2_148_916_238 => "This is a child account. An adult must add it to a Microsoft Family group before it can sign in.",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
|||
|
|
@ -3,8 +3,12 @@ mod msa;
|
|||
mod offline;
|
||||
mod store;
|
||||
mod data;
|
||||
mod diagnostics;
|
||||
|
||||
pub use error::{AuthError, MinecraftAuthError, MinecraftAuthStep};
|
||||
pub use diagnostics::{
|
||||
diagnose_auth_error, preview_samples, AuthErrorGuidance, AuthErrorSample,
|
||||
};
|
||||
pub use offline::{offline_account, offline_uuid, validate_offline_username};
|
||||
pub use store::CredentialsStore;
|
||||
pub use data::{
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ use url::Url;
|
|||
use uuid::Uuid;
|
||||
|
||||
use super::data::{AccountKind, BrowserLogin, DeviceCodeLogin, MinecraftAccount};
|
||||
use super::error::{MinecraftAuthError, MinecraftAuthStep};
|
||||
use super::error::{friendly_xbox_error, MinecraftAuthError, MinecraftAuthStep};
|
||||
use crate::constants::{MICROSOFT_CLIENT_ID, MINECRAFT_SCOPES};
|
||||
|
||||
const DEVICE_CODE_URL: &str =
|
||||
|
|
@ -758,10 +758,20 @@ fn check_xbox_error(
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let message = friendly_xbox_error(token.x_err)
|
||||
.map(String::from)
|
||||
.unwrap_or_else(|| {
|
||||
if token.message.is_empty() {
|
||||
format!("Xbox rejected sign-in (error {}).", token.x_err)
|
||||
} else {
|
||||
token.message.clone()
|
||||
}
|
||||
});
|
||||
|
||||
Err(MinecraftAuthError::XboxError {
|
||||
step,
|
||||
error_code: token.x_err,
|
||||
message: token.message.clone(),
|
||||
message,
|
||||
redirect: token.redirect.clone(),
|
||||
})
|
||||
}
|
||||
|
|
@ -780,11 +790,17 @@ async fn parse_json_response<T: for<'de> Deserialize<'de>>(
|
|||
tracing::error!("[auth] step={step:?} status={status} body={text}");
|
||||
}
|
||||
|
||||
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
|
||||
step,
|
||||
raw: text,
|
||||
source,
|
||||
status_code: status,
|
||||
serde_json::from_str(&text).map_err(|source| {
|
||||
if !status.is_success() {
|
||||
MinecraftAuthError::ServiceError { step, status_code: status }
|
||||
} else {
|
||||
MinecraftAuthError::DeserializeError {
|
||||
step,
|
||||
raw: text,
|
||||
source,
|
||||
status_code: status,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,4 +69,16 @@ pub enum LauncherError {
|
|||
Minecraft(String),
|
||||
}
|
||||
|
||||
impl LauncherError {
|
||||
#[must_use]
|
||||
pub fn auth_guidance(&self) -> Option<crate::auth::AuthErrorGuidance> {
|
||||
match self {
|
||||
LauncherError::AuthError(crate::auth::AuthError::Minecraft(err)) => {
|
||||
crate::auth::diagnose_auth_error(err)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type LauncherResult<T> = Result<T, LauncherError>;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue