Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update form content on magic link auth request #1502

Merged
merged 2 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { act, renderHook } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";

import { useMagicLinkLogin } from "../useMagicLinkLogin";

Expand All @@ -12,32 +12,40 @@ const mockUseSupabase = () => ({
},
},
});
const email = "user@quivr.app";
const watchMock = vi.fn(() => email);
vi.mock("react-hook-form", async () => {
const actual = await vi.importActual<typeof import("react-hook-form")>(
"react-hook-form"
);

return {
...actual,
useForm: () => ({
...actual.useForm(),
watch: watchMock,
}),
};
});

vi.mock("@/lib/context/SupabaseProvider", () => ({
useSupabase: () => mockUseSupabase(),
}));
const setEmail = vi.fn();

describe("useMagicLinkLogin", () => {
afterEach(() => {
vi.restoreAllMocks();
});

it("should not call signInWithOtp if email is empty", async () => {
const { result } = renderHook(() =>
useMagicLinkLogin({
email: "",
setEmail,
})
);
watchMock.mockReturnValueOnce("");
const { result } = renderHook(() => useMagicLinkLogin());
await act(() => result.current.handleMagicLinkLogin());
expect(mockSignInWithOtp).toHaveBeenCalledTimes(0);
});

it("should call signInWithOtp with proper arguments", async () => {
const email = "user@quivr.app";
const { result } = renderHook(() =>
useMagicLinkLogin({
email,
setEmail,
})
);
const { result } = renderHook(() => useMagicLinkLogin());
await result.current.handleMagicLinkLogin();
expect(mockSignInWithOtp).toHaveBeenCalledTimes(1);
expect(mockSignInWithOtp).toHaveBeenCalledWith({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";

import { useSupabase } from "@/lib/context/SupabaseProvider";
import { useToast } from "@/lib/hooks";

type UseMagicLinkLoginProps = {
email: string;
setEmail: (email: string) => void;
};

export const useMagicLinkLogin = ({
email,
setEmail,
}: UseMagicLinkLoginProps): {
handleMagicLinkLogin: () => Promise<void>;
isPending: boolean;
} => {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useMagicLinkLogin = () => {
const { supabase } = useSupabase();
const [isPending, setIsPending] = useState(false);
const { t } = useTranslation("login");
const { publish } = useToast();

const handleMagicLinkLogin = async () => {
const {
register,
watch,
setValue,
formState: { isSubmitSuccessful, isSubmitting },
handleSubmit,
reset,
} = useForm<{ email: string }>({
defaultValues: {
email: "",
},
});

const email = watch("email");

const handleMagicLinkLogin = handleSubmit(async (_, ev) => {
ev?.preventDefault();
if (email === "") {
publish({
variant: "danger",
Expand All @@ -31,8 +36,6 @@ export const useMagicLinkLogin = ({
return;
}

setIsPending(true);

const { error } = await supabase.auth.signInWithOtp({
email,
options: {
Expand All @@ -45,16 +48,19 @@ export const useMagicLinkLogin = ({
variant: "danger",
text: error.message,
});
} else {
publish({
variant: "success",
text: "Magic link sent successfully if email recognized",
});

setEmail("");
throw error; // this error is caught by react-hook-form
}
setIsPending(false);
};

return { handleMagicLinkLogin, isPending };
setValue("email", "");
});

return {
handleMagicLinkLogin,
isSubmitting,
register,
handleSubmit,
isSubmitSuccessful,
reset,
};
};
71 changes: 50 additions & 21 deletions frontend/app/(auth)/login/components/MagicLinkLogin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,61 @@
import { useTranslation } from "react-i18next";

import Button from "@/lib/components/ui/Button";
import Field from "@/lib/components/ui/Field";
import { emailPattern } from "@/lib/config/patterns";

import { useMagicLinkLogin } from "./hooks/useMagicLinkLogin";

type MaginLinkLoginProps = {
email: string;
setEmail: (email: string) => void;
};
export const MagicLinkLogin = (): JSX.Element => {
const {
handleMagicLinkLogin,
isSubmitting,
register,
isSubmitSuccessful,
reset,
} = useMagicLinkLogin();
const { t } = useTranslation(["login", "translation"]);

export const MagicLinkLogin = ({
email,
setEmail,
}: MaginLinkLoginProps): JSX.Element => {
const { handleMagicLinkLogin, isPending } = useMagicLinkLogin({
email,
setEmail,
});
const { t } = useTranslation(["login"]);
if (isSubmitSuccessful) {
return (
<div className="text-center flex flex-col gap-4">
<p>
{t("check_your_email.part1", { ns: "login" })}{" "}
<span className="font-semibold">
{t("check_your_email.magic_link", { ns: "login" })}
</span>{" "}
{t("check_your_email.part2", { ns: "login" })}
</p>
<div>
<span>{t("cant_find", { ns: "login" })}</span>{" "}
<span
className="cursor-pointer underline"
onClick={() => void reset()}
>
{t("try_again")}
</span>
</div>
</div>
);
}

return (
<Button
type="button"
onClick={() => void handleMagicLinkLogin()}
isLoading={isPending}
className="bg-black text-white py-2 font-normal"
>
{t("magicLink")}
</Button>
<form className="w-full" onSubmit={(e) => void handleMagicLinkLogin(e)}>
<Field
{...register("email", {
required: true,
pattern: emailPattern,
})}
placeholder={t("email", { ns: "login" })}
label={t("email", { ns: "translation" })}
inputClassName="py-1 mt-1 mb-3"
/>
<Button
isLoading={isSubmitting}
className="bg-black text-white py-2 font-normal w-full"
>
{t("magicLink", { ns: "login" })}
</Button>
</form>
);
};
9 changes: 1 addition & 8 deletions frontend/app/(auth)/login/hooks/useLogin.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { useEffect, useState } from "react";
import { useEffect } from "react";

import { useSupabase } from "@/lib/context/SupabaseProvider";
import { redirectToPreviousPageOrChatPage } from "@/lib/helpers/redirectToPreviousPageOrChatPage";
import { useEventTracking } from "@/services/analytics/june/useEventTracking";

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useLogin = () => {
const [email, setEmail] = useState("");

const { session } = useSupabase();

const { track } = useEventTracking();
Expand All @@ -18,9 +16,4 @@ export const useLogin = () => {
redirectToPreviousPageOrChatPage();
}
}, [session?.user]);

return {
setEmail,
email,
};
};
15 changes: 3 additions & 12 deletions frontend/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { useTranslation } from "react-i18next";

import { QuivrLogo } from "@/lib/assets/QuivrLogo";
import { Divider } from "@/lib/components/ui/Divider";
import Field from "@/lib/components/ui/Field";

import { GoogleLoginButton } from "./components/GoogleLogin";
import { MagicLinkLogin } from "./components/MagicLinkLogin";
import { useLogin } from "./hooks/useLogin";

const Main = (): JSX.Element => {
const { setEmail, email } = useLogin();
useLogin();

const { t } = useTranslation(["translation", "login"]);

return (
Expand All @@ -27,16 +27,7 @@ const Main = (): JSX.Element => {
<span className="text-primary">Quivr</span>
</p>
<div className="mt-5 flex flex-col">
<Field
name="email"
type="email"
placeholder={t("email")}
onChange={(e) => setEmail(e.target.value)}
value={email}
label="Email"
inputClassName="py-1 mt-1 mb-3"
/>
<MagicLinkLogin email={email} setEmail={setEmail} />
<MagicLinkLogin />
<Divider text={t("or")} className="my-3 uppercase" />
<GoogleLoginButton />
</div>
Expand Down
4 changes: 1 addition & 3 deletions frontend/app/contact/components/ContactForm.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import React from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { LuChevronRight } from "react-icons/lu";

import Button from "@/lib/components/ui/Button";
import Spinner from "@/lib/components/ui/Spinner";
import { emailPattern } from "@/lib/config/patterns";

import { usePostContactSales } from "../hooks/usePostContactSales";

const emailPattern = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;

export const ContactForm = (): JSX.Element => {
const { t } = useTranslation("contact", { keyPrefix: "form" });

Expand Down
1 change: 1 addition & 0 deletions frontend/lib/config/patterns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const emailPattern = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
9 changes: 8 additions & 1 deletion frontend/public/locales/en/login.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,12 @@
"errorMailMissed": "Please enter your email address",
"talk_to": "Talk to",
"restriction_message": "Unpaid users have access to a free and limited demo of Quivr",
"email":"Email address"
"email":"Email address",
"cant_find":"Can't find it ?",
"try_again":"Try again",
"check_your_email":{
"part1":"We just sent you a ",
"magic_link":"Magic link",
"part2":", check your emails and follow the steps."
}
}
19 changes: 13 additions & 6 deletions frontend/public/locales/es/login.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
{
"googleLogin": "Continuar con Google",
"magicLink": "Continuar con correo electrónico",
"errorMailMissed": "Por favor, ingrese su dirección de correo electrónico",
"talk_to": "Hablar con",
"restriction_message": "Los usuarios no pagos tienen acceso a una demostración gratuita y limitada de Quivr",
"email":"Dirección de correo electrónico"
"googleLogin": "Continuar con Google",
"magicLink": "Continuar con correo electrónico",
"errorMailMissed": "Por favor, ingrese su dirección de correo electrónico",
"talk_to": "Hablar con",
"restriction_message": "Los usuarios no pagos tienen acceso a una demostración gratuita y limitada de Quivr",
"email":"Dirección de correo electrónico",
"cant_find":"¿No lo encuentras?",
"try_again":"Inténtalo de nuevo",
"check_your_email": {
"part1":"Acabamos de enviarte un ",
"magic_link":"enlace mágico",
"part2":". Revisa tus correos electrónicos y sigue los pasos."
}
}
19 changes: 13 additions & 6 deletions frontend/public/locales/fr/login.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
{
"googleLogin": "Continuer avec Google",
"magicLink": "Continuer avec e-mail",
"errorMailMissed": "Veuillez saisir votre adresse e-mail",
"talk_to": "Parler à",
"restriction_message": "Les utilisateurs non payants ont accès à une démonstration gratuite et limitée de Quivr",
"email":"Adresse e-mail"
"googleLogin": "Continuer avec Google",
"magicLink": "Continuer avec e-mail",
"errorMailMissed": "Veuillez saisir votre adresse e-mail",
"talk_to": "Parler à",
"restriction_message": "Les utilisateurs non payants ont accès à une démonstration gratuite et limitée de Quivr",
"email":"Adresse e-mail",
"cant_find":"Vous ne le trouvez pas ?",
"try_again":"Réessayez",
"check_your_email": {
"part1":"Nous venons de vous envoyer un ",
"magic_link":"lien magique",
"part2":". Vérifiez vos e-mails et suivez les étapes."
}
}
19 changes: 13 additions & 6 deletions frontend/public/locales/pt-br/login.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
{
"googleLogin": "Continuar com o Google",
"magicLink": "Continuar com o e-mail",
"errorMailMissed": "Por favor, insira seu endereço de e-mail",
"talk_to": "Converse com",
"restriction_message": "Usuários não pagos têm acesso a uma demonstração gratuita e limitada do Quivr",
"email":"Email address"
"googleLogin": "Continuar com o Google",
"magicLink": "Continuar com o e-mail",
"errorMailMissed": "Por favor, insira seu endereço de e-mail",
"talk_to": "Converse com",
"restriction_message": "Usuários não pagos têm acesso a uma demonstração gratuita e limitada do Quivr",
"email":"Email address",
"cant_find":"Não consegue encontrar?",
"try_again":"Tente novamente",
"check_your_email":{
"part1":"Acabamos de enviar um ",
"magic_link":"link mágico",
"part2":" para você, verifique seus emails e siga as instruções."
}
}
Loading
Loading