{heading}
@@ -110,10 +155,10 @@ export default function OneTimePasswordForm({
>
)}
/>
- {error && (
+ {errorMessage && (
-
{error}
+ {errorMessage}
)}
@@ -124,18 +169,7 @@ export default function OneTimePasswordForm({
id: "Didn't receive a code? Resend code",
},
{
- resendOtpLink: (str) => (
-
- {str}
-
- ),
+ resendOtpLink: getResendOtpLink,
}
)}
@@ -176,23 +210,3 @@ const getRequestErrorBody = (
})
}
}
-
-const getVerifyErrorBody = (
- intl: ReturnType,
- errorCode: VerifyOtpError["errorCode"]
-) => {
- switch (errorCode) {
- case "WRONG_OTP":
- return intl.formatMessage({
- id: "The code you entered is incorrect. Please try again.",
- })
- case "OTP_EXPIRED":
- return intl.formatMessage({
- id: "OTP has expired. Please try again.",
- })
- default:
- return intl.formatMessage({
- id: "An error occurred while requesting a new OTP",
- })
- }
-}
diff --git a/app/[lang]/(partner)/(sas)/(protected)/sas-x-scandic/otp/page.tsx b/app/[lang]/(partner)/(sas)/(protected)/sas-x-scandic/otp/page.tsx
index cf7df475e..b03dfd0a1 100644
--- a/app/[lang]/(partner)/(sas)/(protected)/sas-x-scandic/otp/page.tsx
+++ b/app/[lang]/(partner)/(sas)/(protected)/sas-x-scandic/otp/page.tsx
@@ -13,12 +13,15 @@ import OneTimePasswordForm 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: z.enum(["invalidCode"]).optional(),
+ error: otpError.optional(),
})
+export type OtpError = z.infer
+
export default async function SASxScandicOneTimePasswordPage({
searchParams,
params,
@@ -37,37 +40,48 @@ export default async function SASxScandicOneTimePasswordPage({
redirect(`/${params.lang}/sas-x-scandic/login?intent=${intent}`)
}
- const errors = {
- invalidCode: intl.formatMessage({
- id: "The code you’ve entered is incorrect.",
- }),
- }
-
async function handleOtpVerified({ otp }: { otp: string }) {
"use server"
-
const [data, error] = await safeTry(
serverClient().partner.sas.verifyOtp({ otp })
)
- // TODO correct status?
- // TODO handle all errors
- // STATUS === VERIFIED => ok
- // STATUS === ABUSED => otpRetryCount > otpMaxRetryCount
- if (error || data?.status !== "VERIFIED") {
- const search = new URLSearchParams({
- ...searchParams,
- error: "invalidCode",
- }).toString()
+ if (error || !data) {
+ throw error || new Error("OTP verification failed")
+ }
- redirect(`/${params.lang}/sas-x-scandic/otp?${search}`)
+ switch (data.status) {
+ case "ABUSED":
+ redirect(
+ `/${params.lang}/sas-x-scandic/error?errorCode=tooManyFailedAttempts`
+ )
+ case "EXPIRED": {
+ const search = new URLSearchParams({
+ ...searchParams,
+ error: "expiredCode",
+ }).toString()
+
+ redirect(`/${params.lang}/sas-x-scandic/otp?${search}`)
+ }
+ case "RETRY": {
+ const search = new URLSearchParams({
+ ...searchParams,
+ error: "invalidCode",
+ }).toString()
+
+ redirect(`/${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 })
- default:
- throw new Error("")
}
}
@@ -93,7 +107,7 @@ export default async function SASxScandicOneTimePasswordPage({
})}
otpLength={6}
onSubmit={handleOtpVerified}
- error={error ? errors[error] : undefined}
+ error={error}
/>
)
}
diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json
index cf835975e..b59fca4eb 100644
--- a/i18n/dictionaries/en.json
+++ b/i18n/dictionaries/en.json
@@ -523,6 +523,7 @@
"Terms and conditions": "Terms and conditions",
"Thank you": "Thank you",
"Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please contact us.": "Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please contact us.",
+ "The code you’ve entered have expired. Resend code.": "The code you’ve entered have expired. Resend code.",
"The code you’ve entered is incorrect.": "The code you’ve entered is incorrect.",
"The new price is": "The new price is",
"The price has increased": "The price has increased",
diff --git a/server/routers/partners/sas/otp/request/requestOtp.ts b/server/routers/partners/sas/otp/request/requestOtp.ts
index be0434001..700989a5f 100644
--- a/server/routers/partners/sas/otp/request/requestOtp.ts
+++ b/server/routers/partners/sas/otp/request/requestOtp.ts
@@ -18,7 +18,16 @@ import type { OtpState } from "../getOTPState"
const inputSchema = z.object({})
const outputSchema = z.object({
- status: z.string(),
+ status: z.enum([
+ "VERIFIED",
+ "ABUSED",
+ "EXPIRED",
+ "PENDING",
+ "RETRY",
+ "SENT",
+ "NULL",
+ "NOTSENT",
+ ]),
referenceId: z.string().uuid(),
databaseUUID: z.string().uuid(),
otpExpiration: z.number(),
@@ -42,14 +51,15 @@ export const requestOtp = protectedProcedure
tokenResponse.status,
tokenResponse.statusText
)
- if (!tokenResponse.ok) {
- const errorBody = await tokenResponse.json()
- console.error("[SAS] requestOtp error", errorBody)
- throw createError(errorBody)
- }
- const parseResult = outputSchema.safeParse(await tokenResponse.json())
+ const body = await tokenResponse.json()
+ const parseResult = outputSchema.safeParse(body)
if (!parseResult.success) {
+ console.error("[SAS] requestOtp error", body)
+
+ if (!tokenResponse.ok) {
+ throw createError(body)
+ }
throw createError(parseResult.error)
}
diff --git a/server/routers/partners/sas/otp/request/requestOtpError.test.ts b/server/routers/partners/sas/otp/request/requestOtpError.test.ts
index 5bac6bcca..371711c34 100644
--- a/server/routers/partners/sas/otp/request/requestOtpError.test.ts
+++ b/server/routers/partners/sas/otp/request/requestOtpError.test.ts
@@ -4,13 +4,6 @@ import { parseSASRequestOtpError } from "./requestOtpError"
describe("requestOtpError", () => {
it("parses error with invalid error code", () => {
- const error = {
- status: "status",
- error: "error",
- errorCode: "a",
- databaseUUID: "9ffefefe-df0e-4229-9792-5ed31bef1db4",
- }
-
const actual = parseSASRequestOtpError({
status: "status",
error: "error",
@@ -21,17 +14,4 @@ describe("requestOtpError", () => {
errorCode: "UNKNOWN",
})
})
-
- it("parses error as TOO_MANY_REQUESTS error code", () => {
- const actual = parseSASRequestOtpError({
- status: "status",
- error: "error",
- errorCode: 10,
- databaseUUID: "9ffefefe-df0e-4229-9792-5ed31bef1db4",
- otpExpiration: "2021-09-01T00:00:00Z",
- })
- expect(actual).toEqual({
- errorCode: "TOO_MANY_REQUESTS",
- })
- })
})
diff --git a/server/routers/partners/sas/otp/request/requestOtpError.ts b/server/routers/partners/sas/otp/request/requestOtpError.ts
index 8f1621c5f..bd2b358eb 100644
--- a/server/routers/partners/sas/otp/request/requestOtpError.ts
+++ b/server/routers/partners/sas/otp/request/requestOtpError.ts
@@ -51,7 +51,16 @@ const getErrorCodeByNumber = (number: number): RequestOtpResponseError => {
}
const sasOtpRequestErrorSchema = z.object({
- status: z.string(),
+ status: z.enum([
+ "VERIFIED",
+ "ABUSED",
+ "EXPIRED",
+ "PENDING",
+ "RETRY",
+ "SENT",
+ "NULL",
+ "NOTSENT",
+ ]),
otpExpiration: z.string().datetime(),
error: z.string(),
errorCode: z.number(),
diff --git a/server/routers/partners/sas/otp/verify/verifyOtp.ts b/server/routers/partners/sas/otp/verify/verifyOtp.ts
index 91e7e372e..3c868b658 100644
--- a/server/routers/partners/sas/otp/verify/verifyOtp.ts
+++ b/server/routers/partners/sas/otp/verify/verifyOtp.ts
@@ -17,7 +17,16 @@ const inputSchema = z.object({
})
const outputSchema = z.object({
- status: z.string(), // TODO: Change to enum
+ status: z.enum([
+ "VERIFIED",
+ "ABUSED",
+ "EXPIRED",
+ "PENDING",
+ "RETRY",
+ "SENT",
+ "NULL",
+ "NOTSENT",
+ ]),
referenceId: z.string().uuid(),
databaseUUID: z.string().uuid().optional(),
})
@@ -29,7 +38,6 @@ export const verifyOtp = protectedProcedure
const sasAuthToken = getSasToken()
if (!sasAuthToken) {
- // TODO: Should we verify that the SAS token isn't expired?
throw createError("AUTH_TOKEN_NOT_FOUND")
}