Files
everyone-can-use-english/enjoy/src/renderer/components/login/mixin-login-form.tsx
an-lee 6c2dec5220 Fix layout (#1191)
* fix layout

* fix layout
2024-11-20 10:02:38 +08:00

274 lines
7.8 KiB
TypeScript

import {
Button,
toast,
Input,
Label,
Sheet,
SheetTrigger,
SheetContent,
SheetHeader,
SheetTitle,
InputOTP,
InputOTPGroup,
InputOTPSlot,
Separator,
} from "@renderer/components/ui";
import { useContext, useEffect, useState } from "react";
import { AppSettingsProviderContext } from "@renderer/context";
import { t } from "i18next";
import { LoaderIcon } from "lucide-react";
import { REGEXP_ONLY_DIGITS } from "input-otp";
import { v4 as uuidv4 } from "uuid";
export const MixinLoginButton = () => {
const [open, setOpen] = useState(false);
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
variant="outline"
size="icon"
data-tooltip-id="global-tooltip"
data-tooltip-content="Mixin Messenger"
className="w-10 h-10 rounded-full"
>
<img
src="assets/mixin-logo.png"
className="w-full h-full p-1"
alt="mixin-logo"
/>
</Button>
</SheetTrigger>
<SheetContent
side="bottom"
className="h-content"
aria-describedby={undefined}
>
<SheetHeader>
<SheetTitle className="sr-only">Mixin Messenger Login</SheetTitle>
</SheetHeader>
<div className="w-full h-full flex">
<div className="m-auto">{open && <MixinLoginForm />}</div>
</div>
</SheetContent>
</Sheet>
);
};
export const MixinLoginForm = () => {
const [mixinId, setMixinId] = useState<string>("");
const [input, setInput] = useState<string>("");
const [code, setCode] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [codeSent, setCodeSent] = useState<boolean>(false);
const [countdown, setCountdown] = useState<number>(0);
const { login, webApi, EnjoyApp, apiUrl } = useContext(
AppSettingsProviderContext
);
const [state, setState] = useState<string>("");
const [scanning, setScanning] = useState<boolean>(false);
const validateMixinId = (id: string) => {
setInput(id);
if (id?.match(/^[1-9]\d{4,9}$/)) {
setMixinId(id);
} else {
setMixinId("");
}
};
const handleScanToLogin = () => {
const uuid = uuidv4();
setState(uuid);
EnjoyApp.shell.openExternal(
`${apiUrl}/sessions/new?provider=mixin&state=${uuid}`
);
setScanning(true);
};
const pollingOAuthState = () => {
if (!state) return;
webApi.oauthState(state).then((user) => {
if (user?.id && user?.accessToken) {
login(user);
}
});
};
useEffect(() => {
let interval: NodeJS.Timeout;
if (!scanning) return;
interval = setInterval(() => {
pollingOAuthState();
}, 1500);
return () => {
if (interval) clearInterval(interval);
};
}, [scanning]);
useEffect(() => {
let timeout: NodeJS.Timeout;
if (countdown > 0) {
timeout = setTimeout(() => {
setCountdown(countdown - 1);
}, 1000);
}
return () => {
if (timeout) clearTimeout(timeout);
};
}, [countdown]);
if (scanning) {
return (
<div className="w-80">
<div className="flex items-center justify-center mb-4">
<img src="assets/mixin-logo.png" className="w-20 h-20" alt="mixin" />
</div>
<div className="flex items-center justify-center mb-4">
<LoaderIcon className="w-5 h-5 animate-spin" />
</div>
<div className="text-center text-sm text-muted-foreground mb-6">
{t("scanMixinQRCodeDescription")}
</div>
<div className="flex justify-center items-center space-x-4">
<Button
variant="default"
onClick={() =>
EnjoyApp.shell.openExternal(
`${apiUrl}/sessions/new?provider=mixin&state=${state}`
)
}
>
{t("open")}
</Button>
<Button variant="secondary" onClick={() => setScanning(false)}>
{t("cancel")}
</Button>
</div>
</div>
);
}
return (
<div className="w-80">
<div className="flex items-center justify-center mb-4">
<img src="assets/mixin-logo.png" className="w-20 h-20" alt="mixin" />
</div>
<div className="grid gap-6">
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="mixinId">{t("mixinId")}</Label>
<input
id="mixinId"
value={input}
disabled={countdown > 0}
placeholder={t("inputMixinId")}
onInput={(event) => validateMixinId(event.currentTarget.value)}
onBlur={(event) => validateMixinId(event.currentTarget.value)}
className="border py-2 px-4 rounded dark:bg-background dark:text-foreground"
/>
</div>
{codeSent && (
<div className="grid gap-2">
<Label htmlFor="verificationCode">{t("verificationCode")}</Label>
<InputOTP
id="verificationCode"
maxLength={5}
value={code}
pattern={REGEXP_ONLY_DIGITS}
onChange={(value) => setCode(value)}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
</InputOTPGroup>
</InputOTP>
</div>
)}
<div className="flex items-center space-x-2">
<div
onClick={() =>
EnjoyApp.shell.openExternal("https://mixin.one/messenger")
}
className="text-xs text-muted-foreground cursor-pointer"
>
{t("createMixinAccount")}
</div>
<Separator orientation="vertical" />
<div
onClick={handleScanToLogin}
className="text-xs text-muted-foreground cursor-pointer"
>
{t("scanToLogin")}
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<Button
variant="secondary"
size="lg"
className="w-full px-2"
disabled={!mixinId || countdown > 0 || loading}
onClick={() => {
if (!mixinId) return;
if (loading) return;
if (countdown > 0) return;
setLoading(true);
webApi
.loginCode({ mixinId })
.then(() => {
toast.success(t("codeSent"));
setCodeSent(true);
setCountdown(120);
})
.catch((err) => {
toast.error(err.response?.data || err.message);
})
.finally(() => {
setLoading(false);
});
}}
>
{loading && <LoaderIcon className="w-5 h-5 mr-2 animate-spin" />}
{countdown > 0 && <span className="mr-2">{countdown}</span>}
<span>{codeSent ? t("resend") : t("sendCode")}</span>
</Button>
<Button
variant="default"
size="lg"
className="w-full px-2"
disabled={!code || code.length < 5 || !mixinId}
onClick={() => {
webApi
.auth({ provider: "mixin", code, mixinId })
.then((user) => {
if (user?.id && user?.accessToken) login(user);
})
.catch((err) => {
toast.error(err.response?.data || err.message);
});
}}
>
{t("login")}
</Button>
</div>
</div>
</div>
);
};