import { zodResolver } from "@hookform/resolvers/zod" import { Alert, Button, Heading, Hint, Input, Text, toast } from "@medusajs/ui" import i18n from "i18next" import { AnimatePresence, motion } from "motion/react" import { useState } from "react" import { useForm } from "react-hook-form" import { useTranslation } from "react-i18next" import { decodeToken } from "react-jwt" import { Link, useSearchParams } from "react-router-dom" import * as z from "zod" import { Form } from "../../components/common/form" import AvatarBox from "../../components/common/logo-box/avatar-box" import { useSignUpWithEmailPass } from "../../hooks/api/auth" import { useAcceptInvite } from "../../hooks/api/invites" import { isFetchError } from "../../lib/is-fetch-error" const CreateAccountSchema = z .object({ email: z.string().email(), first_name: z.string().min(1), last_name: z.string().min(1), password: z.string().min(1), repeat_password: z.string().min(1), }) .superRefine(({ password, repeat_password }, ctx) => { if (password !== repeat_password) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: i18n.t("invite.passwordMismatch"), path: ["repeat_password"], }) } }) // TODO: Update to V2 format type DecodedInvite = { id: string jti: any exp: string iat: number email: string } export const Invite = () => { const [searchParams] = useSearchParams() const [success, setSuccess] = useState(false) const token = searchParams.get("token") const invite: DecodedInvite | null = token ? decodeToken(token) : null const isValidInvite = invite && validateDecodedInvite(invite) return (
{isValidInvite ? ( {!success ? ( setSuccess(true)} token={token!} invite={invite} /> ) : ( )} ) : ( )}
) } const LoginLink = () => { const { t } = useTranslation() return (
{t("invite.backToLogin")}
) } const InvalidView = () => { const { t } = useTranslation() return (
{t("invite.invalidTokenTitle")} {t("invite.invalidTokenHint")}
) } const CreateView = ({ onSuccess, token, invite, }: { onSuccess: () => void token: string invite: DecodedInvite }) => { const { t } = useTranslation() const [invalid, setInvalid] = useState(false) const [params] = useSearchParams() const isFirstRun = params.get("first_run") === "true" // true when the invite page is open during a "create medusa app" run const form = useForm>({ resolver: zodResolver(CreateAccountSchema), defaultValues: { email: isFirstRun ? "" : invite.email || "", first_name: "", last_name: "", password: "", repeat_password: "", }, }) const { mutateAsync: signUpEmailPass, isPending: isCreatingAuthUser } = useSignUpWithEmailPass() const { mutateAsync: acceptInvite, isPending: isAcceptingInvite } = useAcceptInvite(token) const handleSubmit = form.handleSubmit(async (data) => { try { const authToken = await signUpEmailPass({ email: data.email, password: data.password, }) const invitePayload = { email: data.email, first_name: data.first_name, last_name: data.last_name, } await acceptInvite({ ...invitePayload, auth_token: authToken, }) toast.success(t("invite.toast.accepted")) onSuccess() } catch (error) { if (isFetchError(error) && error.status === 400) { form.setError("root", { type: "manual", message: t("invite.invalidInvite"), }) setInvalid(true) return } form.setError("root", { type: "manual", message: t("errors.serverError"), }) } }) const serverError = form.formState.errors.root?.message const validationError = form.formState.errors.email?.message || form.formState.errors.password?.message || form.formState.errors.repeat_password?.message || form.formState.errors.first_name?.message || form.formState.errors.last_name?.message return (
{t("invite.title")} {t("invite.hint")}
{ return ( ) }} /> { return ( ) }} /> { return ( ) }} /> { return ( ) }} /> { return ( ) }} /> {validationError && (
{validationError}
)} {serverError && ( {serverError} )}
) } const SuccessView = () => { const { t } = useTranslation() return (
{t("invite.successTitle")} {t("invite.successHint")}
{t("invite.backToLogin")}
) } const InviteSchema = z.object({ id: z.string(), jti: z.string(), exp: z.number(), iat: z.number(), }) const validateDecodedInvite = (decoded: any): decoded is DecodedInvite => { return InviteSchema.safeParse(decoded).success }