Feat: support email login (#490)
* may login with email * update login layout * update login style
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -191,6 +191,9 @@
|
||||
"sendCode": "发送验证码",
|
||||
"codeSent": "验证码已发送",
|
||||
"verificationCode": "验证码",
|
||||
"email": "邮箱",
|
||||
"phoneNumber": "手机号",
|
||||
"youCanAlsoLoginWith": "您也可以使用以下方式登录",
|
||||
"delete": "删除",
|
||||
"transcribe": "语音转文本",
|
||||
"stillTranscribing": "语音转文本仍在进行中,请耐心等候。",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user