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 {
useSignInWithEmailPass,
useSignUpWithEmailPass,
} from "../../hooks/api/auth"
import { useAcceptInvite } from "../../hooks/api/invites"
import { isFetchError } from "../../lib/is-fetch-error"
import { AuthLoginResponse } from "@medusajs/js-sdk"
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: logInWithEmailPass, isPending: isLoggingIn } =
useSignInWithEmailPass()
const { mutateAsync: acceptInvite, isPending: isAcceptingInvite } =
useAcceptInvite(token)
/**
* Sign up or login with email and password.
* If the email already exists, it will try to login instead of signing up, to allow other invited actory types to accept the invite
* @param email - The email to sign up or login with.
* @param password - The password to sign up or login with.
* @returns The authentication result.
*/
const signUpOrLoginEmailPass = async (
email: string,
password: string
): Promise => {
try {
return await signUpEmailPass({
email,
password,
})
} catch (error) {
if (
isFetchError(error) &&
error.status === 401 &&
error.message === "Identity with email already exists"
) {
return await logInWithEmailPass({
email,
password,
})
}
throw error
}
}
const handleSubmit = form.handleSubmit(async (data) => {
try {
const authResult = await signUpOrLoginEmailPass(data.email, data.password)
// This should not happen since email verification is not enabled in the admin, but it should be covered as a scenario.
if (typeof authResult !== "string") {
throw new Error(
"Email verification is required, but not supported by this flow."
)
}
const authToken = authResult
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")}
)
}
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
}