import { cookies } from "next/headers" import { redirect } from "next/navigation" import { z } from "zod" import { myPages } from "@/constants/routes/myPages" import { serverClient } from "@/lib/trpc/server" import { getIntl } from "@/i18n" import { safeTry } from "@/utils/safeTry" import { SAS_TOKEN_STORAGE_KEY } from "../sasUtils" import OneTimePasswordForm, { type OnSubmitHandler, } from "./OneTimePasswordForm" import type { ReactNode } from "react" import type { LangParams, PageArgs, SearchParams } from "@/types/params" import type { Lang } from "@/constants/languages" const otpError = z.enum(["invalidCode", "expiredCode"]) const intent = z.enum(["link", "unlink"]) const searchParamsSchema = z.object({ intent: intent, to: z.string(), error: otpError.optional(), }) export type OtpError = z.infer type Intent = z.infer export default async function SASxScandicOneTimePasswordPage({ searchParams, params, }: PageArgs & SearchParams) { const intl = await getIntl() const cookieStore = cookies() const tokenCookie = cookieStore.get(SAS_TOKEN_STORAGE_KEY) const result = searchParamsSchema.safeParse(searchParams) if (!result.success) { throw new Error("Invalid search params") } const { intent, to, error } = result.data if (!verifyTokenValidity(tokenCookie?.value)) { redirect(`/${params.lang}/sas-x-scandic/login?intent=${intent}`) } const handleOtpVerified: OnSubmitHandler = async ({ otp }) => { "use server" const [data, error] = await safeTry( serverClient().partner.sas.verifyOtp({ otp }) ) if (error || !data) { throw error || new Error("OTP verification failed") } switch (data.status) { case "ABUSED": return { url: `/${params.lang}/sas-x-scandic/error?errorCode=tooManyFailedAttempts`, } case "EXPIRED": { const search = new URLSearchParams({ ...searchParams, error: "expiredCode", }).toString() return { url: `/${params.lang}/sas-x-scandic/otp?${search}`, } } case "RETRY": { const search = new URLSearchParams({ ...searchParams, error: "invalidCode", }).toString() return { url: `/${params.lang}/sas-x-scandic/otp?${search}`, } } case "NOTSENT": case "NULL": case "SENT": case "PENDING": // These errors should never happen for verify, but according to the API spec they can throw new Error("Unhandled OTP status") } switch (intent) { case "link": return handleLinkAccount({ lang: params.lang }) case "unlink": return handleUnlinkAccount({ lang: params.lang }) } } const maskedContactInfo = () => ( <>
{to}
) const intentDescriptions: Record = { link: intl.formatMessage( { id: "Please enter the code sent to in order to confirm your account linking.", }, { maskedContactInfo } ), unlink: intl.formatMessage( { id: "Please enter the code sent to in order to unlink your accounts.", }, { maskedContactInfo } ), } return ( ) } function verifyTokenValidity(token: string | undefined) { if (!token) { return false } try { const decoded = JSON.parse(atob(token.split(".")[1])) const expiry = decoded.exp * 1000 return Date.now() < expiry } catch { return false } } async function handleLinkAccount({ lang, }: { lang: Lang }): ReturnType { const [res, error] = await safeTry(serverClient().partner.sas.linkAccount()) if (!res || error) { console.error("[SAS] link account error", error) return { url: `/${lang}/sas-x-scandic/error`, } } switch (res.linkingState) { case "alreadyLinked": return { url: `/${lang}/sas-x-scandic/error?errorCode=alreadyLinked`, type: "replace", } case "linked": return { url: `/${lang}/sas-x-scandic/link/success`, type: "replace", } case "dateOfBirthMismatch": return { url: `/${lang}/sas-x-scandic/error?errorCode=dateOfBirthMismatch`, type: "replace", } case "error": return { url: `/${lang}/sas-x-scandic/error`, type: "replace", } } } async function handleUnlinkAccount({ lang, }: { lang: Lang }): ReturnType { const [res, error] = await safeTry(serverClient().partner.sas.unlinkAccount()) if (!res || error) { console.error("[SAS] unlink account error", error) return { url: `/${lang}/sas-x-scandic/error`, } } switch (res.linkingState) { case "unlinked": return { url: `/${lang}/sas-x-scandic/unlink/success`, type: "replace", } case "notLinked": return { url: myPages[lang], } case "error": return { url: `/${lang}/sas-x-scandic/error`, type: "replace", } } }