feat: better auth error handling (#5403)
* add log * add log * Revert "add log" This reverts commit 2412a3de5f58fa6937b33b8e9c13fc47756670df. * add new minecraft auth error modal * add other auth errors * polish the styles * update link text * add unknown error state * pnpm prepr * fix link * fix lint
This commit is contained in:
@@ -65,6 +65,7 @@ import IncompatibilityWarningModal from '@/components/ui/install_flow/Incompatib
|
|||||||
import InstallConfirmModal from '@/components/ui/install_flow/InstallConfirmModal.vue'
|
import InstallConfirmModal from '@/components/ui/install_flow/InstallConfirmModal.vue'
|
||||||
import ModInstallModal from '@/components/ui/install_flow/ModInstallModal.vue'
|
import ModInstallModal from '@/components/ui/install_flow/ModInstallModal.vue'
|
||||||
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
|
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
|
||||||
|
import MinecraftAuthErrorModal from '@/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue'
|
||||||
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
|
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
|
||||||
import AuthGrantFlowWaitModal from '@/components/ui/modal/AuthGrantFlowWaitModal.vue'
|
import AuthGrantFlowWaitModal from '@/components/ui/modal/AuthGrantFlowWaitModal.vue'
|
||||||
import NavButton from '@/components/ui/NavButton.vue'
|
import NavButton from '@/components/ui/NavButton.vue'
|
||||||
@@ -388,6 +389,7 @@ loading.setEnabled(false)
|
|||||||
|
|
||||||
const error = useError()
|
const error = useError()
|
||||||
const errorModal = ref()
|
const errorModal = ref()
|
||||||
|
const minecraftAuthErrorModal = ref()
|
||||||
|
|
||||||
const install = useInstall()
|
const install = useInstall()
|
||||||
const modInstallModal = ref()
|
const modInstallModal = ref()
|
||||||
@@ -466,6 +468,7 @@ onMounted(() => {
|
|||||||
invoke('show_window')
|
invoke('show_window')
|
||||||
|
|
||||||
error.setErrorModal(errorModal.value)
|
error.setErrorModal(errorModal.value)
|
||||||
|
error.setMinecraftAuthErrorModal(minecraftAuthErrorModal.value)
|
||||||
|
|
||||||
install.setIncompatibilityWarningModal(incompatibilityWarningModal)
|
install.setIncompatibilityWarningModal(incompatibilityWarningModal)
|
||||||
install.setInstallConfirmModal(installConfirmModal)
|
install.setInstallConfirmModal(installConfirmModal)
|
||||||
@@ -1131,6 +1134,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
|||||||
<I18nDebugPanel />
|
<I18nDebugPanel />
|
||||||
<NotificationPanel has-sidebar />
|
<NotificationPanel has-sidebar />
|
||||||
<ErrorModal ref="errorModal" />
|
<ErrorModal ref="errorModal" />
|
||||||
|
<MinecraftAuthErrorModal ref="minecraftAuthErrorModal" />
|
||||||
<ModInstallModal ref="modInstallModal" />
|
<ModInstallModal ref="modInstallModal" />
|
||||||
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
|
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
|
||||||
<InstallConfirmModal ref="installConfirmModal" />
|
<InstallConfirmModal ref="installConfirmModal" />
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const metadata = ref({})
|
|||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
async show(errorVal, context, canClose = true, source = null) {
|
async show(errorVal, context, canClose = true, source = null) {
|
||||||
|
console.log(errorVal, context, canClose, source)
|
||||||
closable.value = canClose
|
closable.value = canClose
|
||||||
|
|
||||||
if (errorVal.message && errorVal.message.includes('Minecraft authentication error:')) {
|
if (errorVal.message && errorVal.message.includes('Minecraft authentication error:')) {
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
CopyIcon,
|
||||||
|
DropdownIcon,
|
||||||
|
LogInIcon,
|
||||||
|
MessagesSquareIcon,
|
||||||
|
WrenchIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import { Admonition, ButtonStyled, Collapsible, NewModal } from '@modrinth/ui'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
||||||
|
import { handleSevereError } from '@/store/error.js'
|
||||||
|
|
||||||
|
import { type MinecraftAuthError, minecraftAuthErrors } from './minecraft-auth-errors'
|
||||||
|
|
||||||
|
const modal = ref<InstanceType<typeof NewModal>>()
|
||||||
|
const rawError = ref<string>('')
|
||||||
|
const matchedError = ref<MinecraftAuthError | null>(null)
|
||||||
|
const debugCollapsed = ref(true)
|
||||||
|
const copied = ref(false)
|
||||||
|
const loadingSignIn = ref(false)
|
||||||
|
|
||||||
|
function show(errorVal: { message?: string }) {
|
||||||
|
rawError.value = errorVal?.message ?? String(errorVal)
|
||||||
|
|
||||||
|
matchedError.value = minecraftAuthErrors.find((e) => rawError.value.includes(e.errorCode)) ?? null
|
||||||
|
|
||||||
|
debugCollapsed.value = true
|
||||||
|
modal.value?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
modal.value?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
show,
|
||||||
|
hide,
|
||||||
|
})
|
||||||
|
|
||||||
|
async function signInAgain() {
|
||||||
|
try {
|
||||||
|
loadingSignIn.value = true
|
||||||
|
const loggedIn = await login_flow()
|
||||||
|
if (loggedIn) {
|
||||||
|
await set_default_user(loggedIn.profile.id)
|
||||||
|
}
|
||||||
|
loadingSignIn.value = false
|
||||||
|
modal.value?.hide()
|
||||||
|
} catch (err) {
|
||||||
|
loadingSignIn.value = false
|
||||||
|
handleSevereError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const debugInfo = computed(() => rawError.value || 'No error message.')
|
||||||
|
|
||||||
|
async function copyToClipboard(text: string) {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
copied.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
copied.value = false
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NewModal ref="modal" header="Sign in Failed" :max-width="'548px'">
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<Admonition
|
||||||
|
type="warning"
|
||||||
|
body=" We couldn't sign you into your Microsoft account. This may be due to account restrictions or
|
||||||
|
regional limitations."
|
||||||
|
>
|
||||||
|
</Admonition>
|
||||||
|
|
||||||
|
<!-- Matched error details -->
|
||||||
|
<div class="bg-surface-2 rounded-2xl p-4 px-5 flex flex-col gap-3">
|
||||||
|
<template v-if="matchedError">
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<h3 class="text-base font-bold m-0">What we think happened</h3>
|
||||||
|
<p class="text-sm text-secondary m-0">
|
||||||
|
{{ matchedError.whatHappened }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<h3 class="text-base font-bold m-0">How to fix it</h3>
|
||||||
|
<ol class="list-none flex flex-col gap-2 m-0 pl-0">
|
||||||
|
<li
|
||||||
|
v-for="(step, index) in matchedError.stepsToFix"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-baseline gap-2"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center justify-center shrink-0 w-5 h-5 rounded-full bg-surface-4 border border-solid border-surface-5 text-xs font-medium"
|
||||||
|
>
|
||||||
|
{{ index + 1 }}
|
||||||
|
</span>
|
||||||
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<span
|
||||||
|
class="text-sm [&_a]:text-info [&_a]:font-medium [&_a]:underline"
|
||||||
|
v-html="step"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<h3 class="text-base font-bold m-0">Unknown error</h3>
|
||||||
|
<p class="text-sm text-secondary m-0">
|
||||||
|
We don’t recognize this error and can’t recommend specific steps to resolve it.
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-secondary m-0">
|
||||||
|
Try visiting
|
||||||
|
<a
|
||||||
|
class="text-info font-medium underline hover:underline"
|
||||||
|
href="https://www.minecraft.net/en-us/login"
|
||||||
|
>Minecraft Login</a
|
||||||
|
>
|
||||||
|
and signing in, as it may prompt you with the necessary steps. You can also contact
|
||||||
|
support and we can look into it further.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action buttons -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ButtonStyled>
|
||||||
|
<a href="https://support.modrinth.com" class="!w-full" @click="modal?.hide()">
|
||||||
|
<MessagesSquareIcon /> Contact support
|
||||||
|
</a>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button :disabled="loadingSignIn" class="!w-full" @click="signInAgain">
|
||||||
|
<LogInIcon /> Sign in again
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="w-full h-[1px] bg-surface-5"></div>
|
||||||
|
|
||||||
|
<!-- Debug info -->
|
||||||
|
<div class="overflow-clip">
|
||||||
|
<button
|
||||||
|
class="flex items-center justify-between w-full bg-transparent border-0 py-4 cursor-pointer"
|
||||||
|
@click="debugCollapsed = !debugCollapsed"
|
||||||
|
>
|
||||||
|
<span class="flex items-center gap-2 text-contrast font-extrabold m-0">
|
||||||
|
<WrenchIcon class="h-4 w-4" />
|
||||||
|
Debug information
|
||||||
|
</span>
|
||||||
|
<DropdownIcon
|
||||||
|
class="h-5 w-5 text-secondary transition-transform"
|
||||||
|
:class="{ 'rotate-180': !debugCollapsed }"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<Collapsible :collapsed="debugCollapsed">
|
||||||
|
<div class="p-3 bg-surface-2 rounded-2xl text-xs flex items-start">
|
||||||
|
<div class="m-0 p-0 rounded-none bg-transparent text-sm font-mono">
|
||||||
|
{{ debugInfo }}
|
||||||
|
</div>
|
||||||
|
<ButtonStyled circular>
|
||||||
|
<button
|
||||||
|
v-tooltip="'Copy debug info'"
|
||||||
|
:disabled="copied"
|
||||||
|
@click="copyToClipboard(debugInfo)"
|
||||||
|
>
|
||||||
|
<template v-if="copied"> <CheckIcon class="text-green" /> </template>
|
||||||
|
<template v-else> <CopyIcon /> </template>
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NewModal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
export interface MinecraftAuthError {
|
||||||
|
errorCode: string
|
||||||
|
whatHappened: string
|
||||||
|
stepsToFix: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const minecraftAuthErrors: MinecraftAuthError[] = [
|
||||||
|
{
|
||||||
|
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">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">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">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">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">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">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">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">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/">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">Xbox</a> and sign in',
|
||||||
|
'Accept the Terms if prompted',
|
||||||
|
'Once finished, try signing in again',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -3,12 +3,24 @@ import { defineStore } from 'pinia'
|
|||||||
export const useError = defineStore('errorsStore', {
|
export const useError = defineStore('errorsStore', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
errorModal: null,
|
errorModal: null,
|
||||||
|
minecraftAuthErrorModal: null,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setErrorModal(ref) {
|
setErrorModal(ref) {
|
||||||
this.errorModal = ref
|
this.errorModal = ref
|
||||||
},
|
},
|
||||||
|
setMinecraftAuthErrorModal(ref) {
|
||||||
|
this.minecraftAuthErrorModal = ref
|
||||||
|
},
|
||||||
showError(error, context, closable = true, source = null) {
|
showError(error, context, closable = true, source = null) {
|
||||||
|
if (
|
||||||
|
error.message &&
|
||||||
|
error.message.includes('Minecraft authentication error:') &&
|
||||||
|
this.minecraftAuthErrorModal
|
||||||
|
) {
|
||||||
|
this.minecraftAuthErrorModal.show(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
this.errorModal.show(error, context, closable, source)
|
this.errorModal.show(error, context, closable, source)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ import _ManageIcon from './icons/manage.svg?component'
|
|||||||
import _MaximizeIcon from './icons/maximize.svg?component'
|
import _MaximizeIcon from './icons/maximize.svg?component'
|
||||||
import _MemoryStickIcon from './icons/memory-stick.svg?component'
|
import _MemoryStickIcon from './icons/memory-stick.svg?component'
|
||||||
import _MessageIcon from './icons/message.svg?component'
|
import _MessageIcon from './icons/message.svg?component'
|
||||||
|
import _MessagesSquareIcon from './icons/messages-square.svg?component'
|
||||||
import _MicrophoneIcon from './icons/microphone.svg?component'
|
import _MicrophoneIcon from './icons/microphone.svg?component'
|
||||||
import _MinimizeIcon from './icons/minimize.svg?component'
|
import _MinimizeIcon from './icons/minimize.svg?component'
|
||||||
import _MinusIcon from './icons/minus.svg?component'
|
import _MinusIcon from './icons/minus.svg?component'
|
||||||
@@ -466,6 +467,7 @@ export const ManageIcon = _ManageIcon
|
|||||||
export const MaximizeIcon = _MaximizeIcon
|
export const MaximizeIcon = _MaximizeIcon
|
||||||
export const MemoryStickIcon = _MemoryStickIcon
|
export const MemoryStickIcon = _MemoryStickIcon
|
||||||
export const MessageIcon = _MessageIcon
|
export const MessageIcon = _MessageIcon
|
||||||
|
export const MessagesSquareIcon = _MessagesSquareIcon
|
||||||
export const MicrophoneIcon = _MicrophoneIcon
|
export const MicrophoneIcon = _MicrophoneIcon
|
||||||
export const MinimizeIcon = _MinimizeIcon
|
export const MinimizeIcon = _MinimizeIcon
|
||||||
export const MinusIcon = _MinusIcon
|
export const MinusIcon = _MinusIcon
|
||||||
|
|||||||
16
packages/assets/icons/messages-square.svg
Normal file
16
packages/assets/icons/messages-square.svg
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!-- @license lucide-static v0.562.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-messages-square"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M16 10a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 14.286V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||||
|
<path d="M20 9a2 2 0 0 1 2 2v10.286a.71.71 0 0 1-1.212.502l-2.202-2.202A2 2 0 0 0 17.172 19H10a2 2 0 0 1-2-2v-1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 544 B |
Reference in New Issue
Block a user