From 2422af196a9f4a83a4a337373cc3d971bd136c0b Mon Sep 17 00:00:00 2001 From: trevor-anderson Date: Sun, 24 Mar 2024 11:28:15 -0400 Subject: [PATCH] feat: add GoogleOAuth btns to Login+Register --- src/pages/LoginPage/LoginForm.tsx | 34 +++++++--- src/pages/LoginPage/LoginPage.tsx | 15 +++-- src/pages/RegisterPage/RegisterForm.tsx | 89 +++++++++++++++++++------ src/pages/RegisterPage/RegisterPage.tsx | 17 ++++- 4 files changed, 118 insertions(+), 37 deletions(-) diff --git a/src/pages/LoginPage/LoginForm.tsx b/src/pages/LoginPage/LoginForm.tsx index 01ba10a8..7bc8b182 100644 --- a/src/pages/LoginPage/LoginForm.tsx +++ b/src/pages/LoginPage/LoginForm.tsx @@ -2,7 +2,10 @@ import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; import { object as yupObject, type InferType } from "yup"; import { useFetchStateContext } from "@/app/FetchStateContext"; -import { Form, FormSubmitButton, TextInput, PasswordInput } from "@/components/Form"; +import { GoogleAuthFormButton } from "@/app/GoogleOAuthContext/GoogleAuthFormButton"; +import { DividerWithText } from "@/components/DataDisplay"; +import { Form, FormSubmitButton } from "@/components/Form"; +import { EmailInput, PasswordInput } from "@/components/Form/Inputs"; import { yupCommonSchema, getInitialValuesFromSchema } from "@/components/Form/helpers"; import { ErrorDialog } from "@/components/Indicators"; import { APP_PATHS } from "@/routes/appPaths"; @@ -12,8 +15,16 @@ export const LoginForm = () => { const nav = useNavigate(); const { fetchWithState, error, clearError } = useFetchStateContext(); - const onSubmit = async (credentials: LoginFormValues) => { - const apiResponse = await fetchWithState(async () => await authService.login(credentials)); + const onSubmit = async ({ password, googleIDToken, ...values }: LoginFormValues) => { + const apiResponse = await fetchWithState( + async () => + await authService.login({ + ...(password + ? { password } // Send one of `password` or `googleIDToken` + : { googleIDToken: googleIDToken! }), + ...values, + }) + ); if (apiResponse?.token) { toast.success("Welcome back!", { toastId: "login-success" }); @@ -26,12 +37,16 @@ export const LoginForm = () => { initialValues={loginFormInitialValues} validationSchema={loginFormSchema} onSubmit={onSubmit} - sx={{ all: "inherit" }} > - + + {error && } + + OR + + } /> ); }; @@ -39,14 +54,15 @@ export const LoginForm = () => { /** * Yup Schema for above `Form`s "validationSchema" prop. */ -const loginFormSchema = yupObject({ +export const loginFormSchema = yupObject({ email: yupCommonSchema.email.required("Required"), - password: yupCommonSchema.password.required("Required"), + password: yupCommonSchema.password, + googleIDToken: yupCommonSchema.googleIDToken, }); /** * Object for above `Form`s "initialValues" prop. */ -const loginFormInitialValues = getInitialValuesFromSchema(loginFormSchema); +export const loginFormInitialValues = getInitialValuesFromSchema(loginFormSchema); -type LoginFormValues = InferType; +export type LoginFormValues = InferType; diff --git a/src/pages/LoginPage/LoginPage.tsx b/src/pages/LoginPage/LoginPage.tsx index e8de5972..e012aa05 100644 --- a/src/pages/LoginPage/LoginPage.tsx +++ b/src/pages/LoginPage/LoginPage.tsx @@ -1,7 +1,7 @@ import Box from "@mui/material/Box"; import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import { Link } from "@/components/Navigation"; -import { AuthPageLayout } from "@/layouts/AuthPageLayout"; +import { AuthPageLayout, authPageLayoutClassNames } from "@/layouts/AuthPageLayout"; import { APP_PATHS } from "@/routes/appPaths"; import { LoginForm } from "./LoginForm"; @@ -9,13 +9,20 @@ import { LoginForm } from "./LoginForm"; * **LoginPage** - renders when path is "/login" */ export const LoginPage = () => ( - + { const nav = useNavigate(); const { fetchWithState, error, clearError } = useFetchStateContext(); - const handleSubmit = async (values: RegisterFormValues) => { + const handleSubmit = async ({ + handle, + password, + googleIDToken, + ...values + }: RegisterFormValues) => { const apiResponse = await fetchWithState( async () => await authService.registerNewUser({ + handle: `@${handle}`, // <-- "@" prefix added to "handle" + ...(password + ? { password } // Send one of `password` or `googleIDToken` + : { googleIDToken: googleIDToken! }), ...values, - handle: `@${values.handle}`, // <-- "@" prefix added to "handle" }) ); if (apiResponse?.token) { - toast.success(`Welcome to Fixit - please select a subscription to get started!`, { + toast.success(`Welcome to Fixit — please select a subscription to get started!`, { toastId: "select-a-sub", }); nav(APP_PATHS.PRODUCTS); } }; - // TODO Show user password requirements - return ( initialValues={registerFormInitialValues} validationSchema={registerFormSchema} onSubmit={handleSubmit} - sx={{ all: "inherit" }} > - @, - }} - /> + - + + {error && } + + OR + + + text="signup_with" + beforeSetFormikState={({ values, errors, touched, ...formikState }) => ({ + ...formikState, + values: { + ...values, + /* Since the OAuth flow is intended to be expeditious, it's desirable to + ensure that the User can proceed to the next step without interruption. + To that end, optional fields like `phone` here are cleared/reset if their + value is invalid, thereby ensuring form submission isn't blocked. */ + phone: values.phone && !errors.phone ? values.phone : null, + }, + errors: { + ...errors, + phone: undefined, // <-- phone is set to null if invalid, so rm any existing errors + handle: !values.handle + ? "Please choose a user handle (this is how other users will identify you)" + : errors.handle, // <-- will be undefined unless the User's value is invalid + }, + touched: { + ...touched, + phone: true, + handle: true, + }, + })} + requiredFieldInputs={ + <> + + + + } + /> ); }; @@ -60,14 +98,21 @@ export const RegisterForm = () => { const registerFormSchema = yupObject({ handle: yupCommonSchema.string .lowercase() - .matches( - /^[a-z0-9_]{3,50}$/, - "Must be between 3-50 characters, and only contain letters, numbers, and underscores" - ) - .required("Please choose a handle (this is how other users will identify you)"), - phone: yupCommonSchema.phone.required("Please provide a phone number"), + .test({ + name: "is-right-length", + message: "User handles must be between 3-50 characters", + test: (value) => value?.length >= 3 && value?.length <= 50, + }) + .test({ + name: "no-banned-chars", + message: "User handles must only contain letters, numbers, and underscores", + test: (value) => /^[a-z0-9_]{3,50}$/i.test(value), + }) + .required("Please choose a user handle (this is how other users will identify you)"), email: yupCommonSchema.email.required("Please provide an email"), - password: yupCommonSchema.password.required("Please enter a password"), + phone: yupCommonSchema.phone.nullable().default(null), + password: yupCommonSchema.password, + googleIDToken: yupCommonSchema.googleIDToken, }); /** diff --git a/src/pages/RegisterPage/RegisterPage.tsx b/src/pages/RegisterPage/RegisterPage.tsx index cd13b66f..bbf4f170 100644 --- a/src/pages/RegisterPage/RegisterPage.tsx +++ b/src/pages/RegisterPage/RegisterPage.tsx @@ -14,7 +14,13 @@ import { RegisterForm } from "./RegisterForm"; export const RegisterPage = () => ( - + a": { display: "contents" }, + }} + > By registering your account, you agree to the Fixit{" "} Terms of Service and the{" "} @@ -22,7 +28,14 @@ export const RegisterPage = () => ( . - + );