Squashed commit of the following:
commit a782b683f30d127936115d0e31a70de1d8268694
Author: Wyvest <wyvestbusiness@gmail.com>
Date: Wed Feb 25 20:56:05 2026 -0500
remove debug logging
commit 640950728f480f1b8c7939a720124ea581c227af
Author: Wyvest <wyvestbusiness@gmail.com>
Date: Wed Feb 25 14:40:03 2026 -0500
add some ms account error parsing
commit 014e3ecb26d75934798592f5be8cfbb437c92263
Merge: 83a6a1e ecfb98b
Author: Wyvest <wyvestbusiness@gmail.com>
Date: Wed Feb 25 13:34:16 2026 -0500
Merge branch 'main' into account-logging
commit 83a6a1e233dd4f3a3de18aabf1c7beac268d1594
Author: Wyvest <wyvestbusiness@gmail.com>
Date: Wed Feb 25 04:05:05 2026 -0500
add a lot of debug logging for accounts
This commit is contained in:
parent
5b566a03a3
commit
e7bb7d6b70
10 changed files with 525 additions and 51 deletions
|
|
@ -78,7 +78,7 @@ export type MessagePayload = { level: MessageLevel; message: string }
|
|||
|
||||
export type MetadataError = { type: "FetchError"; data: string } | { type: "NotModdedManifest"; data: string } | { type: "NotVanillaManifest"; data: string } | { type: "ParseError"; data: string } | { type: "NoMatchingLoader"; data: string } | { type: "NoMatchingVersion"; data: string }
|
||||
|
||||
export type MinecraftAuthError = { type: "PublicKeyReading"; data: string } | { type: "PKCS8Error"; data: string } | { type: "SerializeError"; data: string } | { type: "DeserializeError"; data: string } | { type: "RequestError"; data: string } | { type: "SigningError"; data: string } | { type: "HashError"; data: string } | { type: "SessionIdError"; data: string }
|
||||
export type MinecraftAuthError = { type: "PublicKeyReading"; data: string } | { type: "PKCS8Error"; data: string } | { type: "SerializeError"; data: string } | { type: "DeserializeError"; data: string } | { type: "RequestError"; data: string } | { type: "SigningError"; data: string } | { type: "HashError"; data: string } | { type: "SessionIdError"; data: string } | { type: "XboxError"; data: string }
|
||||
|
||||
/**
|
||||
* A structure of all needed Minecraft credentials for logging in and account management.
|
||||
|
|
@ -313,20 +313,12 @@ export type VersionType =
|
|||
*/
|
||||
"old_beta"
|
||||
|
||||
const ARGS_MAP = { 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'core':'{"getGameVersions":[],"getScreenshots":["id"],"setDefaultUser":["uuid"],"removeUser":["uuid"],"isClusterRunning":["cluster_id"],"killProcess":["pid"],"updateClusterById":["id","request"],"writeSettings":["setting"],"getWorlds":["id"],"setDiscordRPCMessage":["message"],"removeCape":["access_token"],"getLinkedPackages":["cluster_id"],"getPackageBody":["provider","body"],"removeCluster":["id"],"syncCluster":["cluster_id"],"launchCluster":["id","uuid","search_for_java"],"getLogByName":["id","name"],"getProfileOrDefault":["name"],"getClusterById":["id"],"getLogs":["id"],"getLoadersForVersion":["mc_version"],"openMsaLogin":[],"getRunningProcessesByClusterId":["cluster_id"],"getGlobalProfile":[],"refreshAccounts":[],"getRunningProcesses":[],"readSettings":[],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"getPackage":["provider","slug"],"fetchMinecraftProfile":["uuid"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"setClusterStage":["id","stage"],"createSettingsProfile":["name"],"open":["input"],"createCluster":["options"],"updateClusterProfile":["name","profile"],"refreshAccount":["uuid"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getUsers":[],"getUser":["uuid"],"getUsersFromAuthor":["provider","author"],"getMultiplePackages":["provider","slugs"],"changeCape":["access_token","cape_uuid"],"changeSkin":["access_token","skin_url","skin_variant"],"convertUsernameUUID":["username_uuid"],"removePackage":["cluster_id","package_hash"],"searchPackages":["provider","query"],"getClusters":[],"togglePackage":["cluster_id","package_hash"],"installModpack":["modpack","cluster_id"],"getProcessLogTail":["id","max_lines"],"fetchLoggedInProfile":["access_token"],"getDefaultUser":["fallback"]}', 'debug':'{"getBuildTimestamp":[],"getType":[],"getPackageVersion":[],"openDevTools":[],"getPlatform":[],"getLocale":[],"isInDev":[],"getArch":[],"getOsVersion":[],"getGitCommitHash":[],"getFamily":[]}', 'oneclient':'{"installUpdate":[],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"],"extractBundleOverrides":["bundle_path","cluster_id"],"checkForUpdate":[],"downloadPackageFromBundle":["package","cluster_id","bundle_name","skip_compatibility"],"updateBundlePackages":["cluster_id"],"cacheArt":["path"],"refreshArt":["path"],"isBundleSyncing":[],"getVersions":[]}', 'events':'{"message":["event"],"ingress":["event"],"process":["event"]}' }
|
||||
export type Router = { 'folders': { fromCluster: (folderName: string) => Promise<string>,
|
||||
const ARGS_MAP = { 'core':'{"updateClusterById":["id","request"],"createSettingsProfile":["name"],"convertUsernameUUID":["username_uuid"],"refreshAccount":["uuid"],"killProcess":["pid"],"removeCluster":["id"],"writeSettings":["setting"],"getScreenshots":["id"],"getUsersFromAuthor":["provider","author"],"removePackage":["cluster_id","package_hash"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"changeSkin":["access_token","skin_url","skin_variant"],"getProcessLogTail":["id","max_lines"],"searchPackages":["provider","query"],"createCluster":["options"],"getPackageBody":["provider","body"],"getClusterById":["id"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"fetchMinecraftProfile":["uuid"],"fetchLoggedInProfile":["access_token"],"getProfileOrDefault":["name"],"isClusterRunning":["cluster_id"],"getDefaultUser":["fallback"],"refreshAccounts":[],"getRunningProcesses":[],"getLogs":["id"],"openMsaLogin":[],"getClusters":[],"getLoadersForVersion":["mc_version"],"launchCluster":["id","uuid","search_for_java"],"getGlobalProfile":[],"removeUser":["uuid"],"getGameVersions":[],"setDefaultUser":["uuid"],"updateClusterProfile":["name","profile"],"setClusterStage":["id","stage"],"getUser":["uuid"],"getUsers":[],"getPackage":["provider","slug"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"syncCluster":["cluster_id"],"togglePackage":["cluster_id","package_hash"],"changeCape":["access_token","cape_uuid"],"removeCape":["access_token"],"getWorlds":["id"],"getLogByName":["id","name"],"readSettings":[],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"getLinkedPackages":["cluster_id"],"getMultiplePackages":["provider","slugs"],"getRunningProcessesByClusterId":["cluster_id"],"installModpack":["modpack","cluster_id"],"setDiscordRPCMessage":["message"],"open":["input"]}', 'oneclient':'{"refreshArt":["path"],"cacheArt":["path"],"getVersions":[],"getBundlesFor":["cluster_id"],"updateBundlePackages":["cluster_id"],"checkForUpdate":[],"installUpdate":[],"downloadPackageFromBundle":["package","cluster_id","bundle_name","skip_compatibility"],"isBundleSyncing":[],"extractBundleOverrides":["bundle_path","cluster_id"],"getClustersGroupedByMajor":[]}', 'events':'{"message":["event"],"process":["event"],"ingress":["event"]}', 'debug':'{"getGitCommitHash":[],"isInDev":[],"getPackageVersion":[],"getArch":[],"getOsVersion":[],"getLocale":[],"getBuildTimestamp":[],"getType":[],"getFamily":[],"openDevTools":[],"getPlatform":[]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}' }
|
||||
export type Router = { 'events': { ingress: (event: IngressPayload) => Promise<void>,
|
||||
message: (event: MessagePayload) => Promise<void>,
|
||||
process: (event: ProcessPayload) => Promise<void> },
|
||||
'folders': { fromCluster: (folderName: string) => Promise<string>,
|
||||
openCluster: (folderName: string) => Promise<null> },
|
||||
'debug': { openDevTools: () => Promise<void>,
|
||||
isInDev: () => Promise<boolean>,
|
||||
getArch: () => Promise<string>,
|
||||
getFamily: () => Promise<string>,
|
||||
getLocale: () => Promise<string | null>,
|
||||
getType: () => Promise<string>,
|
||||
getPlatform: () => Promise<string>,
|
||||
getOsVersion: () => Promise<string>,
|
||||
getGitCommitHash: () => Promise<string>,
|
||||
getBuildTimestamp: () => Promise<string>,
|
||||
getPackageVersion: () => Promise<string> },
|
||||
'oneclient': { getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>>,
|
||||
getBundlesFor: (clusterId: number) => Promise<ModpackArchive[]>,
|
||||
getVersions: () => Promise<OnlineClusterManifest>,
|
||||
|
|
@ -392,9 +384,17 @@ removeCape: (accessToken: string) => Promise<MojangFullPlayerProfile>,
|
|||
convertUsernameUUID: (usernameUuid: string) => Promise<MowojangProfile>,
|
||||
setDiscordRPCMessage: (message: string) => Promise<null>,
|
||||
open: (input: string) => Promise<null> },
|
||||
'events': { ingress: (event: IngressPayload) => Promise<void>,
|
||||
message: (event: MessagePayload) => Promise<void>,
|
||||
process: (event: ProcessPayload) => Promise<void> } };
|
||||
'debug': { openDevTools: () => Promise<void>,
|
||||
isInDev: () => Promise<boolean>,
|
||||
getArch: () => Promise<string>,
|
||||
getFamily: () => Promise<string>,
|
||||
getLocale: () => Promise<string | null>,
|
||||
getType: () => Promise<string>,
|
||||
getPlatform: () => Promise<string>,
|
||||
getOsVersion: () => Promise<string>,
|
||||
getGitCommitHash: () => Promise<string>,
|
||||
getBuildTimestamp: () => Promise<string>,
|
||||
getPackageVersion: () => Promise<string> } };
|
||||
|
||||
|
||||
export type { InferCommandOutput }
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import type { MinecraftCredentials } from '@/bindings.gen';
|
||||
import { AccountAvatar, Overlay } from '@/components';
|
||||
import { AccountAvatar, isMinecraftAuthError, MinecraftAuthErrorModal, Overlay } from '@/components';
|
||||
import { bindings } from '@/main';
|
||||
import { useCommandMut } from '@onelauncher/common';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
|
||||
export function AddAccountModal() {
|
||||
return (
|
||||
<Overlay.Dialog>
|
||||
<Overlay.Dialog isDismissable>
|
||||
<Overlay.Title>Add Account</Overlay.Title>
|
||||
<p className="text-fg-secondary">Add a new account to OneClient</p>
|
||||
<AddAccountModalButton />
|
||||
|
|
@ -16,9 +17,11 @@ export function AddAccountModal() {
|
|||
|
||||
export function AddAccountModalButton() {
|
||||
const queryClient = useQueryClient();
|
||||
const [authError, setAuthError] = useState<unknown>(null);
|
||||
|
||||
const { data: profile, isPending, mutate: login } = useCommandMut(bindings.core.openMsaLogin, {
|
||||
onSuccess() {
|
||||
onSuccess(data) {
|
||||
setAuthError(null);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['getUsers'],
|
||||
});
|
||||
|
|
@ -26,12 +29,21 @@ export function AddAccountModalButton() {
|
|||
queryKey: ['getDefaultUser'],
|
||||
});
|
||||
},
|
||||
onError(error) {
|
||||
console.error('[auth] AddAccountModal: openMsaLogin failed', error);
|
||||
if (isMinecraftAuthError(error))
|
||||
setAuthError(error);
|
||||
},
|
||||
});
|
||||
|
||||
const onClick = () => {
|
||||
setAuthError(null);
|
||||
login();
|
||||
};
|
||||
|
||||
if (authError)
|
||||
return <MinecraftAuthErrorModal error={authError} />;
|
||||
|
||||
return (
|
||||
profile
|
||||
? (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,184 @@
|
|||
import type { MinecraftAuthErrorInfo } from './minecraft-auth-errors';
|
||||
import { Overlay } from '@/components';
|
||||
import { bindings } from '@/main';
|
||||
import { useCommandMut } from '@onelauncher/common';
|
||||
import { Button } from '@onelauncher/common/components';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { AlertTriangleIcon, Copy01Icon, RefreshCw01Icon } from '@untitled-theme/icons-react';
|
||||
import { useState } from 'react';
|
||||
import { minecraftAuthErrors } from './minecraft-auth-errors';
|
||||
|
||||
/**
|
||||
* Extracts an Xbox error code from a LauncherError or error string.
|
||||
* Error format from backend: "Minecraft authentication error: <CODE> during MSA step <STEP>"
|
||||
*/
|
||||
export function extractXboxErrorCode(error: unknown): string | null {
|
||||
const errorStr = typeof error === 'string'
|
||||
? error
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: typeof error === 'object' && error !== null
|
||||
? JSON.stringify(error)
|
||||
: String(error);
|
||||
|
||||
const match = errorStr.match(/Minecraft authentication error:\s*(\d+)/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an error is a Minecraft/Xbox authentication error.
|
||||
*/
|
||||
export function isMinecraftAuthError(error: unknown): boolean {
|
||||
return extractXboxErrorCode(error) !== null;
|
||||
}
|
||||
|
||||
function MatchedErrorContent({
|
||||
matchedError,
|
||||
}: {
|
||||
matchedError: MinecraftAuthErrorInfo;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 w-full text-left">
|
||||
<div>
|
||||
<h3 className="text-fg-primary font-semibold text-sm mb-1">What happened</h3>
|
||||
<p className="text-fg-secondary text-sm">{matchedError.whatHappened}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-fg-primary font-semibold text-sm mb-1">How to fix it</h3>
|
||||
<ol className="list-none flex flex-col gap-1.5">
|
||||
{matchedError.stepsToFix.map((step, index) => (
|
||||
<li className="flex items-start gap-2" key={step}>
|
||||
<span className="flex-shrink-0 w-5 h-5 rounded-full bg-brand-primary/20 text-brand-primary text-xs flex items-center justify-center font-medium mt-0.5">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span
|
||||
className="text-fg-secondary text-sm [&_a]:text-brand-primary [&_a]:underline"
|
||||
dangerouslySetInnerHTML={{ __html: step }}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UnknownErrorContent() {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 w-full text-left">
|
||||
<div>
|
||||
<h3 className="text-fg-primary font-semibold text-sm mb-1">Unknown error</h3>
|
||||
<p className="text-fg-secondary text-sm">
|
||||
We don't recognize this error and can't recommend specific steps to resolve it.
|
||||
</p>
|
||||
<p className="text-fg-secondary text-sm mt-1">
|
||||
Try visiting
|
||||
{' '}
|
||||
<a
|
||||
className="text-brand-primary underline"
|
||||
href="https://www.minecraft.net/en-us/login"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Minecraft Login
|
||||
</a>
|
||||
{' '}
|
||||
and signing in, as it may prompt you with the necessary steps.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MinecraftAuthErrorModal({
|
||||
error,
|
||||
}: {
|
||||
error: unknown;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const errorCode = extractXboxErrorCode(error);
|
||||
const matchedError = errorCode
|
||||
? minecraftAuthErrors.find(e => e.errorCode === errorCode)
|
||||
: null;
|
||||
|
||||
const { isPending: isRetrying, mutate: retryLogin } = useCommandMut(bindings.core.openMsaLogin, {
|
||||
onSuccess(data) {
|
||||
console.warn('[auth] MinecraftAuthErrorModal: retry openMsaLogin succeeded', data ? `user=${data.username} id=${data.id}` : 'returned null');
|
||||
queryClient.invalidateQueries({ queryKey: ['getUsers'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['getDefaultUser'] });
|
||||
},
|
||||
onError(err) {
|
||||
console.error('[auth] MinecraftAuthErrorModal: retry openMsaLogin failed', err);
|
||||
},
|
||||
});
|
||||
|
||||
const debugInfo = typeof error === 'string'
|
||||
? error
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: JSON.stringify(error, null, 2);
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(debugInfo);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
catch {
|
||||
console.error('[auth] Failed to copy to clipboard');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Overlay.Dialog isDismissable>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-danger/20">
|
||||
<AlertTriangleIcon className="w-4 h-4 text-danger" />
|
||||
</div>
|
||||
<Overlay.Title>Sign-in Error</Overlay.Title>
|
||||
</div>
|
||||
|
||||
<p className="text-fg-secondary text-sm text-center max-w-sm">
|
||||
Something went wrong while signing in with your Microsoft account.
|
||||
{errorCode && (
|
||||
<>
|
||||
{' '}
|
||||
<span className="text-fg-secondary/60 text-xs">
|
||||
(Error code:
|
||||
{' '}
|
||||
{errorCode}
|
||||
)
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
{matchedError ? <MatchedErrorContent matchedError={matchedError} /> : <UnknownErrorContent />}
|
||||
|
||||
<div className="flex flex-col w-full gap-2 mt-1">
|
||||
<Button
|
||||
color="primary"
|
||||
isPending={isRetrying}
|
||||
onPress={() => retryLogin()}
|
||||
size="large"
|
||||
slot="close"
|
||||
>
|
||||
<RefreshCw01Icon className="w-4 h-4" />
|
||||
Sign in again
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="secondary"
|
||||
onPress={handleCopy}
|
||||
size="large"
|
||||
>
|
||||
<Copy01Icon className="w-4 h-4" />
|
||||
{copied ? 'Copied!' : 'Copy error details'}
|
||||
</Button>
|
||||
</div>
|
||||
</Overlay.Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Overlay, RawDebugInfo, SettingsRow, SettingsSwitch, useDebugInfo } from '@/components';
|
||||
import { minecraftAuthErrors, MinecraftAuthErrorModal, Overlay, RawDebugInfo, SettingsRow, SettingsSwitch, useDebugInfo } from '@/components';
|
||||
import { useSettings } from '@/hooks/useSettings';
|
||||
import { bindings } from '@/main';
|
||||
import { Button } from '@onelauncher/common/components';
|
||||
|
|
@ -10,6 +10,7 @@ import { useEffect, useState } from 'react';
|
|||
export function SuperSecretDevOptions() {
|
||||
const { createSetting } = useSettings();
|
||||
const debugInfo = useDebugInfo();
|
||||
const [previewError, setPreviewError] = useState<string | null>(null);
|
||||
|
||||
const [launcherDir, setLauncherDir] = useState('');
|
||||
|
||||
|
|
@ -48,9 +49,43 @@ export function SuperSecretDevOptions() {
|
|||
|
||||
<Button onClick={logRunningProcesses} size="normal">Console log processes</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 border-t border-component-border pt-3">
|
||||
<p className="text-fg-secondary text-xs mb-2">Auth Error Preview</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{minecraftAuthErrors.map(err => (
|
||||
<Button
|
||||
key={err.errorCode}
|
||||
onClick={() => setPreviewError(`Minecraft authentication error: ${err.errorCode} during MSA step XstsAuthorize`)}
|
||||
size="normal"
|
||||
>
|
||||
{err.errorCode}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
onClick={() => setPreviewError('Some unknown auth error occurred')}
|
||||
size="normal"
|
||||
>
|
||||
Unknown
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RawDebugInfo debugInfo={debugInfo} />
|
||||
|
||||
{previewError && (
|
||||
<Overlay
|
||||
isDismissable
|
||||
isOpen
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
setPreviewError(null);
|
||||
}}
|
||||
>
|
||||
<MinecraftAuthErrorModal error={previewError} />
|
||||
</Overlay>
|
||||
)}
|
||||
</Overlay.Dialog>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ export * from './BundleModListModal';
|
|||
export * from './DebugInfo';
|
||||
export * from './ImportSkinModal';
|
||||
export * from './KillMinecraft';
|
||||
export * from './minecraft-auth-errors';
|
||||
export * from './MinecraftAuthErrorModal';
|
||||
export * from './NoAccountPopup';
|
||||
export * from './Overlay';
|
||||
export * from './Popup';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
export interface MinecraftAuthErrorInfo {
|
||||
errorCode: string;
|
||||
whatHappened: string;
|
||||
stepsToFix: Array<string>;
|
||||
}
|
||||
|
||||
export const minecraftAuthErrors: Array<MinecraftAuthErrorInfo> = [
|
||||
{
|
||||
errorCode: '2148916222',
|
||||
whatHappened:
|
||||
'Your Minecraft/Xbox Live account requires age verification to comply with UK regulations. You must complete this before signing in.',
|
||||
stepsToFix: [
|
||||
'Go to the <a href="https://www.minecraft.net/en-us/login" target="_blank" rel="noopener noreferrer">Minecraft Login</a> page and sign in',
|
||||
'Follow the instructions to verify your age',
|
||||
'Once verified, try signing in again',
|
||||
'For additional help, visit <a href="https://support.xbox.com/en-GB/help/family-online-safety/online-safety/UK-age-verification" target="_blank" rel="noopener noreferrer">UK age verification on Xbox</a>',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916233',
|
||||
whatHappened: `This account doesn't have an Xbox profile set up or doesn't own Minecraft.`,
|
||||
stepsToFix: [
|
||||
'Make sure Minecraft is purchased on this account',
|
||||
'Visit <a href="https://www.minecraft.net/en-us/login" target="_blank" rel="noopener noreferrer">Minecraft Login</a> and sign in',
|
||||
'Complete Xbox profile setup if prompted',
|
||||
'Once finished, try signing in again',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916235',
|
||||
whatHappened: `Xbox Live isn't available in your region, so sign-in is blocked.`,
|
||||
stepsToFix: [
|
||||
'Xbox services must be supported in your country before you can sign in',
|
||||
'Check <a href="https://www.xbox.com/en-US/regions" target="_blank" rel="noopener noreferrer">Xbox Availability</a> for supported regions',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916236',
|
||||
whatHappened: 'This account requires adult verification under South Korean regulations.',
|
||||
stepsToFix: [
|
||||
'Visit <a href="https://www.xbox.com" target="_blank" rel="noopener noreferrer">Xbox</a> and sign in',
|
||||
'Complete the identity verification process',
|
||||
'Once finished, try signing in again',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916237',
|
||||
whatHappened: 'This account requires adult verification under South Korean regulations.',
|
||||
stepsToFix: [
|
||||
'Visit <a href="https://www.xbox.com" target="_blank" rel="noopener noreferrer">Xbox</a> and sign in',
|
||||
'Complete the identity verification process',
|
||||
'Once finished, try signing in again',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916238',
|
||||
whatHappened: 'This account is underage and not linked to a Microsoft family group.',
|
||||
stepsToFix: [
|
||||
'Review the <a href="https://help.minecraft.net/hc/en-us/articles/4408968616077" target="_blank" rel="noopener noreferrer">Family Setup Guide</a>',
|
||||
'Join or create a family group as instructed',
|
||||
'Once finished, try signing in again',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916227',
|
||||
whatHappened: 'This account was suspended for violating Xbox Community Standards.',
|
||||
stepsToFix: [
|
||||
'Visit <a href="https://support.xbox.com" target="_blank" rel="noopener noreferrer">Xbox Support</a> and review the enforcement details',
|
||||
'Submit an appeal if one is available',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916229',
|
||||
whatHappened: `This account is restricted and doesn't have permission to play online.`,
|
||||
stepsToFix: [
|
||||
'Have a guardian sign in to <a href="https://account.microsoft.com/family/" target="_blank" rel="noopener noreferrer">Microsoft Family</a>',
|
||||
'Update online play permissions',
|
||||
'Once finished, try signing in again',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916234',
|
||||
whatHappened: `This account hasn't accepted Xbox's Terms of Service.`,
|
||||
stepsToFix: [
|
||||
'Visit <a href="https://www.xbox.com" target="_blank" rel="noopener noreferrer">Xbox</a> and sign in',
|
||||
'Accept the Terms if prompted',
|
||||
'Once finished, try signing in again',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { MinecraftCredentials } from '@/bindings.gen';
|
||||
import { AccountAvatar, SkinViewer } from '@/components';
|
||||
import { AccountAvatar, isMinecraftAuthError, MinecraftAuthErrorModal, Overlay, SkinViewer } from '@/components';
|
||||
import { usePlayerProfile } from '@/hooks/usePlayerProfile';
|
||||
import { bindings } from '@/main';
|
||||
import { OnboardingNavigation } from '@/routes/onboarding/route';
|
||||
|
|
@ -7,6 +7,7 @@ import { useCommandMut, useCommandSuspense } from '@onelauncher/common';
|
|||
import { Button } from '@onelauncher/common/components';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const Route = createFileRoute('/onboarding/account')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -52,16 +53,26 @@ function AccountPreview({
|
|||
|
||||
function RouteComponent() {
|
||||
const queryClient = useQueryClient();
|
||||
const { data: currentAccount } = useCommandSuspense(['getDefaultUser'], () => bindings.core.getDefaultUser(true));
|
||||
const [authError, setAuthError] = useState<unknown>(null);
|
||||
const { data: currentAccount } = useCommandSuspense(['getDefaultUser'], () => {
|
||||
return bindings.core.getDefaultUser(true);
|
||||
});
|
||||
const { data: profile, isPending, mutate: login } = useCommandMut(bindings.core.openMsaLogin, {
|
||||
onSuccess() {
|
||||
onSuccess(data) {
|
||||
setAuthError(null);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['getDefaultUser'],
|
||||
});
|
||||
},
|
||||
onError(error) {
|
||||
console.error('[auth] onboarding/account: openMsaLogin failed', error);
|
||||
if (isMinecraftAuthError(error))
|
||||
setAuthError(error);
|
||||
},
|
||||
});
|
||||
|
||||
const onClick = () => {
|
||||
setAuthError(null);
|
||||
login();
|
||||
};
|
||||
|
||||
|
|
@ -100,6 +111,19 @@ function RouteComponent() {
|
|||
)}
|
||||
</div>
|
||||
<OnboardingNavigation disableNext={currentAccount === null} />
|
||||
|
||||
{authError && (
|
||||
<Overlay
|
||||
isDismissable
|
||||
isOpen
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
setAuthError(null);
|
||||
}}
|
||||
>
|
||||
<MinecraftAuthErrorModal error={authError} />
|
||||
</Overlay>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -321,6 +321,7 @@ pub struct ProfileUpdate {
|
|||
#[taurpc::ipc_type]
|
||||
pub struct TauriLauncherApiImpl;
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[taurpc::resolvers]
|
||||
impl TauriLauncherApi for TauriLauncherApiImpl {
|
||||
// MARK: Impl: clusters
|
||||
|
|
@ -622,7 +623,13 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
|
|||
) -> LauncherResult<Option<MinecraftCredentials>> {
|
||||
use tauri::Manager;
|
||||
|
||||
let flow = api::credentials::begin().await?;
|
||||
let flow = api::credentials::begin().await.map_err(|e| {
|
||||
tracing::error!(
|
||||
"[auth] open_msa_login() - credentials::begin() failed: {:?}",
|
||||
e
|
||||
);
|
||||
e
|
||||
})?;
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
|
|
@ -643,7 +650,14 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
|
|||
.inner_size(800.0, 600.0)
|
||||
.center()
|
||||
.focused(true)
|
||||
.build()?;
|
||||
.build()
|
||||
.map_err(|e| {
|
||||
tracing::error!(
|
||||
"[auth] open_msa_login() - failed to create login window: {:?}",
|
||||
e
|
||||
);
|
||||
e
|
||||
})?;
|
||||
|
||||
win.request_user_attention(Some(tauri::UserAttentionType::Critical))?;
|
||||
|
||||
|
|
@ -652,18 +666,33 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
|
|||
return Ok(None);
|
||||
}
|
||||
|
||||
if win
|
||||
.url()?
|
||||
let current_url = match win.url() {
|
||||
Ok(u) => u,
|
||||
Err(_) => {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if current_url
|
||||
.as_str()
|
||||
.starts_with("https://login.live.com/oauth20_desktop.srf")
|
||||
&& let Some((_, code)) = win.url()?.query_pairs().find(|x| x.0 == "code")
|
||||
{
|
||||
win.close()?;
|
||||
let value = api::credentials::finish(&code.clone(), flow).await?;
|
||||
if let Some((_, code)) = win.url()?.query_pairs().find(|x| x.0 == "code") {
|
||||
win.close()?;
|
||||
let value = api::credentials::finish(&code.clone(), flow)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!(
|
||||
"[auth] open_msa_login() - credentials::finish() failed: {:?}",
|
||||
e
|
||||
);
|
||||
e
|
||||
})?;
|
||||
|
||||
return Ok(Some(value));
|
||||
return Ok(Some(value));
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ pub async fn start_logger() {
|
|||
Core::get()
|
||||
.logger_filter
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("{}=info", env!("CARGO_PKG_NAME"))),
|
||||
.unwrap_or_else(|| format!("{}=debug", env!("CARGO_PKG_NAME"))),
|
||||
)
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -36,18 +36,17 @@ impl CredentialsStore {
|
|||
#[tracing::instrument]
|
||||
pub async fn initialize() -> LauncherResult<Self> {
|
||||
let path = Dirs::get_auth_file().await?;
|
||||
let store = io::read_json(&path).await.ok();
|
||||
let store = io::read_json::<Self>(&path).await.ok();
|
||||
|
||||
store.map_or_else(
|
||||
|| {
|
||||
Ok(Self {
|
||||
users: HashMap::new(),
|
||||
token: None,
|
||||
default_user: None,
|
||||
})
|
||||
},
|
||||
Ok,
|
||||
)
|
||||
if let Some(s) = store {
|
||||
Ok(s)
|
||||
} else {
|
||||
Ok(Self {
|
||||
users: HashMap::new(),
|
||||
token: None,
|
||||
default_user: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Save the current Minecraft credentials.
|
||||
|
|
@ -143,6 +142,10 @@ impl CredentialsStore {
|
|||
}),
|
||||
Err(err) => {
|
||||
if valid_date {
|
||||
tracing::error!(
|
||||
"[auth] begin() - sisu_authenticate failed (device token was fresh): {:?}",
|
||||
err
|
||||
);
|
||||
Err(err.into())
|
||||
} else {
|
||||
let (key, token, current_date, _) = self.refresh(Utc::now(), false).await?;
|
||||
|
|
@ -290,6 +293,7 @@ impl CredentialsStore {
|
|||
_ => {}
|
||||
}
|
||||
|
||||
tracing::error!("[auth] get_default() - token refresh failed: {:?}", err);
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -397,7 +401,7 @@ pub async fn device_token(
|
|||
key: &DeviceKey,
|
||||
current_date: DateTime<Utc>,
|
||||
) -> Result<RequestWithDate<DeviceToken>, MinecraftAuthError> {
|
||||
let res = send_signed_request(
|
||||
let res: SignedRequestResponse<DeviceToken> = send_signed_request(
|
||||
None,
|
||||
"https://device.auth.xboxlive.com/device/authenticate",
|
||||
"/device/authenticate",
|
||||
|
|
@ -474,7 +478,10 @@ async fn sisu_authenticate(
|
|||
.headers
|
||||
.get("X-SessionId")
|
||||
.and_then(|x| x.to_str().ok())
|
||||
.ok_or_else(|| MinecraftAuthError::SessionIdError)?
|
||||
.ok_or_else(|| {
|
||||
tracing::error!("[auth] sisu_authenticate() - missing X-SessionId header in response");
|
||||
MinecraftAuthError::SessionIdError
|
||||
})?
|
||||
.to_string();
|
||||
|
||||
Ok((
|
||||
|
|
@ -533,7 +540,15 @@ async fn oauth_token(
|
|||
source,
|
||||
})?;
|
||||
|
||||
let body =
|
||||
if !status.is_success() {
|
||||
tracing::error!(
|
||||
"[auth] oauth_token() - non-success status={}, body={}",
|
||||
status,
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
let body: OAuthToken =
|
||||
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
|
||||
step: MinecraftAuthStep::OAuthToken,
|
||||
raw: text,
|
||||
|
|
@ -581,7 +596,15 @@ async fn oauth_refresh(
|
|||
source,
|
||||
})?;
|
||||
|
||||
let body =
|
||||
if !status.is_success() {
|
||||
tracing::error!(
|
||||
"[auth] oauth_refresh() - non-success status={}, body={}",
|
||||
status,
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
let body: OAuthToken =
|
||||
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
|
||||
step: MinecraftAuthStep::RefreshOAuthToken,
|
||||
raw: text,
|
||||
|
|
@ -657,7 +680,7 @@ async fn xsts_authorize(
|
|||
key: &DeviceKey,
|
||||
current_date: DateTime<Utc>,
|
||||
) -> Result<RequestWithDate<DeviceToken>, MinecraftAuthError> {
|
||||
let res = send_signed_request(
|
||||
let res: SignedRequestResponse<DeviceToken> = send_signed_request(
|
||||
None,
|
||||
"https://xsts.auth.xboxlive.com/xsts/authorize",
|
||||
"/xsts/authorize",
|
||||
|
|
@ -699,7 +722,13 @@ async fn minecraft_token(token: DeviceToken) -> Result<MinecraftToken, Minecraft
|
|||
.and_then(|x| x.get(0))
|
||||
.and_then(|x| x.get("uhs"))
|
||||
.and_then(|x| x.as_str().map(String::from))
|
||||
.ok_or_else(|| MinecraftAuthError::HashError)?;
|
||||
.ok_or_else(|| {
|
||||
tracing::error!(
|
||||
"[auth] minecraft_token() - failed to extract UHS from display_claims: {:?}",
|
||||
token.display_claims
|
||||
);
|
||||
MinecraftAuthError::HashError
|
||||
})?;
|
||||
|
||||
let token = token.token;
|
||||
let res = auth_retry(|| {
|
||||
|
|
@ -727,6 +756,15 @@ async fn minecraft_token(token: DeviceToken) -> Result<MinecraftToken, Minecraft
|
|||
source,
|
||||
})?;
|
||||
|
||||
if status.is_success() {
|
||||
} else {
|
||||
tracing::error!(
|
||||
"[auth] minecraft_token() - non-success status={}, body={}",
|
||||
status,
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
|
||||
step: MinecraftAuthStep::MinecraftToken,
|
||||
raw: text,
|
||||
|
|
@ -765,6 +803,15 @@ async fn minecraft_profile(token: &str) -> Result<MinecraftProfile, MinecraftAut
|
|||
source,
|
||||
})?;
|
||||
|
||||
if status.is_success() {
|
||||
} else {
|
||||
tracing::error!(
|
||||
"[auth] minecraft_profile() - non-success status={}, body={}",
|
||||
status,
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
|
||||
step: MinecraftAuthStep::MinecraftProfile,
|
||||
raw: text,
|
||||
|
|
@ -801,6 +848,15 @@ async fn minecraft_entitlements(token: &str) -> Result<MinecraftEntitlements, Mi
|
|||
source,
|
||||
})?;
|
||||
|
||||
if status.is_success() {
|
||||
} else {
|
||||
tracing::error!(
|
||||
"[auth] minecraft_entitlements() - non-success status={}, body={}",
|
||||
status,
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
|
||||
step: MinecraftAuthStep::MinecraftEntitlements,
|
||||
raw: text,
|
||||
|
|
@ -871,9 +927,25 @@ struct SignedRequestResponse<T> {
|
|||
pub body: T,
|
||||
}
|
||||
|
||||
/// Xbox error response structure returned by Xbox Live APIs on authentication failure.
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct XboxErrorResponse {
|
||||
/// The Xbox error code (e.g., 2148916233 for no Xbox profile).
|
||||
#[serde(rename = "XErr")]
|
||||
pub x_err: u64,
|
||||
/// A human-readable error message from Xbox.
|
||||
#[serde(rename = "Message")]
|
||||
pub message: String,
|
||||
/// An optional redirect URL for resolving the error.
|
||||
#[serde(rename = "Redirect")]
|
||||
pub redirect: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_wrap)]
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
#[allow(clippy::cast_sign_loss)]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn send_signed_request<T: serde::de::DeserializeOwned>(
|
||||
authorization: Option<&str>,
|
||||
url: &str,
|
||||
|
|
@ -961,6 +1033,25 @@ async fn send_signed_request<T: serde::de::DeserializeOwned>(
|
|||
.await
|
||||
.map_err(|source| MinecraftAuthError::RequestError { step, source })?;
|
||||
|
||||
if !status.is_success() {
|
||||
tracing::error!(
|
||||
"[auth] send_signed_request() - step={:?}, non-success status={}, body={}",
|
||||
step,
|
||||
status,
|
||||
body
|
||||
);
|
||||
|
||||
// Try to parse as an Xbox error response (contains XErr code)
|
||||
if let Ok(xbox_err) = serde_json::from_str::<XboxErrorResponse>(&body) {
|
||||
return Err(MinecraftAuthError::XboxError {
|
||||
step,
|
||||
error_code: xbox_err.x_err,
|
||||
message: xbox_err.message,
|
||||
redirect: xbox_err.redirect,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let body =
|
||||
serde_json::from_str(&body).map_err(|source| MinecraftAuthError::DeserializeError {
|
||||
step,
|
||||
|
|
@ -1061,4 +1152,11 @@ pub enum MinecraftAuthError {
|
|||
HashError,
|
||||
#[error("failed to read user xbox session ID")]
|
||||
SessionIdError,
|
||||
#[error("Minecraft authentication error: {error_code} during MSA step {step:?}")]
|
||||
XboxError {
|
||||
step: MinecraftAuthStep,
|
||||
error_code: u64,
|
||||
message: String,
|
||||
redirect: Option<String>,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue