This commit is contained in:
parent
4463626232
commit
686e62d0e0
2 changed files with 105 additions and 74 deletions
|
@ -1,9 +1,9 @@
|
||||||
import type { mtcute, TelegramAccount } from 'mtcute-repl-worker/client'
|
import type { mtcute, TelegramAccount } from 'mtcute-repl-worker/client'
|
||||||
import type { Setter } from 'solid-js'
|
import type { Setter } from 'solid-js'
|
||||||
import { unknownToError } from '@fuman/utils'
|
import { unknownToError } from '@fuman/utils'
|
||||||
import { LucideChevronRight, MessageSquareMore } from 'lucide-solid'
|
import { LucideChevronRight, LucideLockKeyhole, MessageSquareMore } from 'lucide-solid'
|
||||||
import { workerInvoke, workerOn } from 'mtcute-repl-worker/client'
|
import { workerInvoke, workerOn } from 'mtcute-repl-worker/client'
|
||||||
import { createEffect, createSignal, For, Match, onCleanup, onMount, Show, Switch } from 'solid-js'
|
import { createSignal, For, Match, onCleanup, onMount, Show, Switch } from 'solid-js'
|
||||||
import { Button } from '../../../lib/components/ui/button.tsx'
|
import { Button } from '../../../lib/components/ui/button.tsx'
|
||||||
import { OTPField, OTPFieldGroup, OTPFieldInput, OTPFieldSlot } from '../../../lib/components/ui/otp-field.tsx'
|
import { OTPField, OTPFieldGroup, OTPFieldInput, OTPFieldSlot } from '../../../lib/components/ui/otp-field.tsx'
|
||||||
import { Spinner } from '../../../lib/components/ui/spinner.tsx'
|
import { Spinner } from '../../../lib/components/ui/spinner.tsx'
|
||||||
|
@ -81,7 +81,7 @@ function QrLoginStep(props: StepProps<'qr'>) {
|
||||||
/>
|
/>
|
||||||
) : <Spinner indeterminate class="size-10" />}
|
) : <Spinner indeterminate class="size-10" />}
|
||||||
</div>
|
</div>
|
||||||
<ol class="mt-4 list-inside list-decimal text-sm text-muted-foreground">
|
<ol class="text-muted-foreground mt-4 list-inside list-decimal text-sm">
|
||||||
<li>Open Telegram on your phone</li>
|
<li>Open Telegram on your phone</li>
|
||||||
<li>
|
<li>
|
||||||
Go to
|
Go to
|
||||||
|
@ -147,7 +147,7 @@ function PhoneNumberStep(props: StepProps<'phone'>) {
|
||||||
<h2 class="mt-4 text-xl font-bold">
|
<h2 class="mt-4 text-xl font-bold">
|
||||||
Log in with phone number
|
Log in with phone number
|
||||||
</h2>
|
</h2>
|
||||||
<div class="mt-2 text-center text-sm text-muted-foreground">
|
<div class="text-muted-foreground mt-2 text-center text-sm">
|
||||||
Please confirm your country code
|
Please confirm your country code
|
||||||
<br />
|
<br />
|
||||||
and enter your phone number
|
and enter your phone number
|
||||||
|
@ -177,7 +177,7 @@ function PhoneNumberStep(props: StepProps<'phone'>) {
|
||||||
<TextFieldErrorMessage>{error()}</TextFieldErrorMessage>
|
<TextFieldErrorMessage>{error()}</TextFieldErrorMessage>
|
||||||
</TextFieldRoot>
|
</TextFieldRoot>
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
<div class="text-center text-sm text-muted-foreground">
|
<div class="text-muted-foreground text-center text-sm">
|
||||||
or,
|
or,
|
||||||
{' '}
|
{' '}
|
||||||
<a
|
<a
|
||||||
|
@ -197,7 +197,6 @@ function OtpStep(props: StepProps<'otp'>) {
|
||||||
const [error, setError] = createSignal<string | undefined>()
|
const [error, setError] = createSignal<string | undefined>()
|
||||||
const [loading, setLoading] = createSignal(false)
|
const [loading, setLoading] = createSignal(false)
|
||||||
const [countdown, setCountdown] = createSignal(0)
|
const [countdown, setCountdown] = createSignal(0)
|
||||||
const [inputRef, setInputRef] = createSignal<HTMLInputElement | undefined>()
|
|
||||||
|
|
||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
@ -269,9 +268,9 @@ function OtpStep(props: StepProps<'otp'>) {
|
||||||
setCountdown(0)
|
setCountdown(0)
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
onCleanup(() => clearInterval(interval))
|
onCleanup(() => clearInterval(interval))
|
||||||
})
|
})
|
||||||
createEffect(() => inputRef()?.focus())
|
|
||||||
|
|
||||||
const description = () => {
|
const description = () => {
|
||||||
switch (props.ctx.code.type) {
|
switch (props.ctx.code.type) {
|
||||||
|
@ -297,19 +296,6 @@ function OtpStep(props: StepProps<'otp'>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [innerInputRef, setInnerInputRef] = createSignal<HTMLInputElement | undefined>()
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if (e.key >= '0' && e.key <= '9') {
|
|
||||||
innerInputRef()?.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown)
|
|
||||||
onCleanup(() => window.removeEventListener('keydown', handleKeyDown))
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col items-center justify-center">
|
<div class="flex flex-col items-center justify-center">
|
||||||
<MessageSquareMore class="size-16 pb-2" strokeWidth={1.5} />
|
<MessageSquareMore class="size-16 pb-2" strokeWidth={1.5} />
|
||||||
|
@ -323,7 +309,7 @@ function OtpStep(props: StepProps<'otp'>) {
|
||||||
Wrong number?
|
Wrong number?
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 text-center text-sm text-muted-foreground">
|
<div class="text-muted-foreground mt-4 text-center text-sm">
|
||||||
{description()}
|
{description()}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 flex flex-col items-center text-center">
|
<div class="mt-4 flex flex-col items-center text-center">
|
||||||
|
@ -343,7 +329,7 @@ function OtpStep(props: StepProps<'otp'>) {
|
||||||
handleSubmit()
|
handleSubmit()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
ref={(e) => { setInputRef(e); setInnerInputRef(e) }}
|
ref={props.ctx.setInputRef}
|
||||||
/>
|
/>
|
||||||
</TextFieldFrame>
|
</TextFieldFrame>
|
||||||
<TextFieldErrorMessage>{error()}</TextFieldErrorMessage>
|
<TextFieldErrorMessage>{error()}</TextFieldErrorMessage>
|
||||||
|
@ -358,7 +344,7 @@ function OtpStep(props: StepProps<'otp'>) {
|
||||||
>
|
>
|
||||||
<OTPFieldInput
|
<OTPFieldInput
|
||||||
disabled={loading()}
|
disabled={loading()}
|
||||||
ref={(e) => { setInputRef(e); setInnerInputRef(e) }}
|
ref={props.ctx.setInputRef}
|
||||||
onKeyPress={(e) => {
|
onKeyPress={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
handleSubmit()
|
handleSubmit()
|
||||||
|
@ -377,7 +363,7 @@ function OtpStep(props: StepProps<'otp'>) {
|
||||||
</OTPFieldGroup>
|
</OTPFieldGroup>
|
||||||
</OTPField>
|
</OTPField>
|
||||||
{error() && (
|
{error() && (
|
||||||
<div class="mt-1 text-sm text-error-foreground">{error()}</div>
|
<div class="text-error-foreground mt-1 text-sm">{error()}</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
@ -400,10 +386,24 @@ function OtpStep(props: StepProps<'otp'>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function PasswordStep(props: StepProps<'password'>) {
|
function PasswordStep(props: StepProps<'password'>) {
|
||||||
|
const [inputRef, setInputRef] = createSignal<HTMLInputElement | undefined>()
|
||||||
const [password, setPassword] = createSignal('')
|
const [password, setPassword] = createSignal('')
|
||||||
const [error, setError] = createSignal<string | undefined>()
|
const [error, setError] = createSignal<string | undefined>()
|
||||||
const [loading, setLoading] = createSignal(false)
|
const [loading, setLoading] = createSignal(false)
|
||||||
const [inputRef, setInputRef] = createSignal<HTMLInputElement | undefined>()
|
|
||||||
|
onMount(() => {
|
||||||
|
const pasteHandler = () => {
|
||||||
|
if (document.activeElement !== inputRef()) {
|
||||||
|
inputRef()?.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('paste', pasteHandler)
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
window.removeEventListener('paste', pasteHandler)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
onCleanup(() => abortController.abort())
|
onCleanup(() => abortController.abort())
|
||||||
|
@ -411,6 +411,7 @@ function PasswordStep(props: StepProps<'password'>) {
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!password()) {
|
if (!password()) {
|
||||||
setError('Password is required')
|
setError('Password is required')
|
||||||
|
inputRef()?.focus()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -427,20 +428,23 @@ function PasswordStep(props: StepProps<'password'>) {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
setError(unknownToError(e).message)
|
setError(unknownToError(e).message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inputRef()?.focus()
|
||||||
}
|
}
|
||||||
createEffect(() => inputRef()?.focus())
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col items-center justify-center">
|
<div class="flex flex-col items-center justify-center">
|
||||||
|
<LucideLockKeyhole class="size-16 pb-2" strokeWidth={1.5} />
|
||||||
<h2 class="text-xl font-bold">
|
<h2 class="text-xl font-bold">
|
||||||
2FA password
|
2FA password
|
||||||
</h2>
|
</h2>
|
||||||
<div class="mt-4 text-center text-sm text-muted-foreground">
|
<div class="text-muted-foreground mt-4 text-center text-sm">
|
||||||
Your account is protected with an additional password.
|
Your account is protected with an additional password.
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 flex flex-col">
|
<div class="mt-4">
|
||||||
<TextFieldRoot validationState={error() ? 'invalid' : 'valid'}>
|
<TextFieldRoot validationState={error() ? 'invalid' : 'valid'}>
|
||||||
<TextFieldLabel>Password</TextFieldLabel>
|
<TextFieldLabel>Password</TextFieldLabel>
|
||||||
|
<div class="flex flex-row">
|
||||||
<TextFieldFrame>
|
<TextFieldFrame>
|
||||||
<TextField
|
<TextField
|
||||||
type="password"
|
type="password"
|
||||||
|
@ -449,7 +453,7 @@ function PasswordStep(props: StepProps<'password'>) {
|
||||||
value={password()}
|
value={password()}
|
||||||
onInput={e => setPassword(e.currentTarget.value)}
|
onInput={e => setPassword(e.currentTarget.value)}
|
||||||
disabled={loading()}
|
disabled={loading()}
|
||||||
ref={setInputRef}
|
ref={(e) => { props.ctx.setInputRef(e); setInputRef(e) }}
|
||||||
onKeyPress={(e) => {
|
onKeyPress={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
handleSubmit()
|
handleSubmit()
|
||||||
|
@ -457,19 +461,18 @@ function PasswordStep(props: StepProps<'password'>) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</TextFieldFrame>
|
</TextFieldFrame>
|
||||||
<TextFieldErrorMessage>{error()}</TextFieldErrorMessage>
|
|
||||||
</TextFieldRoot>
|
|
||||||
|
|
||||||
<div class="mt-2 flex w-full justify-end">
|
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="icon"
|
||||||
|
class="ml-2 size-9 min-w-[36px]"
|
||||||
|
disabled={loading() || password() === ''}
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={loading()}
|
|
||||||
>
|
>
|
||||||
Next
|
<LucideChevronRight class="size-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<TextFieldErrorMessage>{error()}</TextFieldErrorMessage>
|
||||||
|
</TextFieldRoot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -480,9 +483,9 @@ function DoneStep(props: StepProps<'done'>) {
|
||||||
<div class="flex flex-col items-center justify-center">
|
<div class="flex flex-col items-center justify-center">
|
||||||
<AccountAvatar
|
<AccountAvatar
|
||||||
account={props.ctx.account}
|
account={props.ctx.account}
|
||||||
class="mb-4 size-24 animate-scale-up shadow-sm fill-mode-forwards"
|
class="animate-scale-up fill-mode-forwards mb-4 size-24 shadow-sm"
|
||||||
/>
|
/>
|
||||||
<div class="animate-fade-out-down text-center font-medium fill-mode-forwards">
|
<div class="animate-fade-out-down fill-mode-forwards text-center font-medium">
|
||||||
Welcome,
|
Welcome,
|
||||||
{' '}
|
{' '}
|
||||||
{props.ctx.account.name}
|
{props.ctx.account.name}
|
||||||
|
@ -508,6 +511,26 @@ export function LoginForm(props: {
|
||||||
|
|
||||||
const [inputRef, setInputRef] = createSignal<HTMLInputElement | undefined>()
|
const [inputRef, setInputRef] = createSignal<HTMLInputElement | undefined>()
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const pasteHandler = () => {
|
||||||
|
if (document.activeElement !== inputRef()) {
|
||||||
|
inputRef()?.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = () => {
|
||||||
|
inputRef()?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('paste', pasteHandler)
|
||||||
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
window.removeEventListener('paste', pasteHandler)
|
||||||
|
window.removeEventListener('keydown', handleKeyDown)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={cn('flex h-full flex-col items-center justify-center gap-4', props.class)}>
|
<div class={cn('flex h-full flex-col items-center justify-center gap-4', props.class)}>
|
||||||
<TransitionSlideLtr onAfterExit={() => inputRef()?.focus()} mode="outin">
|
<TransitionSlideLtr onAfterExit={() => inputRef()?.focus()} mode="outin">
|
||||||
|
|
|
@ -29,26 +29,11 @@ interface PhoneInputProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PhoneInput(props: PhoneInputProps) {
|
export function PhoneInput(props: PhoneInputProps) {
|
||||||
|
let inputRef: HTMLInputElement | undefined
|
||||||
const [countriesList, setCountriesList] = createSignal<mtcute.RawCountry[]>([])
|
const [countriesList, setCountriesList] = createSignal<mtcute.RawCountry[]>([])
|
||||||
const [chosenCode, setChosenCode] = createSignal<ChosenCode | undefined>()
|
const [chosenCode, setChosenCode] = createSignal<ChosenCode | undefined>()
|
||||||
const [inputValue, setInputValue] = createSignal('+')
|
const [inputValue, setInputValue] = createSignal('+')
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
const { countries, countryByIp } = await workerInvoke('telegram', 'loadCountries', { accountId: props.accountId })
|
|
||||||
setCountriesList(countries)
|
|
||||||
|
|
||||||
if (inputValue() === '+') {
|
|
||||||
// guess the country code
|
|
||||||
for (const country of countries) {
|
|
||||||
if (country.iso2 === countryByIp) {
|
|
||||||
setChosenCode(mapCountryCode(country, country.countryCodes[0]))
|
|
||||||
setInputValue(`+${country.countryCodes[0].countryCode} `)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleInput = (e: InputEvent) => {
|
const handleInput = (e: InputEvent) => {
|
||||||
const el = e.currentTarget as HTMLInputElement
|
const el = e.currentTarget as HTMLInputElement
|
||||||
const value = el.value
|
const value = el.value
|
||||||
|
@ -155,6 +140,26 @@ export function PhoneInput(props: PhoneInputProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const { countries, countryByIp } = await workerInvoke('telegram', 'loadCountries', { accountId: props.accountId })
|
||||||
|
setCountriesList(countries)
|
||||||
|
|
||||||
|
if (chosenCode() === undefined) {
|
||||||
|
handleInput({ currentTarget: inputRef } as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputValue() === '+') {
|
||||||
|
// guess the country code
|
||||||
|
for (const country of countries) {
|
||||||
|
if (country.iso2 === countryByIp) {
|
||||||
|
setChosenCode(mapCountryCode(country, country.countryCodes[0]))
|
||||||
|
setInputValue(`+${country.countryCodes[0].countryCode} `)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const handleKeyPress = (e: KeyboardEvent) => {
|
const handleKeyPress = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' && chosenCode() !== undefined) {
|
if (e.key === 'Enter' && chosenCode() !== undefined) {
|
||||||
props.onSubmit?.()
|
props.onSubmit?.()
|
||||||
|
@ -180,7 +185,10 @@ export function PhoneInput(props: PhoneInputProps) {
|
||||||
onKeyPress={handleKeyPress}
|
onKeyPress={handleKeyPress}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
ref={props.ref}
|
ref={(e: HTMLInputElement) => {
|
||||||
|
inputRef = e
|
||||||
|
props.ref?.(e)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</TextFieldFrame>
|
</TextFieldFrame>
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue