import { cookies } from "next/headers" import { redirect, RedirectType } from "next/navigation" import { z } from "zod" 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 { LangParams, PageArgs, SearchParams } from "@/types/params" import type { Lang } from "@/constants/languages" const otpError = z.enum(["invalidCode", "expiredCode"]) const searchParamsSchema = z.object({ intent: z.enum(["link"]), to: z.string(), error: otpError.optional(), }) export type OtpError = 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 }) } } return ( ( { id: "Please enter the code sent to in order to confirm your account linking.", }, { maskedContactInfo: () => ( <>
{to}
), } )} footnote={intl.formatMessage({ id: "This verifcation is needed for additional security.", })} otpLength={6} onSubmit={handleOtpVerified} error={error} /> ) } 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 (error) { 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?errorCode=link_error`, } } console.log("[SAS] link account response", res) switch (res.linkingState) { case "linked": return { url: `/${lang}/sas-x-scandic/link/success`, type: "replace", } } }