mirror of
https://github.com/StanGirard/quivr.git
synced 2024-11-28 13:42:41 +03:00
feat: update form content on magic link auth request (#1502)
Issue: https://github.com/StanGirard/quivr/issues/1407 https://github.com/StanGirard/quivr/assets/63923024/63d36681-f614-456f-8f65-9b726258f709
This commit is contained in:
parent
202daac77a
commit
e15177b7ab
@ -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";
|
||||
|
||||
@ -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({
|
||||
|
@ -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",
|
||||
@ -31,8 +36,6 @@ export const useMagicLinkLogin = ({
|
||||
return;
|
||||
}
|
||||
|
||||
setIsPending(true);
|
||||
|
||||
const { error } = await supabase.auth.signInWithOtp({
|
||||
email,
|
||||
options: {
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||
import { redirectToPreviousPageOrChatPage } from "@/lib/helpers/redirectToPreviousPageOrChatPage";
|
||||
@ -6,8 +6,6 @@ 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();
|
||||
@ -18,9 +16,4 @@ export const useLogin = () => {
|
||||
redirectToPreviousPageOrChatPage();
|
||||
}
|
||||
}, [session?.user]);
|
||||
|
||||
return {
|
||||
setEmail,
|
||||
email,
|
||||
};
|
||||
};
|
||||
|
@ -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 (
|
||||
@ -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>
|
||||
|
@ -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" });
|
||||
|
||||
|
1
frontend/lib/config/patterns.ts
Normal file
1
frontend/lib/config/patterns.ts
Normal file
@ -0,0 +1 @@
|
||||
export const emailPattern = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
|
@ -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."
|
||||
}
|
||||
}
|
@ -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."
|
||||
}
|
||||
}
|
@ -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."
|
||||
}
|
||||
}
|
@ -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."
|
||||
}
|
||||
}
|
@ -1,8 +1,15 @@
|
||||
{
|
||||
"googleLogin": "Продолжить с Google",
|
||||
"magicLink": "Продолжить с электронной почтой",
|
||||
"errorMailMissed": "Пожалуйста, введите ваш адрес электронной почты",
|
||||
"talk_to": "Общение с",
|
||||
"restriction_message": "Неоплаченным пользователям доступен бесплатный и ограниченный демонстрационный доступ к Quivr",
|
||||
"email":"Адрес электронной почты"
|
||||
"googleLogin": "Продолжить с Google",
|
||||
"magicLink": "Продолжить с электронной почтой",
|
||||
"errorMailMissed": "Пожалуйста, введите ваш адрес электронной почты",
|
||||
"talk_to": "Общение с",
|
||||
"restriction_message": "Неоплаченным пользователям доступен бесплатный и ограниченный демонстрационный доступ к Quivr",
|
||||
"email":"Адрес электронной почты",
|
||||
"cant_find":"Не можете найти?",
|
||||
"try_again":"Попробуйте еще раз",
|
||||
"check_your_email": {
|
||||
"part1":"Мы только что отправили вам ",
|
||||
"magic_link":"волшебную ссылку",
|
||||
"part2":". Проверьте свою электронную почту и следуйте инструкциям."
|
||||
}
|
||||
}
|
@ -1,8 +1,15 @@
|
||||
{
|
||||
"googleLogin": "使用Google继续",
|
||||
"magicLink": "使用电子邮件继续",
|
||||
"errorMailMissed": "请输入您的电子邮件地址",
|
||||
"talk_to": "与之交谈",
|
||||
"restriction_message": "未付费用户可以访问 Quivr 的免费和有限演示",
|
||||
"email":"电子邮件地址"
|
||||
"googleLogin": "使用Google继续",
|
||||
"magicLink": "使用电子邮件继续",
|
||||
"errorMailMissed": "请输入您的电子邮件地址",
|
||||
"talk_to": "与之交谈",
|
||||
"restriction_message": "未付费用户可以访问 Quivr 的免费和有限演示",
|
||||
"email":"电子邮件地址",
|
||||
"cant_find":"找不到吗?",
|
||||
"try_again":"再试一次",
|
||||
"check_your_email": {
|
||||
"part1":"我们刚刚发送了一个",
|
||||
"magic_link":"魔法链接",
|
||||
"part2":",请查看您的电子邮件并按照步骤进行操作。"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user