Feat: support email login (#490)

* may login with email

* update login layout

* update login style
This commit is contained in:
an-lee
2024-04-07 11:38:21 +08:00
committed by GitHub
parent 3d9a1ccba6
commit 52287357d5
4 changed files with 238 additions and 115 deletions

View File

@@ -70,9 +70,10 @@ export class Client {
}
auth(params: {
provider: "mixin" | "github" | "bandu";
provider: "mixin" | "github" | "bandu" | "email";
code: string;
phoneNumber?: string;
email?: string;
}): Promise<UserType> {
return this.api.post("/api/sessions", decamelizeKeys(params));
}
@@ -81,7 +82,7 @@ export class Client {
return this.api.get("/api/me");
}
loginCode(params: { phoneNumber: string }): Promise<void> {
loginCode(params: { phoneNumber?: string; email?: string }): Promise<void> {
return this.api.post("/api/sessions/login_code", decamelizeKeys(params));
}

View File

@@ -192,6 +192,9 @@
"sendCode": "Send code",
"codeSent": "Code sent, please check your SMS",
"verificationCode": "Verification code",
"email": "Email",
"phoneNumber": "Phone number",
"youCanAlsoLoginWith": "You can also login with",
"transcribe": "Transcribe",
"stillTranscribing": "AI is still working on the transcription. Please wait for a while.",
"unableToSetLibraryPath": "Unable to set library path to {{path}}",

View File

@@ -191,6 +191,9 @@
"sendCode": "发送验证码",
"codeSent": "验证码已发送",
"verificationCode": "验证码",
"email": "邮箱",
"phoneNumber": "手机号",
"youCanAlsoLoginWith": "您也可以使用以下方式登录",
"delete": "删除",
"transcribe": "语音转文本",
"stillTranscribing": "语音转文本仍在进行中,请耐心等候。",

View File

@@ -6,6 +6,11 @@ import {
Sheet,
SheetContent,
SheetTrigger,
Label,
Card,
CardContent,
CardHeader,
CardTitle,
} from "@renderer/components/ui";
import { useContext, useEffect, useRef, useState } from "react";
import { AppSettingsProviderContext } from "@renderer/context";
@@ -110,62 +115,82 @@ export const LoginForm = () => {
return (
<>
<div className="w-full max-w-sm px-6 flex flex-col space-y-4">
<Button
variant="outline"
size="lg"
className="w-full h-12 relative rounded-full"
onClick={() => handleLogin("github")}
>
<img
src="assets/github-mark.png"
className="w-8 h-8 absolute left-4"
alt="github-logo"
/>
<span className="text-lg">GitHub</span>
</Button>
<Button
variant="outline"
size="lg"
className="w-full h-12 relative rounded-full"
onClick={() => handleLogin("mixin")}
>
<img
src="assets/mixin-logo.png"
className="w-8 h-8 absolute left-4"
alt="mixin-logo"
/>
<span className="text-lg">Mixin Messenger</span>
</Button>
<Sheet>
<SheetTrigger asChild>
<Button
variant="outline"
size="lg"
className="w-full h-12 relative rounded-full"
>
<img
src="assets/bandu-logo.svg"
className="w-10 h-10 absolute left-4"
alt="bandu-logo"
/>
<span className="text-lg"></span>
</Button>
</SheetTrigger>
<SheetContent side="bottom" className="h-screen">
<div className="w-full h-full flex">
<div className="m-auto">
<PandoLoginForm />
</div>
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>{t('login')}</CardTitle>
</CardHeader>
<CardContent>
<EmailLoginForm />
<div className="">
<Separator className="my-4" />
<div className="flex items-center justify-center text-xs text-muted-foreground mb-4">
{t('youCanAlsoLoginWith')}
</div>
</SheetContent>
</Sheet>
</div>
<div className="flex items-center space-x-2 justify-center">
<Button
variant="outline"
size="icon"
data-tooltip-id="global-tooltip"
data-tooltip-content="GitHub"
className="w-10 h-10 rounded-full"
onClick={() => handleLogin("github")}
>
<img
src="assets/github-mark.png"
className="w-full h-full"
alt="github-logo"
/>
</Button>
<Button
variant="outline"
size="icon"
data-tooltip-id="global-tooltip"
data-tooltip-content="Mixin"
className="w-10 h-10 rounded-full p-1"
onClick={() => handleLogin("mixin")}
>
<img
src="assets/mixin-logo.png"
className="w-full h-full"
alt="mixin-logo"
/>
</Button>
<Sheet>
<SheetTrigger asChild>
<Button
variant="outline"
size="icon"
data-tooltip-id="global-tooltip"
data-tooltip-content="学升"
className="w-10 h-10 rounded-full"
>
<img
src="assets/bandu-logo.svg"
className="w-full h-full"
alt="bandu-logo"
/>
</Button>
</SheetTrigger>
<SheetContent side="bottom" className="h-screen">
<div className="w-full h-full flex">
<div className="m-auto">
<PandoLoginForm />
</div>
</div>
</SheetContent>
</Sheet>
</div>
</div>
</CardContent>
</Card>
<div
className={`absolute top-0 left-0 w-screen h-screen z-10 flex flex-col overflow-hidden ${
webviewUrl ? "" : "hidden"
}`}
className={`absolute top-0 left-0 w-screen h-screen z-10 flex flex-col overflow-hidden ${webviewUrl ? "" : "hidden"
}`}
>
<div className="flex items-center py-2 px-6">
<Button variant="ghost" onClick={() => setWebviewUrl(null)}>
@@ -181,6 +206,100 @@ export const LoginForm = () => {
);
};
const EmailLoginForm = () => {
const [email, setEmail] = useState<string>("");
const [code, setCode] = useState<string>("");
const [codeSent, setCodeSent] = useState<boolean>(false);
const [countdown, setCountdown] = useState<number>(0);
const { login, webApi } = useContext(AppSettingsProviderContext);
useEffect(() => {
if (countdown > 0) {
setTimeout(() => {
setCountdown(countdown - 1);
}, 1000);
}
}, [countdown]);
return (
<div className="w-full">
<div className="w-full grid gap-4 mb-6">
<div className="grid gap-2">
<Label htmlFor="email">{t('email')}</Label>
<Input
id="email"
className="h-10"
type="email"
placeholder="m@example.com"
required
value={email}
disabled={countdown > 0}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="code">{t('verificationCode')}</Label>
<Input
id="code"
className="h-10"
type="text"
required
minLength={5}
maxLength={5}
placeholder={t("verificationCode")}
value={code}
onChange={(e) => setCode(e.target.value)}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<Button
variant="secondary"
size="lg"
className="w-full"
disabled={!email || countdown > 0}
onClick={() => {
webApi
.loginCode({ email })
.then(() => {
toast.success(t("codeSent"));
setCodeSent(true);
setCountdown(120);
})
.catch((err) => {
toast.error(err.message);
});
}}
>
{countdown > 0 && <span className="mr-2">{countdown}</span>}
<span>{codeSent ? t("resend") : t("sendCode")}</span>
</Button>
<Button
variant="default"
size="lg"
className="w-full"
disabled={!code || code.length < 5 || !email}
onClick={() => {
webApi
.auth({ provider: "email", code, email })
.then((user) => {
if (user?.id && user?.accessToken) login(user);
})
.catch((err) => {
toast.error(err.message);
});
}}
>
{t("login")}
</Button>
</div>
</div>
);
}
const PandoLoginForm = () => {
const ref = useRef<HTMLInputElement>(null);
const [iti, setIti] = useState<any>(null);
@@ -234,45 +353,23 @@ const PandoLoginForm = () => {
<img src="assets/bandu-logo.svg" className="w-20 h-20" alt="bandu" />
</div>
<div className="mb-12">
<div className="mb-2">
<input
onInput={validatePhone}
onBlur={validatePhone}
className="border text-lg py-2 px-4 rounded"
ref={ref}
/>
</div>
{phoneNumber && (
<div className="mb-8">
<Button
variant="default"
size="lg"
className="w-full"
disabled={countdown > 0}
onClick={() => {
webApi
.loginCode({ phoneNumber })
.then(() => {
toast.success(t("codeSent"));
setCodeSent(true);
setCountdown(60);
})
.catch((err) => {
toast.error(err.message);
});
}}
>
{countdown > 0 && <span className="mr-2">{countdown}</span>}
<span>{codeSent ? t("resend") : t("sendCode")}</span>
</Button>
<div className="grid gap-6">
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="phone">{t('phoneNumber')}</Label>
<input
id="phone"
value={phoneNumber}
onInput={validatePhone}
onBlur={validatePhone}
className="border text-lg py-2 px-4 rounded"
ref={ref}
/>
</div>
)}
{codeSent && (
<div className="mb-2 w-full">
<div className="grid gap-2">
<Label htmlFor="verrificationCode">{t('verificationCode')}</Label>
<Input
id="verrificationCode"
className="border py-2 h-10 px-4 rounded"
type="text"
minLength={5}
@@ -282,30 +379,49 @@ const PandoLoginForm = () => {
onChange={(e) => setCode(e.target.value)}
/>
</div>
)}
</div>
{code && (
<div>
<Button
variant="default"
size="lg"
className="w-full"
disabled={!code || code.length < 5 || !phoneNumber}
onClick={() => {
webApi
.auth({ provider: "bandu", code, phoneNumber })
.then((user) => {
if (user?.id && user?.accessToken) login(user);
})
.catch((err) => {
toast.error(err.message);
});
}}
>
{t("login")}
</Button>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<Button
variant="secondary"
size="lg"
className="w-full"
disabled={!phoneNumber || countdown > 0}
onClick={() => {
webApi
.loginCode({ phoneNumber })
.then(() => {
toast.success(t("codeSent"));
setCodeSent(true);
setCountdown(60);
})
.catch((err) => {
toast.error(err.message);
});
}}
>
{countdown > 0 && <span className="mr-2">{countdown}</span>}
<span>{codeSent ? t("resend") : t("sendCode")}</span>
</Button>
<Button
variant="default"
size="lg"
className="w-full"
disabled={!code || code.length < 5 || !phoneNumber}
onClick={() => {
webApi
.auth({ provider: "bandu", code, phoneNumber })
.then((user) => {
if (user?.id && user?.accessToken) login(user);
})
.catch((err) => {
toast.error(err.message);
});
}}
>
{t("login")}
</Button>
</div>
</div>
</div>
);