From 87c30447a667dda837873572daa0dabee9ae3fd9 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:24:28 +0700 Subject: [PATCH] Fix login UX auto-code flow and input border visibility --- app/assets/css/main.css | 2 +- app/pages/login.vue | 167 +++++++++++++++++++++++++++++++--------- 2 files changed, 131 insertions(+), 38 deletions(-) diff --git a/app/assets/css/main.css b/app/assets/css/main.css index b4e318d..0c7fdd4 100644 --- a/app/assets/css/main.css +++ b/app/assets/css/main.css @@ -40,7 +40,7 @@ --radius-box: 2rem; --size-selector: 0.3125rem; --size-field: 0.3125rem; - --border: 0.5px; + --border: 1px; --depth: 0; --noise: 1; } diff --git a/app/pages/login.vue b/app/pages/login.vue index ab11626..ddb00ff 100644 --- a/app/pages/login.vue +++ b/app/pages/login.vue @@ -22,10 +22,12 @@ const expiresAt = ref(''); const code = ref(''); const feedback = ref(''); const feedbackTone = ref<'success' | 'error'>('success'); +const autoRequestTimer = ref | null>(null); +const lastRequestedEmail = ref(''); -const requestCodeMutation = useMutation(RequestLoginCodeDocument); -const verifyCodeMutation = useMutation(VerifyLoginCodeDocument); -const consumeLoginTokenMutation = useMutation(ConsumeLoginTokenDocument); +const requestCodeMutation = useMutation(RequestLoginCodeDocument, { throws: 'never' }); +const verifyCodeMutation = useMutation(VerifyLoginCodeDocument, { throws: 'never' }); +const consumeLoginTokenMutation = useMutation(ConsumeLoginTokenDocument, { throws: 'never' }); const telegramBotUrl = computed(() => config.public.telegramBotUrl || ''); const maxBotUrl = computed(() => config.public.maxBotUrl || ''); @@ -51,12 +53,50 @@ async function finalizeSession(accessToken: string) { await navigateTo('/products'); } +function clearAutoRequestTimer() { + if (!autoRequestTimer.value) { + return; + } + + clearTimeout(autoRequestTimer.value); + autoRequestTimer.value = null; +} + +function normalizeApolloErrorMessage(message: string) { + if (message.includes('User for this destination was not found.')) { + return 'Пользователь с таким e-mail не найден. Вход доступен только для созданных аккаунтов.'; + } + return message; +} + +function requestCodeFailedMessage() { + const mutationError = requestCodeMutation.error.value; + if (!mutationError) { + return 'Не получилось отправить код.'; + } + + const firstGraphqlError = mutationError.graphQLErrors[0]?.message; + if (firstGraphqlError) { + return normalizeApolloErrorMessage(firstGraphqlError); + } + + if (mutationError.message) { + return normalizeApolloErrorMessage(mutationError.message); + } + + return 'Не получилось отправить код.'; +} + async function requestCode() { if (!isEmailReady.value) { feedback.value = 'Введите корректный email.'; feedbackTone.value = 'error'; return; } + if (requestCodeMutation.loading.value) { + return; + } + clearAutoRequestTimer(); feedback.value = ''; const result = await requestCodeMutation.mutate({ @@ -68,20 +108,28 @@ async function requestCode() { const payload = result?.data?.requestLoginCode; if (!payload) { - feedback.value = 'Не получилось отправить код.'; + feedback.value = requestCodeFailedMessage(); feedbackTone.value = 'error'; return; } + lastRequestedEmail.value = normalizedEmail.value; challengeToken.value = payload.challengeToken; maskedEmail.value = payload.destination; expiresAt.value = new Date(payload.expiresAt).toLocaleString(); + code.value = ''; feedback.value = `Код отправлен на ${payload.destination}.`; feedbackTone.value = 'success'; step.value = 'verify'; } async function verifyCode() { + if (!challengeToken.value || !code.value.trim()) { + feedback.value = 'Введите код из письма.'; + feedbackTone.value = 'error'; + return; + } + feedback.value = ''; const result = await verifyCodeMutation.mutate({ input: { @@ -114,33 +162,79 @@ async function consumeLoginToken(loginToken: string) { await finalizeSession(payload.accessToken); } +function scheduleAutoRequest() { + clearAutoRequestTimer(); + + if (step.value !== 'request') { + return; + } + if (!isEmailReady.value) { + return; + } + if (normalizedEmail.value === lastRequestedEmail.value) { + return; + } + + autoRequestTimer.value = setTimeout(() => { + void requestCode(); + }, 450); +} + +function onEmailBlur() { + if (step.value !== 'request') { + return; + } + if (!isEmailReady.value) { + return; + } + if (normalizedEmail.value === lastRequestedEmail.value) { + return; + } + + void requestCode(); +} + +watch([normalizedEmail, step], () => { + if (step.value !== 'request') { + clearAutoRequestTimer(); + return; + } + + scheduleAutoRequest(); +}); + onMounted(async () => { const loginToken = typeof route.query.login_token === 'string' ? route.query.login_token : ''; if (loginToken) { await consumeLoginToken(loginToken); } }); + +onBeforeUnmount(() => { + clearAutoRequestTimer(); +});