diff --git a/package.json b/package.json index 96cbc8becd..86b9e3c3a5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App/App.tsx b/src/App/App.tsx index b824837356..d58d23e9e6 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -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"; @@ -152,6 +153,14 @@ function RootLayout() { + ); } diff --git a/src/auth/cognito.ts b/src/auth/cognito.ts index f315b313f9..f74e889391 100644 --- a/src/auth/cognito.ts +++ b/src/auth/cognito.ts @@ -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" @@ -30,16 +31,6 @@ interface OauthTokenRes { token_type: string; } -/**@template T - type of error to be handled */ -interface AuthError { - __type: T | (string & {}); - message: string; -} - -export const isError = (data: any): data is AuthError => { - return !!data.__type; -}; - /** type: bearer */ interface Token { id: string; @@ -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", @@ -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) { diff --git a/src/auth/load-auth.ts b/src/auth/load-auth.ts index e6c6793ac6..7d6073725f 100644 --- a/src/auth/load-auth.ts +++ b/src/auth/load-auth.ts @@ -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 => { try { diff --git a/src/components/Signup/ConfirmForm.tsx b/src/components/Signup/ConfirmForm.tsx index 058070de64..e44e304be3 100644 --- a/src/components/Signup/ConfirmForm.tsx +++ b/src/components/Signup/ConfirmForm.tsx @@ -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"; diff --git a/src/components/Signup/SignupForm.tsx b/src/components/Signup/SignupForm.tsx index 4ac192adda..5b2b2af4f3 100644 --- a/src/components/Signup/SignupForm.tsx +++ b/src/components/Signup/SignupForm.tsx @@ -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"; diff --git a/src/pages/ResetPassword/InitForm.tsx b/src/pages/ResetPassword/InitForm.tsx index 02eeb7d5ef..27a1fe39f7 100644 --- a/src/pages/ResetPassword/InitForm.tsx +++ b/src/pages/ResetPassword/InitForm.tsx @@ -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"; @@ -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"; diff --git a/src/pages/ResetPassword/SetPasswordForm.tsx b/src/pages/ResetPassword/SetPasswordForm.tsx index 442fb4af2f..0bd506cc50 100644 --- a/src/pages/ResetPassword/SetPasswordForm.tsx +++ b/src/pages/ResetPassword/SetPasswordForm.tsx @@ -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"; diff --git a/src/pages/SignUp/ConfirmForm.tsx b/src/pages/SignUp/ConfirmForm.tsx index 3baba71859..61007e86f5 100644 --- a/src/pages/SignUp/ConfirmForm.tsx +++ b/src/pages/SignUp/ConfirmForm.tsx @@ -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"; diff --git a/src/pages/SignUp/SignupForm/SignupForm.tsx b/src/pages/SignUp/SignupForm/SignupForm.tsx index 97d4a3a83e..b87309fffe 100644 --- a/src/pages/SignUp/SignupForm/SignupForm.tsx +++ b/src/pages/SignUp/SignupForm/SignupForm.tsx @@ -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"; @@ -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"; @@ -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; }} > diff --git a/src/pages/Signin.tsx b/src/pages/Signin.tsx index 1f5cebdcd8..34889adb6e 100644 --- a/src/pages/Signin.tsx +++ b/src/pages/Signin.tsx @@ -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({ __error: "Unknown error occured" }, 500); + } +}; export const loader: LoaderFunction = async ({ request, @@ -49,54 +91,61 @@ export const loader: LoaderFunction = async ({ export function Component() { // const navigate = useNavigate(); - const fetcher = useFetcher(); + const [params] = useSearchParams(); + const fetcher = useFetcher(); 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 (
-
+

Philanthropy for Everyone

Log in to support 18000+ causes or register and manage your nonprofit.

- + + OR -
+ Forgot password? -
+
By signing in, you agree to our{" "} { + __type: T | (string & {}); + message: string; +} + +export const isError = (data: any): data is AuthError => { + return !!data.__type; +}; + export interface DetailedUser extends UserV2 { /** deferred: detailed userV2.endowments */ orgs: Promise; @@ -64,3 +74,5 @@ export const signIn = v.object({ ), password: v.pipe(v.string("required"), v.nonEmpty("required")), }); + +export type SignIn = v.InferOutput; diff --git a/yarn.lock b/yarn.lock index 4ba68de170..29dfbe2558 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2237,6 +2237,7 @@ __metadata: react-player: "npm:2.16.0" react-redux: "npm:9.1.2" react-router-dom: "npm:6.23.0" + sonner: "npm:1.5.0" swiper: "npm:11.1.4" tailwindcss: "npm:3.4.3" type-fest: "npm:4.20.0" @@ -4950,6 +4951,16 @@ __metadata: languageName: node linkType: hard +"sonner@npm:1.5.0": + version: 1.5.0 + resolution: "sonner@npm:1.5.0" + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: 10/f189ec3cacf294b875eeb349faefa4be90d2ff4dcde6dc73b3b77a4a8ed6ff393a1d6a1b1b90ef86de861a4c84d988aec09558cabd39fcb1befddd384c4010b0 + languageName: node + linkType: hard + "source-map-js@npm:^1.2.0": version: 1.2.0 resolution: "source-map-js@npm:1.2.0"