refactor: share email code confirmation screen

This commit is contained in:
3252a8
2026-05-28 17:11:15 +03:00
parent a96aaa763f
commit 6a722bfa17
5 changed files with 149 additions and 203 deletions
+3 -20
View File
@@ -143,34 +143,17 @@
0 0 26px color-mix(in srgb, var(--accent) 42%, transparent); 0 0 26px color-mix(in srgb, var(--accent) 42%, transparent);
} }
.link-email-dialog-card { .email-code-fullscreen {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
height: min(100%, 560px);
max-height: 100%;
overflow: hidden;
}
.link-email-dialog-card .dialog-head {
position: relative;
z-index: 2;
}
.link-email-dialog-card .payment-dialog-body {
min-height: 0;
}
.password-code-fullscreen {
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: 220; z-index: 220;
background: var(--bg); background: var(--bg);
overflow-y: auto; overflow-y: auto;
overscroll-behavior: contain; overscroll-behavior: contain;
animation: password-code-fullscreen-enter 0.2s ease-out both; animation: email-code-fullscreen-enter 0.2s ease-out both;
} }
@keyframes password-code-fullscreen-enter { @keyframes email-code-fullscreen-enter {
from { from {
opacity: 0; opacity: 0;
} }
+1 -2
View File
@@ -2732,8 +2732,7 @@ a {
width: min(100%, 560px); width: min(100%, 560px);
} }
.payment-dialog-card, .payment-dialog-card {
.link-email-dialog-card {
width: min(100%, 640px); width: min(100%, 640px);
} }
} }
+63 -125
View File
@@ -5,13 +5,13 @@
CheckCircle2, CheckCircle2,
CircleX, CircleX,
LockKeyhole, LockKeyhole,
RefreshCw,
TriangleAlert, TriangleAlert,
} from "$components/ui/icons.js"; } from "$components/ui/icons.js";
import { Tooltip } from "$components/ui/primitives.js"; import { Tooltip } from "$components/ui/primitives.js";
import Button from "$components/ui/button.svelte"; import Button from "$components/ui/button.svelte";
import Dialog from "$components/ui/dialog.svelte"; import Dialog from "$components/ui/dialog.svelte";
import EmailCodeScreen from "./auth/EmailCodeScreen.svelte";
import Input from "$components/ui/input.svelte"; import Input from "$components/ui/input.svelte";
import { import {
EmptyCard, EmptyCard,
@@ -391,139 +391,77 @@
</Dialog> </Dialog>
{#if setPasswordOpen && setPasswordPending} {#if setPasswordOpen && setPasswordPending}
<div class="password-code-fullscreen" role="dialog" aria-modal="true"> <div class="email-code-fullscreen" role="dialog" aria-modal="true">
<div class="phone-screen auth-screen"> <EmailCodeScreen
<header class="screen-head center-title"> bind:code={setPasswordCode}
<Button email={setPasswordEmail || ""}
variant="icon" busy={setPasswordBusy}
size="icon" resendCooldown={setPasswordResendCooldown}
onclick={closeSetPasswordDialog} status={setPasswordStatus}
aria-label={t("wa_back")} isError={setPasswordIsError}
> {t}
<ArrowLeft size={19} /> onBack={closeSetPasswordDialog}
</Button> onConfirm={confirmSetPassword}
<div> onResend={requestSetPasswordCode}
<h1>{t("wa_email_verification_title")}</h1> />
<p>{t("wa_email_sent_to", { email: setPasswordEmail || "" })}</p> </div>
</div> {/if}
<span></span>
</header> {#if linkEmailOpen && linkEmailPending}
<div class="otp-wrap"> <div class="email-code-fullscreen" role="dialog" aria-modal="true">
<label class="otp-input-wrap"> <EmailCodeScreen
<input bind:code={linkEmailCode}
bind:value={setPasswordCode} email={linkEmailPending}
inputmode="numeric" busy={linkEmailBusy}
autocomplete="one-time-code" resendCooldown={linkEmailResendCooldown}
maxlength="6" status={linkEmailStatus}
aria-label={t("wa_email_code_aria")} isError={linkEmailIsError}
/> {t}
<span class="otp-slots" aria-hidden="true"> onBack={closeLinkEmailDialog}
{#each Array.from({ length: 6 }) as _, index} onConfirm={verifyLinkEmailCode}
<span class:filled={setPasswordCode[index]}>{setPasswordCode[index] || ""}</span> onResend={requestLinkEmailCode}
{/each} />
</span>
</label>
<Button class="wide" onclick={confirmSetPassword} disabled={setPasswordBusy}>
{t("wa_confirm")}
</Button>
{#if setPasswordStatus}
<StatusMessage error={setPasswordIsError}>{setPasswordStatus}</StatusMessage>
{/if}
<button
class="link-button"
type="button"
onclick={requestSetPasswordCode}
disabled={setPasswordBusy || setPasswordResendCooldown > 0}
>
<RefreshCw size={15} />
{setPasswordResendCooldown > 0
? t("wa_auth_resend_wait", { seconds: setPasswordResendCooldown })
: t("wa_resend_code")}
</button>
</div>
</div>
</div> </div>
{/if} {/if}
<Dialog <Dialog
open={linkEmailOpen} open={linkEmailOpen && !linkEmailPending}
title={t("wa_link_email_modal_title")} title={t("wa_link_email_modal_title")}
description={linkEmailPending description={t("wa_link_email_modal_desc")}
? t("wa_email_sent_to", { email: linkEmailPending })
: t("wa_link_email_modal_desc")}
closeLabel={t("wa_close")} closeLabel={t("wa_close")}
onclose={closeLinkEmailDialog} onclose={closeLinkEmailDialog}
class={`payment-dialog-card${linkEmailPending ? " link-email-dialog-card" : ""}`} class="payment-dialog-card"
> >
<div class="payment-dialog-body"> <div class="payment-dialog-body">
{#if !linkEmailPending} <div class="field-error-wrap">
<div class="field-error-wrap"> <Tooltip.Root open={Boolean(linkEmailFieldError)}>
<Tooltip.Root open={Boolean(linkEmailFieldError)}> <Input
<Input bind:value={linkEmailValue}
bind:value={linkEmailValue} type="email"
type="email" placeholder={t("wa_email_placeholder")}
placeholder={t("wa_email_placeholder")} autocomplete="email"
autocomplete="email" class={linkEmailFieldError ? "input-error" : ""}
class={linkEmailFieldError ? "input-error" : ""} on:input={() => (linkEmailFieldError = "")}
on:input={() => (linkEmailFieldError = "")} />
/> {#if linkEmailFieldError}
{#if linkEmailFieldError} <Tooltip.Trigger class="field-error-trigger" aria-label={linkEmailFieldError}>
<Tooltip.Trigger class="field-error-trigger" aria-label={linkEmailFieldError}> <span class="field-error-icon" aria-hidden="true"><TriangleAlert size={18} /></span>
<span class="field-error-icon" aria-hidden="true"><TriangleAlert size={18} /></span> </Tooltip.Trigger>
</Tooltip.Trigger> {/if}
{/if} {#if linkEmailFieldError}
{#if linkEmailFieldError} <Tooltip.Portal>
<Tooltip.Portal> <Tooltip.Content class="field-error-tooltip">{linkEmailFieldError}</Tooltip.Content>
<Tooltip.Content class="field-error-tooltip">{linkEmailFieldError}</Tooltip.Content> </Tooltip.Portal>
</Tooltip.Portal> {/if}
{/if} </Tooltip.Root>
</Tooltip.Root> </div>
</div> <Button
<Button class="wide bottom-action payment-submit-button"
class="wide bottom-action payment-submit-button" onclick={requestLinkEmailCode}
onclick={requestLinkEmailCode} disabled={linkEmailBusy}
disabled={linkEmailBusy} >
> {t("wa_send_code_email")}
{t("wa_send_code_email")} </Button>
</Button>
{:else}
<div class="link-email-code-layout">
<div class="otp-wrap link-email-code-center">
<label class="otp-input-wrap">
<input
bind:value={linkEmailCode}
inputmode="numeric"
autocomplete="one-time-code"
maxlength="6"
aria-label={t("wa_email_code_aria")}
/>
<span class="otp-slots" aria-hidden="true">
{#each Array.from({ length: 6 }) as _, index}
<span class:filled={linkEmailCode[index]}>{linkEmailCode[index] || ""}</span>
{/each}
</span>
</label>
<Button
class="wide bottom-action payment-submit-button"
onclick={verifyLinkEmailCode}
disabled={linkEmailBusy}
>
{t("wa_confirm")}
</Button>
</div>
<button
class="link-button link-email-resend"
type="button"
onclick={requestLinkEmailCode}
disabled={linkEmailBusy || linkEmailResendCooldown > 0}
>
<RefreshCw size={15} />
{linkEmailResendCooldown > 0
? t("wa_auth_resend_wait", { seconds: linkEmailResendCooldown })
: t("wa_resend_code")}
</button>
</div>
{/if}
{#if linkEmailStatus} {#if linkEmailStatus}
<StatusMessage error={linkEmailIsError}>{linkEmailStatus}</StatusMessage> <StatusMessage error={linkEmailIsError}>{linkEmailStatus}</StatusMessage>
{/if} {/if}
+19 -56
View File
@@ -1,16 +1,10 @@
<script> <script>
import { import { LockKeyhole, Mail, Send, TriangleAlert } from "$components/ui/icons.js";
ArrowLeft,
LockKeyhole,
Mail,
RefreshCw,
Send,
TriangleAlert,
} from "$components/ui/icons.js";
import { Tooltip } from "$components/ui/primitives.js"; import { Tooltip } from "$components/ui/primitives.js";
import Button from "$components/ui/button.svelte"; import Button from "$components/ui/button.svelte";
import BrandMark from "$lib/webapp/BrandMark.svelte"; import BrandMark from "$lib/webapp/BrandMark.svelte";
import EmailCodeScreen from "./EmailCodeScreen.svelte";
import Input from "$components/ui/input.svelte"; import Input from "$components/ui/input.svelte";
import Spinner from "$components/ui/spinner.svelte"; import Spinner from "$components/ui/spinner.svelte";
import { StatusMessage } from "$components/patterns/webapp/index.js"; import { StatusMessage } from "$components/patterns/webapp/index.js";
@@ -55,52 +49,21 @@
$: authCardHeight = authPanelHeight ? `${authPanelHeight}px` : undefined; $: authCardHeight = authPanelHeight ? `${authPanelHeight}px` : undefined;
</script> </script>
<div class="phone-screen auth-screen"> {#if screen === "code"}
{#if screen === "code"} <EmailCodeScreen
<header class="screen-head center-title"> bind:code={emailCode}
<Button variant="icon" size="icon" onclick={onBackToLogin} aria-label={t("wa_back")}> email={pendingEmail}
<ArrowLeft size={19} /> busy={authBusy}
</Button> resendCooldown={authResendCooldown}
<div> status={authStatus}
<h1>{t("wa_email_verification_title")}</h1> isError={authIsError}
<p>{t("wa_email_sent_to", { email: pendingEmail })}</p> {t}
</div> onBack={onBackToLogin}
<span></span> onConfirm={verifyEmailCode}
</header> onResend={requestEmailCode}
<div class="otp-wrap"> />
<label class="otp-input-wrap"> {:else}
<input <div class="phone-screen auth-screen">
bind:value={emailCode}
inputmode="numeric"
autocomplete="one-time-code"
maxlength="6"
aria-label={t("wa_email_code_aria")}
/>
<span class="otp-slots" aria-hidden="true">
{#each Array.from({ length: 6 }) as _, index}
<span class:filled={emailCode[index]}>{emailCode[index] || ""}</span>
{/each}
</span>
</label>
<Button class="wide" onclick={verifyEmailCode} disabled={authBusy}>
{t("wa_confirm")}
</Button>
{#if authStatus}
<StatusMessage error={authIsError}>{authStatus}</StatusMessage>
{/if}
<button
class="link-button"
type="button"
onclick={requestEmailCode}
disabled={authBusy || authResendCooldown > 0}
>
<RefreshCw size={15} />
{authResendCooldown > 0
? t("wa_auth_resend_wait", { seconds: authResendCooldown })
: t("wa_resend_code")}
</button>
</div>
{:else}
<div class="auth-card-wrap"> <div class="auth-card-wrap">
<div class="login-brand login-brand-auth"> <div class="login-brand login-brand-auth">
<BrandMark {brand} size="xl" /> <BrandMark {brand} size="xl" />
@@ -305,5 +268,5 @@
</div> </div>
{/if} {/if}
</div> </div>
{/if} </div>
</div> {/if}
@@ -0,0 +1,63 @@
<script>
import { ArrowLeft, RefreshCw } from "$components/ui/icons.js";
import Button from "$components/ui/button.svelte";
import { StatusMessage } from "$components/patterns/webapp/index.js";
export let code = "";
export let email = "";
export let busy = false;
export let resendCooldown = 0;
export let status = "";
export let isError = false;
export let t = (key) => key;
export let onBack = () => {};
export let onConfirm = () => {};
export let onResend = () => {};
</script>
<div class="phone-screen auth-screen">
<header class="screen-head center-title">
<Button variant="icon" size="icon" onclick={onBack} aria-label={t("wa_back")}>
<ArrowLeft size={19} />
</Button>
<div>
<h1>{t("wa_email_verification_title")}</h1>
<p>{t("wa_email_sent_to", { email })}</p>
</div>
<span></span>
</header>
<div class="otp-wrap">
<label class="otp-input-wrap">
<input
bind:value={code}
inputmode="numeric"
autocomplete="one-time-code"
maxlength="6"
aria-label={t("wa_email_code_aria")}
/>
<span class="otp-slots" aria-hidden="true">
{#each Array.from({ length: 6 }) as _, index}
<span class:filled={code[index]}>{code[index] || ""}</span>
{/each}
</span>
</label>
<Button class="wide" onclick={onConfirm} disabled={busy}>
{t("wa_confirm")}
</Button>
{#if status}
<StatusMessage error={isError}>{status}</StatusMessage>
{/if}
<button
class="link-button"
type="button"
onclick={onResend}
disabled={busy || resendCooldown > 0}
>
<RefreshCw size={15} />
{resendCooldown > 0
? t("wa_auth_resend_wait", { seconds: resendCooldown })
: t("wa_resend_code")}
</button>
</div>
</div>