Skip to content

Commit 8bf9dd0

Browse files
committed
feat(auth): enhance login modal with email login
Signed-off-by: Innei <tukon479@gmail.com>
1 parent d9a2638 commit 8bf9dd0

File tree

4 files changed

+230
-1
lines changed

4 files changed

+230
-1
lines changed

apps/renderer/src/modules/app-layout/feed-column/index.mobile.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const MobileRootLayout = () => {
2727
contentClassName="overflow-visible pb-safe"
2828
title="Login"
2929
hideHeader
30+
dismissableClassName="hidden"
3031
content={<LoginModalContent canClose={false} runtime={"browser"} />}
3132
/>
3233
</RootPortal>
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { Button } from "@follow/components/ui/button/index.js"
2+
import {
3+
Form,
4+
FormControl,
5+
FormField,
6+
FormItem,
7+
FormLabel,
8+
FormMessage,
9+
} from "@follow/components/ui/form/index.js"
10+
import { Input } from "@follow/components/ui/input/Input.js"
11+
import { loginHandler, signUp } from "@follow/shared/auth"
12+
import { zodResolver } from "@hookform/resolvers/zod"
13+
import { useForm } from "react-hook-form"
14+
import { useTranslation } from "react-i18next"
15+
import { Link } from "react-router"
16+
import { toast } from "sonner"
17+
import { z } from "zod"
18+
19+
import { useCurrentModal, useModalStack } from "~/components/ui/modal/stacked/hooks"
20+
21+
async function onSubmit(values: z.infer<typeof formSchema>) {
22+
const res = await loginHandler("credential", "browser", values)
23+
if (res?.error) {
24+
toast.error(res.error.message)
25+
return
26+
}
27+
window.location.reload()
28+
}
29+
const formSchema = z.object({
30+
email: z.string().email(),
31+
password: z.string().max(128),
32+
})
33+
export function LoginWithPassword() {
34+
const { t } = useTranslation("app")
35+
const form = useForm<z.infer<typeof formSchema>>({
36+
resolver: zodResolver(formSchema),
37+
defaultValues: {
38+
email: "",
39+
password: "",
40+
},
41+
})
42+
const { isValid } = form.formState
43+
44+
const { present } = useModalStack()
45+
const { dismiss } = useCurrentModal()
46+
47+
return (
48+
<Form {...form}>
49+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
50+
<FormField
51+
control={form.control}
52+
name="email"
53+
render={({ field }) => (
54+
<FormItem>
55+
<FormLabel>{t("login.email")}</FormLabel>
56+
<FormControl>
57+
<Input type="email" {...field} />
58+
</FormControl>
59+
<FormMessage />
60+
</FormItem>
61+
)}
62+
/>
63+
<FormField
64+
control={form.control}
65+
name="password"
66+
render={({ field }) => (
67+
<FormItem className="mt-4">
68+
<FormLabel>{t("login.password")}</FormLabel>
69+
<FormControl>
70+
<Input type="password" {...field} />
71+
</FormControl>
72+
<FormMessage />
73+
</FormItem>
74+
)}
75+
/>
76+
<Link to="/forget-password" className="block py-1 text-xs text-accent hover:underline">
77+
{t("login.forget_password.note")}
78+
</Link>
79+
<Button
80+
type="submit"
81+
className="w-full"
82+
buttonClassName="text-base !mt-3"
83+
disabled={!isValid}
84+
>
85+
{t("login.continueWith", { provider: t("words.email") })}
86+
</Button>
87+
<Button
88+
buttonClassName="!mt-3 text-base"
89+
className="w-full"
90+
variant="outline"
91+
onClick={() => {
92+
dismiss()
93+
present({
94+
content: RegisterForm,
95+
title: t("register.label", { app_name: APP_NAME }),
96+
})
97+
}}
98+
>
99+
{t("login.signUp")}
100+
</Button>
101+
</form>
102+
</Form>
103+
)
104+
}
105+
106+
const registerFormSchema = z
107+
.object({
108+
email: z.string().email(),
109+
password: z.string().min(8).max(128),
110+
confirmPassword: z.string(),
111+
})
112+
.refine((data) => data.password === data.confirmPassword, {
113+
message: "Passwords don't match",
114+
path: ["confirmPassword"],
115+
})
116+
117+
function RegisterForm() {
118+
const { t } = useTranslation("app")
119+
120+
const form = useForm<z.infer<typeof registerFormSchema>>({
121+
resolver: zodResolver(registerFormSchema),
122+
defaultValues: {
123+
email: "",
124+
password: "",
125+
confirmPassword: "",
126+
},
127+
})
128+
129+
const { isValid } = form.formState
130+
131+
function onSubmit(values: z.infer<typeof registerFormSchema>) {
132+
return signUp.email({
133+
email: values.email,
134+
password: values.password,
135+
name: values.email.split("@")[0],
136+
callbackURL: "/",
137+
fetchOptions: {
138+
onSuccess() {
139+
window.location.reload()
140+
},
141+
onError(context) {
142+
toast.error(context.error.message)
143+
},
144+
},
145+
})
146+
}
147+
148+
return (
149+
<div className="relative">
150+
<Form {...form}>
151+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
152+
<FormField
153+
control={form.control}
154+
name="email"
155+
render={({ field }) => (
156+
<FormItem>
157+
<FormLabel>{t("register.email")}</FormLabel>
158+
<FormControl>
159+
<Input type="email" {...field} />
160+
</FormControl>
161+
<FormMessage />
162+
</FormItem>
163+
)}
164+
/>
165+
<FormField
166+
control={form.control}
167+
name="password"
168+
render={({ field }) => (
169+
<FormItem>
170+
<FormLabel>{t("register.password")}</FormLabel>
171+
<FormControl>
172+
<Input type="password" {...field} />
173+
</FormControl>
174+
<FormMessage />
175+
</FormItem>
176+
)}
177+
/>
178+
<FormField
179+
control={form.control}
180+
name="confirmPassword"
181+
render={({ field }) => (
182+
<FormItem>
183+
<FormLabel>{t("register.confirm_password")}</FormLabel>
184+
<FormControl>
185+
<Input type="password" {...field} />
186+
</FormControl>
187+
<FormMessage />
188+
</FormItem>
189+
)}
190+
/>
191+
<Button disabled={!isValid} type="submit" className="w-full">
192+
{t("register.submit")}
193+
</Button>
194+
</form>
195+
</Form>
196+
</div>
197+
)
198+
}

apps/renderer/src/modules/auth/LoginModalContent.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import type { FC } from "react"
1212
import { useEffect, useMemo, useRef, useState } from "react"
1313
import { useTranslation } from "react-i18next"
1414

15-
import { useCurrentModal } from "~/components/ui/modal/stacked/hooks"
15+
import { useCurrentModal, useModalStack } from "~/components/ui/modal/stacked/hooks"
1616
import type { AuthProvider } from "~/queries/users"
1717
import { useAuthProviders } from "~/queries/users"
1818

19+
import { LoginWithPassword } from "./Form"
20+
1921
interface LoginModalContentProps {
2022
runtime?: LoginRuntime
2123
canClose?: boolean
@@ -35,10 +37,12 @@ const defaultProviders = {
3537
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none"><path d="M24 0v24H0V0h24ZM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018Zm.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01l-.184-.092Z"/><path fill="currentColor" d="M7.024 2.31a9.08 9.08 0 0 1 2.125 1.046A11.432 11.432 0 0 1 12 3c.993 0 1.951.124 2.849.355a9.08 9.08 0 0 1 2.124-1.045c.697-.237 1.69-.621 2.28.032c.4.444.5 1.188.571 1.756c.08.634.099 1.46-.111 2.28C20.516 7.415 21 8.652 21 10c0 2.042-1.106 3.815-2.743 5.043a9.456 9.456 0 0 1-2.59 1.356c.214.49.333 1.032.333 1.601v3a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-.991c-.955.117-1.756.013-2.437-.276c-.712-.302-1.208-.77-1.581-1.218c-.354-.424-.74-1.38-1.298-1.566a1 1 0 0 1 .632-1.898c.666.222 1.1.702 1.397 1.088c.48.62.87 1.43 1.63 1.753c.313.133.772.22 1.49.122L8 17.98a3.986 3.986 0 0 1 .333-1.581a9.455 9.455 0 0 1-2.59-1.356C4.106 13.815 3 12.043 3 10c0-1.346.483-2.582 1.284-3.618c-.21-.82-.192-1.648-.112-2.283l.005-.038c.073-.582.158-1.267.566-1.719c.59-.653 1.584-.268 2.28-.031Z"/></g></svg>',
3638
},
3739
}
40+
3841
const overrideProviderIconMap = {
3942
apple: <i className="i-mgc-apple-cute-fi text-black dark:text-white" />,
4043
credential: <i className="i-mingcute-mail-line text-black dark:text-white" />,
4144
}
45+
4246
export const LoginModalContent = (props: LoginModalContentProps) => {
4347
const modal = useCurrentModal()
4448

@@ -194,7 +198,9 @@ export const AuthProvidersRender: FC<{
194198
providers: AuthProvider[]
195199
runtime?: LoginRuntime
196200
}> = ({ providers, runtime }) => {
201+
const { t } = useTranslation()
197202
const [authProcessingLockSet, setAuthProcessingLockSet] = useState(() => new Set<string>())
203+
const { present } = useModalStack()
198204
return (
199205
<AutoResizeHeight spring>
200206
{providers.length > 0 && (
@@ -205,6 +211,14 @@ export const AuthProvidersRender: FC<{
205211
disabled={authProcessingLockSet.has(provider.id)}
206212
onClick={() => {
207213
if (authProcessingLockSet.has(provider.id)) return
214+
215+
if (provider.id === "credential") {
216+
present({
217+
content: LoginWithPassword,
218+
title: t("login.with_email.title"),
219+
})
220+
return
221+
}
208222
loginHandler(provider.id, runtime)
209223

210224
setAuthProcessingLockSet((prev) => {

locales/app/en.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,14 @@
228228
"feed_view_type.pictures": "Pictures",
229229
"feed_view_type.social_media": "Social Media",
230230
"feed_view_type.videos": "Videos",
231+
"login.confirm_password.label": "Confirm Password",
232+
"login.continueWith": "Continue with {{provider}}",
233+
"login.email": "Email",
234+
"login.forget_password.note": "Forgot your password?",
235+
"login.password": "Password",
236+
"login.signUp": "Sign up with email",
237+
"login.submit": "Submit",
238+
"login.with_email.title": "Login with Email",
231239
"mark_all_read_button.auto_confirm_info": "Will be confirmed automatically after {{countdown}}s.",
232240
"mark_all_read_button.confirm": "Confirm",
233241
"mark_all_read_button.confirm_mark_all": "Mark <which /> as read?",
@@ -287,6 +295,13 @@
287295
"player.volume": "Volume",
288296
"quick_add.placeholder": "Quick follow a feed, typing feed url here...",
289297
"quick_add.title": "Quick Follow",
298+
"register.confirm_password": "Confirm Password",
299+
"register.email": "Email",
300+
"register.label": "Create a {{app_name}} account",
301+
"register.login": "Login",
302+
"register.note": "Already have an account? <LoginLink />",
303+
"register.password": "Password",
304+
"register.submit": "Create account",
290305
"resize.tooltip.double_click_to_collapse": "<b>Double click</b> to collapse",
291306
"resize.tooltip.drag_to_resize": "<b>Drag</b> to resize",
292307
"search.empty.no_results": "No results found.",
@@ -393,6 +408,7 @@
393408
"words.browser": "Browser",
394409
"words.confirm": "Confirm",
395410
"words.discover": "Discover",
411+
"words.email": "Email",
396412
"words.feeds": "Feeds",
397413
"words.import": "Import",
398414
"words.inbox": "Inbox",

0 commit comments

Comments
 (0)