Skip to content

Commit

Permalink
signin to action
Browse files Browse the repository at this point in the history
  • Loading branch information
ap-justin committed Oct 30, 2024
1 parent 3827425 commit 1487785
Show file tree
Hide file tree
Showing 13 changed files with 146 additions and 68 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"react-player": "2.16.0",
"react-redux": "9.1.2",
"react-router-dom": "6.23.0",
"sonner": "1.5.0",
"swiper": "11.1.4",
"type-fest": "4.20.0",
"valibot": "0.42.0",
Expand Down
9 changes: 9 additions & 0 deletions src/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
ScrollRestoration,
useNavigation,
} from "react-router-dom";
import { Toaster } from "sonner";
import Layout from "./Layout";
import { rootAction } from "./root-action";
import { rootLoader } from "./root-loader";
Expand Down Expand Up @@ -152,6 +153,14 @@ function RootLayout() {
<ModalContext>
<ScrollRestoration />
<Outlet />
<Toaster
position="bottom-right"
toastOptions={{
classNames: {
error: "text-red",
},
}}
/>
</ModalContext>
);
}
17 changes: 3 additions & 14 deletions src/auth/cognito.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { IS_TEST } from "constants/env";
import { logger } from "helpers/logger";
import type { AuthError } from "types/auth";

const clientId = IS_TEST
? "7bl9gfckbneg0udsmkvsu48ssg"
Expand Down Expand Up @@ -30,16 +31,6 @@ interface OauthTokenRes {
token_type: string;
}

/**@template T - type of error to be handled */
interface AuthError<T extends string = string> {
__type: T | (string & {});
message: string;
}

export const isError = (data: any): data is AuthError => {
return !!data.__type;
};

/** type: bearer */
interface Token {
id: string;
Expand Down Expand Up @@ -289,7 +280,7 @@ class OAuth extends Storage {
: "https://bettergiving.auth.us-east-1.amazoncognito.com";
}

async initiate(state: string) {
initiateUrl(state: string) {
const scopes = [
"email",
"openid",
Expand All @@ -306,9 +297,7 @@ class OAuth extends Storage {
scope: scopes.join(" "),
});

window.location.href = `${
this.domain
}/oauth2/authorize?${params.toString()}`;
return `${this.domain}/oauth2/authorize?${params.toString()}`;
}

async exchange(code: string) {
Expand Down
4 changes: 2 additions & 2 deletions src/auth/load-auth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { logger } from "helpers";
import { decodeJwt } from "jose";
import type { UserV2 } from "types/auth";
import { cognito, isError } from "./cognito";
import { type UserV2, isError } from "types/auth";
import { cognito } from "./cognito";

export const loadAuth = async (): Promise<UserV2 | null> => {
try {
Expand Down
3 changes: 2 additions & 1 deletion src/components/Signup/ConfirmForm.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { yupResolver } from "@hookform/resolvers/yup";
import { cognito, isError } from "auth/cognito";
import { cognito } from "auth/cognito";
import { Field, RhfForm } from "components/form";
import { useErrorContext } from "contexts/ErrorContext";
import { useState } from "react";
import { type UseFormReturn, useForm } from "react-hook-form";
import { requiredString } from "schemas/string";
import { isError } from "types/auth";
import { object } from "yup";
import type { CodeRecipientEmail, StateSetter } from "./types";

Expand Down
3 changes: 2 additions & 1 deletion src/components/Signup/SignupForm.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { yupResolver } from "@hookform/resolvers/yup";
import { cognito, isError } from "auth/cognito";
import { cognito } from "auth/cognito";
import { Form } from "components/form";
import { useErrorContext } from "contexts/ErrorContext";
import { Eye, EyeOff, Lock } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { requiredString } from "schemas/string";
import { isError } from "types/auth";
import { object } from "yup";
import type { Donor, StateSetter } from "./types";

Expand Down
3 changes: 2 additions & 1 deletion src/pages/ResetPassword/InitForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { yupResolver } from "@hookform/resolvers/yup";
import { cognito, isError } from "auth/cognito";
import { cognito } from "auth/cognito";
import { Form, Input } from "components/form";
import { appRoutes } from "constants/routes";
import { useErrorContext } from "contexts/ErrorContext";
Expand All @@ -8,6 +8,7 @@ import { Mail } from "lucide-react";
import { type UseFormReturn, useForm } from "react-hook-form";
import { Link } from "react-router-dom";
import { requiredString } from "schemas/string";
import { isError } from "types/auth";
import { object } from "yup";
import type { StepSetter } from "./types";

Expand Down
3 changes: 2 additions & 1 deletion src/pages/ResetPassword/SetPasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { yupResolver } from "@hookform/resolvers/yup";
import { cognito, isError } from "auth/cognito";
import { cognito } from "auth/cognito";
import { Form, Input, PasswordInput } from "components/form";
import { useErrorContext } from "contexts/ErrorContext";
import useCounter from "hooks/useCounter";
import { useState } from "react";
import { type UseFormReturn, useForm } from "react-hook-form";
import { password, requiredString } from "schemas/string";
import { isError } from "types/auth";
import { object, ref } from "yup";
import type { CodeRecipientEmail, StepSetter } from "./types";

Expand Down
3 changes: 2 additions & 1 deletion src/pages/SignUp/ConfirmForm.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { yupResolver } from "@hookform/resolvers/yup";
import { cognito, isError } from "auth/cognito";
import { cognito } from "auth/cognito";
import { Form, Input } from "components/form";
import { useErrorContext } from "contexts/ErrorContext";
import useCounter from "hooks/useCounter";
import { useState } from "react";
import { type UseFormReturn, useForm } from "react-hook-form";
import { requiredString } from "schemas/string";
import { isError } from "types/auth";
import { object } from "yup";
import type { CodeRecipientEmail, StateSetter, UserType } from "./types";

Expand Down
7 changes: 4 additions & 3 deletions src/pages/SignUp/SignupForm/SignupForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { yupResolver } from "@hookform/resolvers/yup";
import googleIcon from "assets/icons/google.svg";
import { cognito, isError, oauth } from "auth/cognito";
import { cognito, oauth } from "auth/cognito";
import ExtLink from "components/ExtLink";
import Image from "components/Image";
import { Separator } from "components/Separator";
Expand All @@ -13,7 +13,7 @@ import { Mail } from "lucide-react";
import { useController, useForm } from "react-hook-form";
import { Link } from "react-router-dom";
import { password, requiredString } from "schemas/string";
import type { OAuthState, SignInRouteState } from "types/auth";
import { type OAuthState, type SignInRouteState, isError } from "types/auth";
import { mixed, object, ref } from "yup";
import type { FormValues, StateSetter, UserType } from "../types";
import UserTypeSelector from "./UserTypeSelector";
Expand Down Expand Up @@ -120,7 +120,8 @@ export default function SignupForm(props: Props) {
pathname: redirect.path,
data: redirect.data,
};
await oauth.initiate(JSON.stringify(state));
const to = oauth.initiateUrl(JSON.stringify(state));
window.location.href = to;
}}
>
<Image src={googleIcon} height={18} width={18} />
Expand Down
138 changes: 94 additions & 44 deletions src/pages/Signin.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,83 @@
import { getFormProps, getInputProps, useForm } from "@conform-to/react";
import {
type SubmissionResult,
getFormProps,
getInputProps,
useForm,
} from "@conform-to/react";
import googleIcon from "assets/icons/google.svg";
import { oauth } from "auth/cognito";
import { cognito, oauth } from "auth/cognito";
import { loadAuth } from "auth/load-auth";
import ExtLink from "components/ExtLink";
import Image from "components/Image";
import { Separator } from "components/Separator";
import { Form, Input, PasswordInput } from "components/form";
import { Input, PasswordInput } from "components/form";
import { parseWithValibot } from "conform-to-valibot";
import { appRoutes } from "constants/routes";
import { getAuthRedirect } from "helpers";
import { decodeState, toWithState } from "helpers/state-params";
import { Mail } from "lucide-react";
import { useEffect } from "react";
import {
type ActionFunction,
Link,
type LoaderFunction,
json,
redirect,
useFetcher,
useLoaderData,
useSearchParams,
} from "react-router-dom";
import { type OAuthState, signIn } from "types/auth";
import { toast } from "sonner";
import { type OAuthState, isError, signIn } from "types/auth";

type ActionData = { __error: string } | SubmissionResult | undefined;

const isActionErr = (data: ActionData): data is { __error: string } =>
data ? "__error" in data : false;
const isValiErr = (data: ActionData): data is SubmissionResult =>
data ? "status" in data : false;

export const action: ActionFunction = async ({ request }) => {
try {
const url = new URL(request.url);
const state = decodeState(url.searchParams.get("_s"));
const r = getAuthRedirect(state as any);

// async function submit(fv: FormValues) {
// try {
// const res = await cognito.initiate(fv.email.toLowerCase(), fv.password);
const fv = await request.formData();
if (fv.get("intent") === "oauth") {
const routeState: OAuthState = {
pathname: r.path,
data: r.data,
};
return redirect(oauth.initiateUrl(JSON.stringify(routeState)));
}

// if (isError(res)) {
// return displayError(res.message);
// }
// navigate(toWithState(redirect.path, redirect.data), { replace: true });
// return;
// } catch (err) {
// handleError(err, { context: "signing in" });
// }
// }
const payload = parseWithValibot(fv, { schema: signIn });

export const action: ActionFunction = async ({ request }) => {};
if (payload.status !== "success") return payload.reply();

const res = await cognito.initiate(
payload.value.email.toLowerCase(),
payload.value.password
);
if (isError(res)) {
return payload.reply({ fieldErrors: { password: [res.message] } });
}

const to = new URL(request.url);
to.pathname = r.path;
to.search = r.search;
if (r.data) {
const encoded = btoa(JSON.stringify(r.data));
to.searchParams.set("_s", encoded);
}

return redirect(to.toString());
} catch (err) {
console.error(err);
return json<ActionData>({ __error: "Unknown error occured" }, 500);
}
};

export const loader: LoaderFunction = async ({
request,
Expand All @@ -49,54 +91,61 @@ export const loader: LoaderFunction = async ({

export function Component() {
// const navigate = useNavigate();
const fetcher = useFetcher();
const [params] = useSearchParams();
const fetcher = useFetcher<ActionData>();
const fromState = useLoaderData() as unknown;
const redirect = getAuthRedirect(fromState as any);

const [form, fields] = useForm({
shouldRevalidate: "onInput",
// Optional: Required only if you're validating on the server
lastResult: typeof fetcher.data === "string" ? undefined : fetcher.data,
lastResult: isValiErr(fetcher.data) ? fetcher.data : undefined,
onValidate({ formData }) {
return parseWithValibot(formData, { schema: signIn });
},
});

useEffect(() => {
if (isActionErr(fetcher.data)) {
toast.error(fetcher.data.__error);
}
}, [fetcher.data]);

const isSubmitting = fetcher.state === "submitting";

return (
<div className="grid justify-items-center gap-3.5 px-4 py-14 text-navy-l1">
<Form
{...getFormProps(form)}
className="grid w-full max-w-md px-6 sm:px-7 py-7 sm:py-8 bg-white border border-gray-l4 rounded-2xl"
disabled={isSubmitting}
>
<div className="grid w-full max-w-md px-6 sm:px-7 py-7 sm:py-8 bg-white border border-gray-l4 rounded-2xl">
<h3 className="text-center text-2xl font-bold text-navy-d4">
Philanthropy for Everyone
</h3>
<p className="text-center font-normal max-sm:text-sm mt-2">
Log in to support 18000+ causes or register and manage your nonprofit.
</p>
<button
className="flex-center btn-outline-2 gap-2 h-12 sm:h-[52px] mt-6 border-[0.8px]"
type="button"
onClick={() => {
const routeState: OAuthState = {
pathname: redirect.path,
data: redirect.data,
};
oauth.initiate(JSON.stringify(routeState));
}}
<fetcher.Form
method="POST"
action={`.?${params.toString()}`}
className="contents"
>
<Image src={googleIcon} height={18} width={18} />
<span className="normal-case font-heading font-semibold text-navy-d4">
Continue with Google
</span>
</button>
<button
name="intent"
value="oauth"
type="submit"
className="flex-center btn-outline-2 gap-2 h-12 sm:h-[52px] mt-6 border-[0.8px]"
>
<Image src={googleIcon} height={18} width={18} />
<span className="normal-case font-heading font-semibold text-navy-d4">
Continue with Google
</span>
</button>
</fetcher.Form>
<Separator classes="my-4 before:mr-3.5 after:ml-3.5 before:bg-navy-l5 after:bg-navy-l5 font-medium text-[13px] text-navy-l3">
OR
</Separator>
<div className="grid gap-3">
<fetcher.Form
method="POST"
action={`.?${params.toString()}`}
{...getFormProps(form)}
className="grid gap-3"
>
<Input
{...getInputProps(fields.email, { type: "text" })}
placeholder="Email address"
Expand All @@ -115,8 +164,9 @@ export function Component() {
>
Forgot password?
</Link>
</div>
</fetcher.Form>
<button
form={form.id}
type="submit"
className="flex-center bg-blue-d1 disabled:bg-gray text-white enabled:hover:bg-blue enabled:active:bg-blue-d2 h-12 sm:h-[52px] rounded-full normal-case sm:text-lg font-bold w-full mt-4"
>
Expand All @@ -132,7 +182,7 @@ export function Component() {
Sign up
</Link>
</span>
</Form>
</div>
<span className="text-xs sm:text-sm text-center w-80">
By signing in, you agree to our{" "}
<ExtLink
Expand Down
Loading

0 comments on commit 1487785

Please sign in to comment.