Skip to content

Commit

Permalink
feat(auth): enhance login modal with email login
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <tukon479@gmail.com>
  • Loading branch information
Innei committed Dec 23, 2024
1 parent d9a2638 commit 8bf9dd0
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const MobileRootLayout = () => {
contentClassName="overflow-visible pb-safe"
title="Login"
hideHeader
dismissableClassName="hidden"
content={<LoginModalContent canClose={false} runtime={"browser"} />}
/>
</RootPortal>
Expand Down
198 changes: 198 additions & 0 deletions apps/renderer/src/modules/auth/Form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { Button } from "@follow/components/ui/button/index.js"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@follow/components/ui/form/index.js"
import { Input } from "@follow/components/ui/input/Input.js"
import { loginHandler, signUp } from "@follow/shared/auth"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { Link } from "react-router"
import { toast } from "sonner"
import { z } from "zod"

import { useCurrentModal, useModalStack } from "~/components/ui/modal/stacked/hooks"

async function onSubmit(values: z.infer<typeof formSchema>) {
const res = await loginHandler("credential", "browser", values)
if (res?.error) {
toast.error(res.error.message)
return
}
window.location.reload()
}
const formSchema = z.object({
email: z.string().email(),
password: z.string().max(128),
})
export function LoginWithPassword() {
const { t } = useTranslation("app")
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: "",
password: "",
},
})
const { isValid } = form.formState

const { present } = useModalStack()
const { dismiss } = useCurrentModal()

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t("login.email")}</FormLabel>
<FormControl>
<Input type="email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem className="mt-4">
<FormLabel>{t("login.password")}</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Link to="/forget-password" className="block py-1 text-xs text-accent hover:underline">
{t("login.forget_password.note")}
</Link>
<Button
type="submit"
className="w-full"
buttonClassName="text-base !mt-3"
disabled={!isValid}
>
{t("login.continueWith", { provider: t("words.email") })}
</Button>
<Button
buttonClassName="!mt-3 text-base"
className="w-full"
variant="outline"
onClick={() => {
dismiss()
present({
content: RegisterForm,
title: t("register.label", { app_name: APP_NAME }),
})
}}
>
{t("login.signUp")}
</Button>
</form>
</Form>
)
}

const registerFormSchema = z
.object({
email: z.string().email(),
password: z.string().min(8).max(128),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
})

function RegisterForm() {
const { t } = useTranslation("app")

const form = useForm<z.infer<typeof registerFormSchema>>({
resolver: zodResolver(registerFormSchema),
defaultValues: {
email: "",
password: "",
confirmPassword: "",
},
})

const { isValid } = form.formState

function onSubmit(values: z.infer<typeof registerFormSchema>) {
return signUp.email({
email: values.email,
password: values.password,
name: values.email.split("@")[0],
callbackURL: "/",
fetchOptions: {
onSuccess() {
window.location.reload()
},
onError(context) {
toast.error(context.error.message)
},
},
})
}

return (
<div className="relative">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t("register.email")}</FormLabel>
<FormControl>
<Input type="email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t("register.password")}</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>{t("register.confirm_password")}</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button disabled={!isValid} type="submit" className="w-full">
{t("register.submit")}
</Button>
</form>
</Form>
</div>
)
}
16 changes: 15 additions & 1 deletion apps/renderer/src/modules/auth/LoginModalContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import type { FC } from "react"
import { useEffect, useMemo, useRef, useState } from "react"
import { useTranslation } from "react-i18next"

import { useCurrentModal } from "~/components/ui/modal/stacked/hooks"
import { useCurrentModal, useModalStack } from "~/components/ui/modal/stacked/hooks"
import type { AuthProvider } from "~/queries/users"
import { useAuthProviders } from "~/queries/users"

import { LoginWithPassword } from "./Form"

interface LoginModalContentProps {
runtime?: LoginRuntime
canClose?: boolean
Expand All @@ -35,10 +37,12 @@ const defaultProviders = {
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>',
},
}

const overrideProviderIconMap = {
apple: <i className="i-mgc-apple-cute-fi text-black dark:text-white" />,
credential: <i className="i-mingcute-mail-line text-black dark:text-white" />,
}

export const LoginModalContent = (props: LoginModalContentProps) => {
const modal = useCurrentModal()

Expand Down Expand Up @@ -194,7 +198,9 @@ export const AuthProvidersRender: FC<{
providers: AuthProvider[]
runtime?: LoginRuntime
}> = ({ providers, runtime }) => {
const { t } = useTranslation()
const [authProcessingLockSet, setAuthProcessingLockSet] = useState(() => new Set<string>())
const { present } = useModalStack()
return (
<AutoResizeHeight spring>
{providers.length > 0 && (
Expand All @@ -205,6 +211,14 @@ export const AuthProvidersRender: FC<{
disabled={authProcessingLockSet.has(provider.id)}
onClick={() => {
if (authProcessingLockSet.has(provider.id)) return

if (provider.id === "credential") {
present({
content: LoginWithPassword,
title: t("login.with_email.title"),
})
return
}
loginHandler(provider.id, runtime)

setAuthProcessingLockSet((prev) => {
Expand Down
16 changes: 16 additions & 0 deletions locales/app/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,14 @@
"feed_view_type.pictures": "Pictures",
"feed_view_type.social_media": "Social Media",
"feed_view_type.videos": "Videos",
"login.confirm_password.label": "Confirm Password",
"login.continueWith": "Continue with {{provider}}",
"login.email": "Email",
"login.forget_password.note": "Forgot your password?",
"login.password": "Password",
"login.signUp": "Sign up with email",
"login.submit": "Submit",
"login.with_email.title": "Login with Email",
"mark_all_read_button.auto_confirm_info": "Will be confirmed automatically after {{countdown}}s.",
"mark_all_read_button.confirm": "Confirm",
"mark_all_read_button.confirm_mark_all": "Mark <which /> as read?",
Expand Down Expand Up @@ -287,6 +295,13 @@
"player.volume": "Volume",
"quick_add.placeholder": "Quick follow a feed, typing feed url here...",
"quick_add.title": "Quick Follow",
"register.confirm_password": "Confirm Password",
"register.email": "Email",
"register.label": "Create a {{app_name}} account",
"register.login": "Login",
"register.note": "Already have an account? <LoginLink />",
"register.password": "Password",
"register.submit": "Create account",
"resize.tooltip.double_click_to_collapse": "<b>Double click</b> to collapse",
"resize.tooltip.drag_to_resize": "<b>Drag</b> to resize",
"search.empty.no_results": "No results found.",
Expand Down Expand Up @@ -393,6 +408,7 @@
"words.browser": "Browser",
"words.confirm": "Confirm",
"words.discover": "Discover",
"words.email": "Email",
"words.feeds": "Feeds",
"words.import": "Import",
"words.inbox": "Inbox",
Expand Down

0 comments on commit 8bf9dd0

Please sign in to comment.