Fix login UX auto-code flow and input border visibility
This commit is contained in:
@@ -40,7 +40,7 @@
|
|||||||
--radius-box: 2rem;
|
--radius-box: 2rem;
|
||||||
--size-selector: 0.3125rem;
|
--size-selector: 0.3125rem;
|
||||||
--size-field: 0.3125rem;
|
--size-field: 0.3125rem;
|
||||||
--border: 0.5px;
|
--border: 1px;
|
||||||
--depth: 0;
|
--depth: 0;
|
||||||
--noise: 1;
|
--noise: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,10 +22,12 @@ const expiresAt = ref('');
|
|||||||
const code = ref('');
|
const code = ref('');
|
||||||
const feedback = ref('');
|
const feedback = ref('');
|
||||||
const feedbackTone = ref<'success' | 'error'>('success');
|
const feedbackTone = ref<'success' | 'error'>('success');
|
||||||
|
const autoRequestTimer = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const lastRequestedEmail = ref('');
|
||||||
|
|
||||||
const requestCodeMutation = useMutation(RequestLoginCodeDocument);
|
const requestCodeMutation = useMutation(RequestLoginCodeDocument, { throws: 'never' });
|
||||||
const verifyCodeMutation = useMutation(VerifyLoginCodeDocument);
|
const verifyCodeMutation = useMutation(VerifyLoginCodeDocument, { throws: 'never' });
|
||||||
const consumeLoginTokenMutation = useMutation(ConsumeLoginTokenDocument);
|
const consumeLoginTokenMutation = useMutation(ConsumeLoginTokenDocument, { throws: 'never' });
|
||||||
|
|
||||||
const telegramBotUrl = computed(() => config.public.telegramBotUrl || '');
|
const telegramBotUrl = computed(() => config.public.telegramBotUrl || '');
|
||||||
const maxBotUrl = computed(() => config.public.maxBotUrl || '');
|
const maxBotUrl = computed(() => config.public.maxBotUrl || '');
|
||||||
@@ -51,12 +53,50 @@ async function finalizeSession(accessToken: string) {
|
|||||||
await navigateTo('/products');
|
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() {
|
async function requestCode() {
|
||||||
if (!isEmailReady.value) {
|
if (!isEmailReady.value) {
|
||||||
feedback.value = 'Введите корректный email.';
|
feedback.value = 'Введите корректный email.';
|
||||||
feedbackTone.value = 'error';
|
feedbackTone.value = 'error';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (requestCodeMutation.loading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearAutoRequestTimer();
|
||||||
|
|
||||||
feedback.value = '';
|
feedback.value = '';
|
||||||
const result = await requestCodeMutation.mutate({
|
const result = await requestCodeMutation.mutate({
|
||||||
@@ -68,20 +108,28 @@ async function requestCode() {
|
|||||||
|
|
||||||
const payload = result?.data?.requestLoginCode;
|
const payload = result?.data?.requestLoginCode;
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
feedback.value = 'Не получилось отправить код.';
|
feedback.value = requestCodeFailedMessage();
|
||||||
feedbackTone.value = 'error';
|
feedbackTone.value = 'error';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastRequestedEmail.value = normalizedEmail.value;
|
||||||
challengeToken.value = payload.challengeToken;
|
challengeToken.value = payload.challengeToken;
|
||||||
maskedEmail.value = payload.destination;
|
maskedEmail.value = payload.destination;
|
||||||
expiresAt.value = new Date(payload.expiresAt).toLocaleString();
|
expiresAt.value = new Date(payload.expiresAt).toLocaleString();
|
||||||
|
code.value = '';
|
||||||
feedback.value = `Код отправлен на ${payload.destination}.`;
|
feedback.value = `Код отправлен на ${payload.destination}.`;
|
||||||
feedbackTone.value = 'success';
|
feedbackTone.value = 'success';
|
||||||
step.value = 'verify';
|
step.value = 'verify';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function verifyCode() {
|
async function verifyCode() {
|
||||||
|
if (!challengeToken.value || !code.value.trim()) {
|
||||||
|
feedback.value = 'Введите код из письма.';
|
||||||
|
feedbackTone.value = 'error';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
feedback.value = '';
|
feedback.value = '';
|
||||||
const result = await verifyCodeMutation.mutate({
|
const result = await verifyCodeMutation.mutate({
|
||||||
input: {
|
input: {
|
||||||
@@ -114,33 +162,79 @@ async function consumeLoginToken(loginToken: string) {
|
|||||||
await finalizeSession(payload.accessToken);
|
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 () => {
|
onMounted(async () => {
|
||||||
const loginToken = typeof route.query.login_token === 'string' ? route.query.login_token : '';
|
const loginToken = typeof route.query.login_token === 'string' ? route.query.login_token : '';
|
||||||
if (loginToken) {
|
if (loginToken) {
|
||||||
await consumeLoginToken(loginToken);
|
await consumeLoginToken(loginToken);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearAutoRequestTimer();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="mx-auto flex min-h-[calc(100vh-4rem)] w-full max-w-3xl items-center py-8">
|
<section class="mx-auto flex min-h-[calc(100vh-4rem)] w-full max-w-3xl items-center py-8">
|
||||||
<div class="card w-full bg-base-100 shadow-xl">
|
<div class="card w-full border border-base-300/60 bg-base-100 shadow-xl">
|
||||||
<div class="card-body p-5 md:p-8">
|
<div class="card-body p-5 md:p-8">
|
||||||
<div class="mb-4 text-center">
|
<div class="mb-4 text-center">
|
||||||
<h1 class="text-3xl font-extrabold">Вход в личный кабинет</h1>
|
<h1 class="text-3xl font-extrabold">Вход в личный кабинет</h1>
|
||||||
<p class="mt-1 text-sm text-base-content/70">Вход через e-mail</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="step === 'request'" class="space-y-4">
|
<div v-if="step === 'request'" class="space-y-4">
|
||||||
<label class="label">
|
<fieldset class="fieldset">
|
||||||
<span class="label-text font-semibold">E-mail</span>
|
<legend class="fieldset-legend text-base font-semibold">E-mail</legend>
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
v-model="email"
|
v-model="email"
|
||||||
type="email"
|
type="email"
|
||||||
class="input input-bordered input-primary w-full"
|
class="input w-full"
|
||||||
placeholder="name@company.com"
|
placeholder="name@company.com"
|
||||||
|
@keydown.enter.prevent="requestCode"
|
||||||
|
@blur="onEmailBlur"
|
||||||
>
|
>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<div class="grid gap-2 sm:grid-cols-2">
|
<div class="grid gap-2 sm:grid-cols-2">
|
||||||
<a
|
<a
|
||||||
@@ -163,29 +257,25 @@ onMounted(async () => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<p v-if="requestCodeMutation.loading.value" class="text-sm text-base-content/70">
|
||||||
v-if="isEmailReady"
|
Проверяем e-mail и отправляем код...
|
||||||
class="btn btn-primary w-full"
|
</p>
|
||||||
:disabled="requestCodeMutation.loading.value"
|
|
||||||
@click="requestCode"
|
|
||||||
>
|
|
||||||
{{ requestCodeMutation.loading.value ? 'Отправляем…' : 'Получить код' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="space-y-4">
|
<div v-else class="space-y-4">
|
||||||
<h2 class="text-xl font-semibold">E-mail</h2>
|
<p class="text-sm text-base-content/70">E-mail: {{ maskedEmail }}</p>
|
||||||
<p class="text-sm text-base-content/70">{{ maskedEmail }}</p>
|
|
||||||
<label class="label">
|
<fieldset class="fieldset">
|
||||||
<span class="label-text font-semibold">Код</span>
|
<legend class="fieldset-legend text-base font-semibold">Код</legend>
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
v-model="code"
|
v-model="code"
|
||||||
type="text"
|
type="text"
|
||||||
maxlength="6"
|
maxlength="6"
|
||||||
class="input input-bordered input-primary w-full"
|
class="input w-full"
|
||||||
placeholder="123456"
|
placeholder="123456"
|
||||||
|
@keydown.enter.prevent="verifyCode"
|
||||||
>
|
>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary w-full"
|
class="btn btn-primary w-full"
|
||||||
@@ -195,8 +285,11 @@ onMounted(async () => {
|
|||||||
{{ verifyCodeMutation.loading.value ? 'Проверяем…' : 'Войти' }}
|
{{ verifyCodeMutation.loading.value ? 'Проверяем…' : 'Войти' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn btn-ghost w-full" @click="step = 'request'">
|
<button
|
||||||
Вернуться
|
class="btn btn-ghost w-full"
|
||||||
|
@click="step = 'request'; code = ''; feedback = ''; challengeToken = ''; maskedEmail = ''"
|
||||||
|
>
|
||||||
|
Изменить e-mail
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p class="text-xs text-base-content/60">Код действует до {{ expiresAt }}</p>
|
<p class="text-xs text-base-content/60">Код действует до {{ expiresAt }}</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user