Skip to content

Commit

Permalink
feat: add GoogleOAuth btns to Login+Register
Browse files Browse the repository at this point in the history
  • Loading branch information
trevor-anderson committed Mar 24, 2024
1 parent 8947030 commit 2422af1
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 37 deletions.
34 changes: 25 additions & 9 deletions src/pages/LoginPage/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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" });
Expand All @@ -26,27 +37,32 @@ export const LoginForm = () => {
initialValues={loginFormInitialValues}
validationSchema={loginFormSchema}
onSubmit={onSubmit}
sx={{ all: "inherit" }}
>
<TextInput id="email" type="email" autoComplete="email" />
<EmailInput id="email" />
<PasswordInput id="password" autoComplete="current-password" />
<FormSubmitButton />

{error && <ErrorDialog error={error} onDismiss={clearError} />}

<DividerWithText flexItem>OR</DividerWithText>

<GoogleAuthFormButton text="signin_with" requiredFieldInputs={<EmailInput id="email" />} />
</Form>
);
};

/**
* 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<typeof loginFormSchema>;
export type LoginFormValues = InferType<typeof loginFormSchema>;
15 changes: 11 additions & 4 deletions src/pages/LoginPage/LoginPage.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
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";

/**
* **LoginPage** - renders when path is "/login"
*/
export const LoginPage = () => (
<AuthPageLayout pageTitle="User Login">
<AuthPageLayout
pageTitle="User Login"
sx={{
[`&.${authPageLayoutClassNames.root}`]: {
justifyContent: "center",
gap: "1.5rem",
},
}}
>
<LoginForm />
<Box
style={{
alignSelf: "center",
marginTop: "0.5rem",
whiteSpace: "pre-line",
marginTop: "1.5rem",
display: "flex",
flexDirection: "row",
alignItems: "center",
Expand Down
89 changes: 67 additions & 22 deletions src/pages/RegisterPage/RegisterForm.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import { object as yupObject, type InferType } from "yup";
import InputAdornment from "@mui/material/InputAdornment";
import { useFetchStateContext } from "@/app/FetchStateContext";
import { Form, FormSubmitButton, TextInput, PasswordInput, PhoneInput } from "@/components/Form";
import { GoogleAuthFormButton } from "@/app/GoogleOAuthContext/GoogleAuthFormButton";
import { DividerWithText } from "@/components/DataDisplay";
import { Form, FormSubmitButton } from "@/components/Form";
import { UserHandleInput, EmailInput, PasswordInput, PhoneInput } from "@/components/Form/Inputs";
import { yupCommonSchema, getInitialValuesFromSchema } from "@/components/Form/helpers";
import { ErrorDialog } from "@/components/Indicators";
import { APP_PATHS } from "@/routes/appPaths";
Expand All @@ -13,43 +15,79 @@ export const RegisterForm = () => {
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 (
<Form<RegisterFormValues>
initialValues={registerFormInitialValues}
validationSchema={registerFormSchema}
onSubmit={handleSubmit}
sx={{ all: "inherit" }}
>
<TextInput
id="handle"
InputProps={{
startAdornment: <InputAdornment position="start">@</InputAdornment>,
}}
/>
<UserHandleInput id="handle" />
<PhoneInput id="phone" />
<TextInput id="email" type="email" autoComplete="email" />
<EmailInput id="email" />
<PasswordInput id="password" autoComplete="new-password" />
<FormSubmitButton />

{error && <ErrorDialog title="Invalid Input" error={error} onDismiss={clearError} />}

<DividerWithText flexItem>OR</DividerWithText>

<GoogleAuthFormButton<RegisterFormValues>
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={
<>
<UserHandleInput id="handle" />
<EmailInput id="email" />
</>
}
/>
</Form>
);
};
Expand All @@ -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,
});

/**
Expand Down
17 changes: 15 additions & 2 deletions src/pages/RegisterPage/RegisterPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,28 @@ import { RegisterForm } from "./RegisterForm";
export const RegisterPage = () => (
<AuthPageLayout pageTitle="User Registration">
<RegisterForm />
<Text variant="caption" style={{ marginTop: "1.5rem" }}>
<Text
variant="caption"
sx={{
whiteSpace: "pre-line",
"& > a": { display: "contents" },
}}
>
By registering your account, you agree to the Fixit{" "}
<Link to={APP_PATHS.ToS}>Terms of Service</Link> and the{" "}
<Anchor href="https://stripe.com/connect-account/legal/full">
Stripe Connected Account Agreement
</Anchor>
.
</Text>
<LegalLinks style={{ margin: "1rem 0 0 0.8rem" }} />
<LegalLinks
style={{
/* This comp is nudged to the right by the below margin in order to
have the center of the form/page align with the center of the middle
word, rather than the center of the container (looks better). */
marginLeft: "1.25rem",
}}
/>
</AuthPageLayout>
);

Expand Down

0 comments on commit 2422af1

Please sign in to comment.