Merged in feat/server-action-trpc (pull request #422)

feat(SW-160): update profile

Approved-by: Christel Westerberg
This commit is contained in:
Simon.Emanuelsson
2024-08-23 07:31:06 +00:00
committed by Michael Zetterberg
32 changed files with 459 additions and 244 deletions

View File

@@ -1,64 +1,100 @@
"use server"
import { ZodError } from "zod"
import { z } from "zod"
import { ApiLang } from "@/constants/languages"
import * as api from "@/lib/api"
import { protectedServerActionProcedure } from "@/server/trpc"
import { editProfileSchema } from "@/components/Forms/Edit/Profile/schema"
import { countriesMap } from "@/components/TempDesignSystem/Form/Country/countries"
import { phoneValidator } from "@/utils/phoneValidator"
import { type State, Status } from "@/types/components/myPages/myProfile/edit"
import { Status } from "@/types/components/myPages/myProfile/edit"
export async function editProfile(_prevState: State, values: FormData) {
try {
const data: Record<string, any> = Object.fromEntries(values.entries())
/**
* TODO: Update profile data when endpoint from
* API team is ready
*/
console.info(`Raw Data`)
console.log(data)
data.address = {
city: data["address.city"],
countryCode: data["address.countryCode"],
streetAddress: data["address.streetAddress"],
zipCode: data["address.zipCode"],
const editProfilePayload = z
.object({
address: z.object({
city: z.string().optional(),
countryCode: z.nativeEnum(countriesMap),
streetAddress: z.string().optional(),
zipCode: z.string().min(1, { message: "Zip code is required" }),
}),
dateOfBirth: z.string(),
email: z.string().email(),
language: z.nativeEnum(ApiLang),
newPassword: z.string().optional(),
password: z.string().optional(),
phoneNumber: phoneValidator("Phone is required"),
})
.transform((data) => {
if (!data.password || !data.newPassword) {
delete data.password
delete data.newPassword
}
const parsedData = editProfileSchema.safeParse(data)
if (parsedData.success) {
console.info(`Success`)
console.log(parsedData.data)
return {
message: "All good!",
status: Status.success,
}
} else {
console.error("Error parsing edit profile")
console.error(parsedData.error)
return {
message: "Invalid data, parse failed!",
status: Status.error,
}
}
} catch (error) {
if (error instanceof ZodError) {
console.error(`ZodError handling profile edit`)
console.error(error)
return data
})
export const editProfile = protectedServerActionProcedure
.input(editProfileSchema)
.mutation(async function ({ ctx, input }) {
const payload = editProfilePayload.safeParse(input)
if (!payload.success) {
return {
errors: error.issues.map((issue) => ({
message: `Server validation: ${issue.message}`,
path: issue.path.join("."),
data: input,
issues: payload.error.issues.map((issue) => ({
field: issue.path.join("."),
message: issue.message,
})),
message: "Invalid form data",
message: "Validation failed.",
status: Status.error,
}
}
console.error(`EditProfile Server Action Error`)
console.error(error)
const response = await api.patch(api.endpoints.v1.profile, {
body: payload.data,
cache: "no-store",
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
})
if (!response.ok) {
console.info(`Response not ok`)
console.error(response)
return {
data: input,
issues: [],
message: "Server error",
status: Status.error,
}
}
const json = await response.json()
if (json.errors?.length) {
json.errors.forEach((error: any) => {
console.info(`API Fail in response`)
console.error(error)
})
}
const validatedData = editProfileSchema.safeParse(json.data.attributes)
if (!validatedData.success) {
console.log({ ees: validatedData.error })
return {
data: input,
issues: validatedData.error.issues.map((issue) => ({
field: issue.path.join("."),
message: issue.message,
})),
message: "Data is insufficient",
status: Status.error,
}
}
return {
message: "Something went wrong. Please try again.",
status: Status.error,
data: validatedData.data,
message: "All good!",
status: Status.success,
}
}
}
})

View File

@@ -1,6 +1,7 @@
.container {
display: grid;
gap: var(--Spacing-x2);
justify-items: flex-start;
max-width: 510px;
}
@@ -18,4 +19,4 @@
.container {
gap: var(--Spacing-x3);
}
}
}

View File

@@ -1,5 +1,5 @@
/**
* Due to css import issues with parallell routes we are forced to
* Due to css import issues with parallel routes we are forced to
* use a regular css file and import it in the page.tsx
*/
.profile-layout {

View File

@@ -233,4 +233,4 @@ export const {
auth,
signIn,
signOut,
} = NextAuth(config)
} = NextAuth(config)

View File

@@ -10,3 +10,9 @@
gap: var(--Spacing-x2);
grid-template-columns: max(164px) 1fr;
}
@media (min-width: 768px) {
.divider {
display: none;
}
}

View File

@@ -4,6 +4,7 @@ import { useIntl } from "react-intl"
import { languageSelect } from "@/constants/languages"
import Divider from "@/components/TempDesignSystem/Divider"
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
import DateSelect from "@/components/TempDesignSystem/Form/Date"
import Input from "@/components/TempDesignSystem/Form/Input"
@@ -15,24 +16,24 @@ import Body from "@/components/TempDesignSystem/Text/Body"
import styles from "./formContent.module.css"
export default function FormContent() {
const { formatMessage } = useIntl()
const intl = useIntl()
// const { pending } = useFormStatus()
const city = formatMessage({ id: "City" })
const country = formatMessage({ id: "Country" })
const email = `${formatMessage({ id: "Email" })} ${formatMessage({ id: "Address" }).toLowerCase()}`
const street = formatMessage({ id: "Address" })
const phoneNumber = formatMessage({ id: "Phone number" })
const password = formatMessage({ id: "Current password" })
const retypeNewPassword = formatMessage({ id: "Retype new password" })
const zipCode = formatMessage({ id: "Zip code" })
const city = intl.formatMessage({ id: "City" })
const country = intl.formatMessage({ id: "Country" })
const email = `${intl.formatMessage({ id: "Email" })} ${intl.formatMessage({ id: "Address" }).toLowerCase()}`
const street = intl.formatMessage({ id: "Address" })
const phoneNumber = intl.formatMessage({ id: "Phone number" })
const password = intl.formatMessage({ id: "Current password" })
const retypeNewPassword = intl.formatMessage({ id: "Retype new password" })
const zipCode = intl.formatMessage({ id: "Zip code" })
return (
<>
<section className={styles.user}>
<header>
<Body textTransform="bold">
{formatMessage({ id: "User information" })}
{intl.formatMessage({ id: "User information" })}
</Body>
</header>
<DateSelect name="dateOfBirth" registerOptions={{ required: true }} />
@@ -59,16 +60,19 @@ export default function FormContent() {
<Phone label={phoneNumber} name="phoneNumber" />
<Select
items={languageSelect}
label={formatMessage({ id: "Language" })}
label={intl.formatMessage({ id: "Language" })}
name="language"
placeholder={formatMessage({ id: "Select language" })}
placeholder={intl.formatMessage({ id: "Select language" })}
/>
</section>
<Divider className={styles.divider} color="subtle" />
<section className={styles.password}>
<header>
<Body textTransform="bold">{formatMessage({ id: "Password" })}</Body>
<Body textTransform="bold">
{intl.formatMessage({ id: "Password" })}
</Body>
</header>
<Input label={password} name="currentPassword" type="password" />
<Input label={password} name="password" type="password" />
<NewPassword />
<Input
label={retypeNewPassword}

View File

@@ -24,10 +24,6 @@
grid-area: buttons;
}
.btn {
justify-content: center;
}
@media screen and (min-width: 768px) {
.container {
grid-template-areas:
@@ -40,9 +36,9 @@
}
.btnContainer {
align-self: center;
flex-direction: row;
gap: var(--Spacing-x2);
justify-self: flex-end;
align-self: center;
}
}
}

View File

@@ -1,8 +1,8 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { isValidPhoneNumber, parsePhoneNumber } from "libphonenumber-js"
import { useParams } from "next/navigation"
import { useFormState as useReactFormState } from "react-dom"
import { useParams, useRouter } from "next/navigation"
import { useEffect, useState } from "react"
import { FormProvider, useForm } from "react-hook-form"
import { usePhoneInput } from "react-international-phone"
import { useIntl } from "react-intl"
@@ -13,32 +13,32 @@ import { editProfile } from "@/actions/editProfile"
import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link"
import Title from "@/components/TempDesignSystem/Text/Title"
import { toast } from "@/components/TempDesignSystem/Toasts"
import FormContent from "./FormContent"
import { type EditProfileSchema, editProfileSchema } from "./schema"
import styles from "./form.module.css"
import type {
EditFormProps,
State,
import {
type EditFormProps,
Status,
} from "@/types/components/myPages/myProfile/edit"
import type { Lang } from "@/constants/languages"
const formId = "edit-profile"
export default function Form({ user }: EditFormProps) {
const { formatMessage } = useIntl()
const intl = useIntl()
const router = useRouter()
const params = useParams()
const lang = params.lang as Lang
/**
* like react, react-hook-form also exports a useFormState hook,
* we want to clearly keep them separate by naming.
* RHF isValid defaults to false and never
* changes when JS is disabled, therefore
* we need to keep it insync ourselves
*/
const [state, formAction] = useReactFormState<State, FormData>(
editProfile,
null
)
const [isValid, setIsValid] = useState(true)
const { inputValue: phoneInput } = usePhoneInput({
defaultCountry:
@@ -61,7 +61,7 @@ export default function Form({ user }: EditFormProps) {
email: user.email,
language: user.language,
phoneNumber: phoneInput,
currentPassword: "",
password: "",
newPassword: "",
retypeNewPassword: "",
},
@@ -70,44 +70,77 @@ export default function Form({ user }: EditFormProps) {
reValidateMode: "onChange",
})
async function handleSubmit(data: EditProfileSchema) {
const response = await editProfile(data)
switch (response.status) {
case Status.error:
if (response.issues?.length) {
response.issues.forEach((issue) => {
console.error(issue)
})
}
toast.error(
intl.formatMessage({
id: "An error occured when trying to update profile.",
})
)
break
case Status.success:
toast.success(
intl.formatMessage({ id: "Successfully updated profile!" })
)
/**
* TODO: Toaster?
* Design only has toasters for credit cards
* and membership cards
*/
router.push(profile[lang])
break
}
}
useEffect(() => {
setIsValid(methods.formState.isValid)
}, [setIsValid, methods.formState.isValid])
return (
<section className={styles.container}>
<hgroup className={styles.title}>
<Title as="h4" color="red" level="h1" textTransform="capitalize">
{formatMessage({ id: "Welcome" })}
{intl.formatMessage({ id: "Welcome" })}
</Title>
<Title as="h4" color="burgundy" level="h2" textTransform="capitalize">
{user.name}
</Title>
</hgroup>
<div className={styles.btnContainer}>
<Button
asChild
intent="secondary"
size="small"
theme="base"
className={styles.btn}
>
<Button asChild intent="secondary" size="small" theme="base">
<Link href={profile[lang]}>
{formatMessage({ id: "Discard changes" })}
{intl.formatMessage({ id: "Discard changes" })}
</Link>
</Button>
<Button
disabled={
!methods.formState.isValid || methods.formState.isSubmitting
}
className={styles.btn}
disabled={!isValid || methods.formState.isSubmitting}
form={formId}
intent="primary"
size="small"
theme="base"
type="submit"
>
{formatMessage({ id: "Save" })}
{intl.formatMessage({ id: "Save" })}
</Button>
</div>
<form action={formAction} className={styles.form} id={formId}>
<form
/**
* Ignoring since ts doesn't recognize that tRPC
* parses FormData before reaching the route
* @ts-ignore */
action={editProfile}
className={styles.form}
id={formId}
onSubmit={methods.handleSubmit(handleSubmit)}
>
<FormProvider {...methods}>
<FormContent />
</FormProvider>

View File

@@ -25,12 +25,12 @@ export const editProfileSchema = z
"Please enter a valid phone number"
),
currentPassword: z.string().optional(),
password: z.string().optional(),
newPassword: z.string().optional(),
retypeNewPassword: z.string().optional(),
})
.superRefine((data, ctx) => {
if (data.currentPassword) {
if (data.password) {
if (!data.newPassword) {
ctx.addIssue({
code: "custom",
@@ -50,7 +50,7 @@ export const editProfileSchema = z
ctx.addIssue({
code: "custom",
message: "Current password is required",
path: ["currentPassword"],
path: ["password"],
})
}
}

View File

@@ -58,12 +58,14 @@ a.default {
align-items: center;
display: flex;
gap: var(--Spacing-x1);
justify-content: center;
}
.icon {
display: flex;
align-items: center;
display: flex;
gap: var(--Spacing-x-half);
justify-content: center;
}
/* SIZES */
@@ -798,4 +800,4 @@ a.default {
.icon.tertiaryLightSecondary:disabled svg,
.icon.tertiaryLightSecondary:disabled svg * {
fill: var(--Tertiary-Light-Button-Secondary-On-Fill-Disabled);
}
}

View File

@@ -17,7 +17,6 @@ import type { Key } from "react-aria-components"
import type { DateProps } from "./date"
/** TODO: Get selecting with Enter-key to work */
export default function DateSelect({ name, registerOptions = {} }: DateProps) {
const { formatMessage } = useIntl()
const d = useWatch({ name })
@@ -54,6 +53,18 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
const monthLabel = formatMessage({ id: "Month" })
const yearLabel = formatMessage({ id: "Year" })
let dateValue = null
try {
/**
* parseDate throws when its not a valid
* date, but we can't check isNan since
* we recieve the date as "1999-01-01"
*/
dateValue = parseDate(d)
} catch (error) {
console.error(error)
}
return (
<DatePicker
aria-label={formatMessage({ id: "Select date of birth" })}
@@ -61,7 +72,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
isRequired={!!registerOptions.required}
name={name}
ref={field.ref}
value={isNaN(d) ? undefined : parseDate(d)}
value={dateValue}
>
<Group>
<DateInput className={styles.container}>

View File

@@ -32,8 +32,8 @@ const config = {
defaultVariants: {
color: "burgundy",
textAlign: "left",
type: "h1",
textTransform: "uppercase",
type: "h1",
},
} as const

View File

@@ -1,10 +1,10 @@
export enum Lang {
en = "en",
sv = "sv",
no = "no",
fi = "fi",
da = "da",
de = "de",
en = "en",
fi = "fi",
no = "no",
sv = "sv",
}
export const languages: Record<Lang, string> = {
@@ -56,11 +56,21 @@ export const localeToLang: Record<string, Lang> = {
"se-NO": Lang.no,
} as const
export enum ApiLang {
Da = "Da",
De = "De",
En = "En",
Fi = "Fi",
No = "No",
Sv = "Sv",
Unknown = "Unknown",
}
export const languageSelect = [
{ label: "Danish", value: "Da" },
{ label: "German", value: "De" },
{ label: "English", value: "En" },
{ label: "Finnish", value: "Fi" },
{ label: "Norwegian", value: "No" },
{ label: "Swedish", value: "Sv" },
{ label: "Danish", value: ApiLang.Da },
{ label: "German", value: ApiLang.De },
{ label: "English", value: ApiLang.En },
{ label: "Finnish", value: ApiLang.Fi },
{ label: "Norwegian", value: ApiLang.No },
{ label: "Swedish", value: ApiLang.Sv },
]

View File

@@ -9,6 +9,7 @@
"Amenities": "Faciliteter",
"An error occurred when adding a credit card, please try again later.": "Der opstod en fejl under tilføjelse af et kreditkort. Prøv venligst igen senere.",
"Are you sure you want to remove the card ending with": "Er du sikker på, at du vil fjerne kortet, der slutter med",
"An error occurred when trying to update profile.": "Der opstod en fejl under forsøg på at opdatere profilen.",
"Arrival date": "Ankomstdato",
"as of today": "fra idag",
"As our": "Som vores",
@@ -133,6 +134,7 @@
"Something went wrong and we couldn't add your card. Please try again later.": "Noget gik galt, og vi kunne ikke tilføje dit kort. Prøv venligst igen senere.",
"Something went wrong and we couldn't remove your card. Please try again later.": "Noget gik galt, og vi kunne ikke fjerne dit kort. Prøv venligst igen senere.",
"Street": "Gade",
"Successfully updated profile!": "Profilen er opdateret med succes!",
"special character": "speciel karakter",
"Total Points": "Samlet antal point",
"Your points to spend": "Dine brugbare pointer",
@@ -185,4 +187,4 @@
"Use bonus cheque": "Brug bonuscheck",
"Book reward night": "Book belønningsaften",
"Find hotels": "Find hoteller"
}
}

View File

@@ -8,6 +8,7 @@
"Amenities": "Annehmlichkeiten",
"An error occurred when adding a credit card, please try again later.": "Beim Hinzufügen einer Kreditkarte ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.",
"Are you sure you want to remove the card ending with": "Möchten Sie die Karte mit der Endung",
"An error occurred when trying to update profile.": "Beim Versuch, das Profil zu aktualisieren, ist ein Fehler aufgetreten.",
"Arrival date": "Ankunftsdatum",
"as of today": "Stand heute",
"As our": "Als unser",
@@ -128,6 +129,7 @@
"Something went wrong and we couldn't add your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht hinzufügen. Bitte versuchen Sie es später erneut.",
"Something went wrong and we couldn't remove your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht entfernen. Bitte versuchen Sie es später noch einmal.",
"Street": "Straße",
"Successfully updated profile!": "Profil erfolgreich aktualisiert!",
"special character": "sonderzeichen",
"Total Points": "Gesamtpunktzahl",
"Your points to spend": "Meine Punkte",
@@ -179,4 +181,4 @@
"Use bonus cheque": "Bonusscheck nutzen",
"Book reward night": "Bonusnacht buchen",
"Find hotels": "Hotels finden"
}
}

View File

@@ -9,6 +9,7 @@
"Amenities": "Amenities",
"An error occurred when adding a credit card, please try again later.": "An error occurred when adding a credit card, please try again later.",
"Are you sure you want to remove the card ending with": "Are you sure you want to remove the card ending with",
"An error occurred when trying to update profile.": "An error occurred when trying to update profile.",
"Arrival date": "Arrival date",
"as of today": "as of today",
"As our": "As our",
@@ -139,6 +140,7 @@
"Something went wrong and we couldn't add your card. Please try again later.": "Something went wrong and we couldn't add your card. Please try again later.",
"Something went wrong and we couldn't remove your card. Please try again later.": "Something went wrong and we couldn't remove your card. Please try again later.",
"Street": "Street",
"Successfully updated profile!": "Successfully updated profile!",
"special character": "special character",
"Total Points": "Total Points",
"Your points to spend": "Your points to spend",
@@ -150,9 +152,9 @@
"TUI Points": "TUI Points",
"User information": "User information",
"uppercase letter": "uppercase letter",
"Welcome": "Welcome",
"Visiting address": "Visiting address",
"We could not add a card right now, please try again later.": "We could not add a card right now, please try again later.",
"Welcome": "Welcome",
"Welcome to": "Welcome to",
"Wellness & Exercise": "Wellness & Exercise",
"Where should you go next?": "Where should you go next?",
@@ -191,4 +193,4 @@
"Use bonus cheque": "Use bonus cheque",
"Book reward night": "Book reward night",
"Find hotels": "Find hotels"
}
}

View File

@@ -9,6 +9,7 @@
"Amenities": "Mukavuudet",
"An error occurred when adding a credit card, please try again later.": "Luottokorttia lisättäessä tapahtui virhe. Yritä myöhemmin uudelleen.",
"Are you sure you want to remove the card ending with": "Haluatko varmasti poistaa kortin, joka päättyy numeroon",
"An error occurred when trying to update profile.": "Profiilia päivitettäessä tapahtui virhe.",
"Arrival date": "Saapumispäivä",
"as of today": "tästä päivästä lähtien",
"As our": "Kuin meidän",
@@ -133,6 +134,7 @@
"Something went wrong and we couldn't add your card. Please try again later.": "Jotain meni pieleen, emmekä voineet lisätä korttiasi. Yritä myöhemmin uudelleen.",
"Something went wrong and we couldn't remove your card. Please try again later.": "Jotain meni pieleen, emmekä voineet poistaa korttiasi. Yritä myöhemmin uudelleen.",
"Street": "Katu",
"Successfully updated profile!": "Profiilin päivitys onnistui!",
"special character": "erikoishahmo",
"Total Points": "Kokonaispisteet",
"Your points to spend": "Sinun pisteesi käytettäväksi",
@@ -185,4 +187,4 @@
"Use bonus cheque": "Käytä bonussekkiä",
"Book reward night": "Kirjapalkinto-ilta",
"Find hotels": "Etsi hotelleja"
}
}

View File

@@ -9,6 +9,7 @@
"Amenities": "Fasiliteter",
"An error occurred when adding a credit card, please try again later.": "Det oppstod en feil ved å legge til et kredittkort. Prøv igjen senere.",
"Are you sure you want to remove the card ending with": "Er du sikker på at du vil fjerne kortet som slutter på",
"An error occurred when trying to update profile.": "Det oppstod en feil under forsøk på å oppdatere profilen.",
"Arrival date": "Ankomstdato",
"as of today": "per idag",
"As our": "Som vår",
@@ -133,6 +134,7 @@
"Something went wrong and we couldn't add your card. Please try again later.": "Noe gikk galt, og vi kunne ikke legge til kortet ditt. Prøv igjen senere.",
"Something went wrong and we couldn't remove your card. Please try again later.": "Noe gikk galt, og vi kunne ikke fjerne kortet ditt. Vennligst prøv igjen senere.",
"Street": "Gate",
"Successfully updated profile!": "Vellykket oppdatert profil!",
"special character": "spesiell karakter",
"Total Points": "Totale poeng",
"Your points to spend": "Dine brukbare poeng",
@@ -185,4 +187,4 @@
"Use bonus cheque": "Bruk bonussjekk",
"Book reward night": "Bestill belønningskveld",
"Find hotels": "Finn hotell"
}
}

View File

@@ -9,6 +9,7 @@
"Amenities": "Bekvämligheter",
"An error occurred when adding a credit card, please try again later.": "Ett fel uppstod när ett kreditkort lades till, försök igen senare.",
"Are you sure you want to remove the card ending with": "Är du säker på att du vill ta bort kortet som slutar med",
"An error occurred when trying to update profile.": "Ett fel uppstod när du försökte uppdatera profilen.",
"Arrival date": "Ankomstdatum",
"as of today": "från och med idag",
"As our": "Som vår",
@@ -136,6 +137,7 @@
"Something went wrong and we couldn't add your card. Please try again later.": "Något gick fel och vi kunde inte lägga till ditt kort. Försök igen senare.",
"Something went wrong and we couldn't remove your card. Please try again later.": "Något gick fel och vi kunde inte ta bort ditt kort. Försök igen senare.",
"Street": "Gata",
"Successfully updated profile!": "Profilen har uppdaterats framgångsrikt!",
"special character": "speciell karaktär",
"Total Points": "Poäng totalt",
"Your points to spend": "Dina spenderbara poäng",
@@ -187,4 +189,4 @@
"Use bonus cheque": "Use bonus cheque",
"Book reward night": "Book reward night",
"Find hotels": "Hitta hotell"
}
}

View File

@@ -14,6 +14,7 @@ export { endpoints } from "./endpoints"
const defaultOptions: RequestInit = {
cache: "no-store",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
mode: "cors",
@@ -25,25 +26,40 @@ const fetch = fetchRetry(global.fetch, {
return Math.pow(2, attempt) * 150 // 150, 300, 600
},
})
const url = new URL(env.API_BASEURL)
export async function get(
endpoint: Endpoint | `${Endpoint}/${string}`,
options: RequestOptionsWithOutBody,
params?: URLSearchParams
params = {}
) {
const url = new URL(
`${env.API_BASEURL}/${endpoint}${params ? `?${params.toString()}` : ""}`
)
url.pathname = endpoint
const searchParams = new URLSearchParams(params)
if (searchParams.size) {
searchParams.forEach((value, key) => {
url.searchParams.set(key, value)
})
url.searchParams.sort()
}
return fetch(url, merge.all([defaultOptions, { method: "GET" }, options]))
}
export async function patch(
endpoint: Endpoint | `${Endpoint}/${string}`,
options: RequestOptionsWithJSONBody
options: RequestOptionsWithJSONBody,
params = {}
) {
const { body, ...requestOptions } = options
url.pathname = endpoint
const searchParams = new URLSearchParams(params)
if (searchParams.size) {
searchParams.forEach((value, key) => {
url.searchParams.set(key, value)
})
url.searchParams.sort()
}
return fetch(
`${env.API_BASEURL}/${endpoint}`,
url,
merge.all([
defaultOptions,
{ body: JSON.stringify(body), method: "PATCH" },
@@ -55,11 +71,19 @@ export async function patch(
export async function post(
endpoint: Endpoint | `${Endpoint}/${string}`,
options: RequestOptionsWithJSONBody,
params?: URLSearchParams
params = {},
) {
const { body, ...requestOptions } = options
url.pathname = endpoint
const searchParams = new URLSearchParams(params)
if (searchParams.size) {
searchParams.forEach((value, key) => {
url.searchParams.set(key, value)
})
url.searchParams.sort()
}
return fetch(
`${env.API_BASEURL}/${endpoint}${params ? `?${params.toString()}` : ""}`,
url,
merge.all([
defaultOptions,
{ body: JSON.stringify(body), method: "POST" },
@@ -71,10 +95,15 @@ export async function post(
export async function remove(
endpoint: Endpoint | `${Endpoint}/${string}`,
options: RequestOptionsWithOutBody,
params?: URLSearchParams
params = {},
) {
return fetch(
`${env.API_BASEURL}/${endpoint}${params ? `?${params.toString()}` : ""}`,
merge.all([defaultOptions, { method: "DELETE" }, options])
)
url.pathname = endpoint
const searchParams = new URLSearchParams(params)
if (searchParams.size) {
searchParams.forEach((value, key) => {
url.searchParams.set(key, value)
})
url.searchParams.sort()
}
return fetch(url, merge.all([defaultOptions, { method: "DELETE" }, options]))
}

View File

@@ -4,6 +4,7 @@ import "dayjs/locale/fi"
import "dayjs/locale/sv"
import d from "dayjs"
import advancedFormat from "dayjs/plugin/advancedFormat"
import isToday from "dayjs/plugin/isToday"
import relativeTime from "dayjs/plugin/relativeTime"
import utc from "dayjs/plugin/utc"
@@ -55,6 +56,7 @@ d.locale("no", {
/**
* If more plugins are needed https://day.js.org/docs/en/plugin/plugin
*/
d.extend(advancedFormat)
d.extend(isToday)
d.extend(relativeTime)
d.extend(utc)

108
package-lock.json generated
View File

@@ -19,9 +19,9 @@
"@scandic-hotels/design-system": "git+https://x-token-auth:$DESIGN_SYSTEM_ACCESS_TOKEN@bitbucket.org/scandic-swap/design-system.git#v0.1.0-rc.8",
"@t3-oss/env-nextjs": "^0.9.2",
"@tanstack/react-query": "^5.28.6",
"@trpc/client": "^11.0.0-next-beta.318",
"@trpc/react-query": "^11.0.0-next-beta.318",
"@trpc/server": "^11.0.0-next-beta.318",
"@trpc/client": "^11.0.0-rc.467",
"@trpc/react-query": "^11.0.0-rc.467",
"@trpc/server": "^11.0.0-rc.467",
"@vercel/otel": "^1.9.1",
"class-variance-authority": "^0.7.0",
"clean-deep": "^3.4.0",
@@ -33,7 +33,7 @@
"graphql-tag": "^2.12.6",
"libphonenumber-js": "^1.10.60",
"next": "^14.2.3",
"next-auth": "^5.0.0-beta.15",
"next-auth": "^5.0.0-beta.19",
"react": "^18",
"react-dom": "^18",
"react-feather": "^2.0.10",
@@ -106,15 +106,16 @@
}
},
"node_modules/@auth/core": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@auth/core/-/core-0.28.1.tgz",
"integrity": "sha512-gvp74mypYZADpTlfGRp6HE0G3pIHWvtJpy+KZ+8FvY0cmlIpHog+jdMOdd29dQtLtN25kF2YbfHsesCFuGUQbg==",
"version": "0.32.0",
"resolved": "https://registry.npmjs.org/@auth/core/-/core-0.32.0.tgz",
"integrity": "sha512-3+ssTScBd+1fd0/fscAyQN1tSygXzuhysuVVzB942ggU4mdfiTbv36P0ccVnExKWYJKvu3E2r3/zxXCCAmTOrg==",
"license": "ISC",
"dependencies": {
"@panva/hkdf": "^1.1.1",
"@types/cookie": "0.6.0",
"cookie": "0.6.0",
"jose": "^5.1.3",
"oauth4webapi": "^2.4.0",
"oauth4webapi": "^2.9.0",
"preact": "10.11.3",
"preact-render-to-string": "5.2.3"
},
@@ -3676,9 +3677,10 @@
}
},
"node_modules/@panva/hkdf": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.1.1.tgz",
"integrity": "sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
"integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
@@ -5641,20 +5643,22 @@
}
},
"node_modules/@tanstack/query-core": {
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.29.0.tgz",
"integrity": "sha512-WgPTRs58hm9CMzEr5jpISe8HXa3qKQ8CxewdYZeVnA54JrPY9B1CZiwsCoLpLkf0dGRZq+LcX5OiJb0bEsOFww==",
"version": "5.51.9",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.9.tgz",
"integrity": "sha512-HsAwaY5J19MD18ykZDS3aVVh+bAt0i7m6uQlFC2b77DLV9djo+xEN7MWQAQQTR8IM+7r/zbozTQ7P0xr0bHuew==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.29.2",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.29.2.tgz",
"integrity": "sha512-nyuWILR4u7H5moLGSiifLh8kIqQDLNOHGuSz0rcp+J75fNc8aQLyr5+I2JCHU3n+nJrTTW1ssgAD8HiKD7IFBQ==",
"version": "5.51.11",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.51.11.tgz",
"integrity": "sha512-4Kq2x0XpDlpvSnaLG+8pHNH60zEc3mBvb3B2tOMDjcPCi/o+Du3p/9qpPLwJOTliVxxPJAP27fuIhLrsRdCr7A==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.29.0"
"@tanstack/query-core": "5.51.9"
},
"funding": {
"type": "github",
@@ -5838,38 +5842,41 @@
}
},
"node_modules/@trpc/client": {
"version": "11.0.0-rc.334",
"resolved": "https://registry.npmjs.org/@trpc/client/-/client-11.0.0-rc.334.tgz",
"integrity": "sha512-nVKOxCfhKxBXvs1MwjfNNsWHQH/rHWzS4SxYPtYld7zqANTfNqn8cuClHMKlkeMKljObFiCfWhbdrBamKO/dDw==",
"version": "11.0.0-rc.467",
"resolved": "https://registry.npmjs.org/@trpc/client/-/client-11.0.0-rc.467.tgz",
"integrity": "sha512-ovZaGdAUl+EEmtJJc5uuo95B0gw8+q3jwNjUQQmmSMU5Isq4sYdjIWNkhbrFtR8CovllFyrRrjAgCWdaOTEY4g==",
"funding": [
"https://trpc.io/sponsor"
],
"license": "MIT",
"peerDependencies": {
"@trpc/server": "11.0.0-rc.334+fdf26e552"
"@trpc/server": "11.0.0-rc.467+8f72171d6"
}
},
"node_modules/@trpc/react-query": {
"version": "11.0.0-rc.334",
"resolved": "https://registry.npmjs.org/@trpc/react-query/-/react-query-11.0.0-rc.334.tgz",
"integrity": "sha512-DvhU7qMfQkLyrhQoDlrtMAGIgJyin9I8WCsF0oGlAD95ygoJcviD8D+lQzSCUttul9rPcNqWDuGhm5laS7V43A==",
"version": "11.0.0-rc.467",
"resolved": "https://registry.npmjs.org/@trpc/react-query/-/react-query-11.0.0-rc.467.tgz",
"integrity": "sha512-PNpHgISXJ60s0fJc6JUomKe3iu1wj6pZNFHJgQecAEK0gs1y6VM8Oh8CHgZg8+J/KDP/UtUmBcbpFP9l8Nq48w==",
"funding": [
"https://trpc.io/sponsor"
],
"license": "MIT",
"peerDependencies": {
"@tanstack/react-query": "^5.25.0",
"@trpc/client": "11.0.0-rc.334+fdf26e552",
"@trpc/server": "11.0.0-rc.334+fdf26e552",
"@tanstack/react-query": "^5.49.2",
"@trpc/client": "11.0.0-rc.467+8f72171d6",
"@trpc/server": "11.0.0-rc.467+8f72171d6",
"react": ">=18.2.0",
"react-dom": ">=18.2.0"
}
},
"node_modules/@trpc/server": {
"version": "11.0.0-rc.334",
"resolved": "https://registry.npmjs.org/@trpc/server/-/server-11.0.0-rc.334.tgz",
"integrity": "sha512-ckg+f4z3Lc0wYvm1Cx8Zjz8b2hguNlUFQeF8vLrtWYBL8HEolshmxOHMG9MLc6WDE6T1R7DZrG+xEwjI44XH9w==",
"version": "11.0.0-rc.467",
"resolved": "https://registry.npmjs.org/@trpc/server/-/server-11.0.0-rc.467.tgz",
"integrity": "sha512-94Gv26ALuBfxgFlSGV3x2uF2ixUEViuK0m3IPKOvCTMreisZkBqyTa3NkBcuPZW/AMUieM5P4Q2NrbHTIA0fKQ==",
"funding": [
"https://trpc.io/sponsor"
]
],
"license": "MIT"
},
"node_modules/@trysound/sax": {
"version": "0.2.0",
@@ -5968,7 +5975,8 @@
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"license": "MIT"
},
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
@@ -7960,6 +7968,7 @@
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -12346,9 +12355,10 @@
}
},
"node_modules/jose": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/jose/-/jose-5.2.4.tgz",
"integrity": "sha512-6ScbIk2WWCeXkmzF6bRPmEuaqy1m8SbsRFMa/FLrSCkGIhj8OLVG/IH+XHVmNMx/KUo8cVWEE6oKR4dJ+S0Rkg==",
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/jose/-/jose-5.6.3.tgz",
"integrity": "sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
@@ -14366,18 +14376,19 @@
}
},
"node_modules/next-auth": {
"version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.16.tgz",
"integrity": "sha512-dX2snB+ezN23tFzSes3n3uosT9iBf0eILPYWH/R2fd9n3ZzdMQlRzq7JIOPeS1aLc84IuRlyuyXyx9XmmZB6og==",
"version": "5.0.0-beta.19",
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.19.tgz",
"integrity": "sha512-YHu1igcAxZPh8ZB7GIM93dqgY6gcAzq66FOhQFheAdOx1raxNcApt05nNyNCSB6NegSiyJ4XOPsaNow4pfDmsg==",
"license": "ISC",
"dependencies": {
"@auth/core": "0.28.1"
"@auth/core": "0.32.0"
},
"peerDependencies": {
"@simplewebauthn/browser": "^9.0.1",
"@simplewebauthn/server": "^9.0.2",
"next": "^14",
"next": "^14 || ^15.0.0-0",
"nodemailer": "^6.6.5",
"react": "^18.2.0"
"react": "^18.2.0 || ^19.0.0-0"
},
"peerDependenciesMeta": {
"@simplewebauthn/browser": {
@@ -14508,9 +14519,10 @@
"dev": true
},
"node_modules/oauth4webapi": {
"version": "2.10.4",
"resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.10.4.tgz",
"integrity": "sha512-DSoj8QoChzOCQlJkRmYxAJCIpnXFW32R0Uq7avyghIeB6iJq0XAblOD7pcq3mx4WEBDwMuKr0Y1qveCBleG2Xw==",
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.11.1.tgz",
"integrity": "sha512-aNzOnL98bL6izG97zgnZs1PFEyO4WDVRhz2Pd066NPak44w5ESLRCYmJIyey8avSBPOMtBjhF3ZDDm7bIb7UOg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
@@ -15235,6 +15247,7 @@
"version": "10.11.3",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz",
"integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
@@ -15244,6 +15257,7 @@
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz",
"integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==",
"license": "MIT",
"dependencies": {
"pretty-format": "^3.8.0"
},
@@ -15290,7 +15304,8 @@
"node_modules/pretty-format": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
"license": "MIT"
},
"node_modules/process": {
"version": "0.11.10",
@@ -16543,6 +16558,7 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-1.5.0.tgz",
"integrity": "sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"

View File

@@ -35,9 +35,9 @@
"@scandic-hotels/design-system": "git+https://x-token-auth:$DESIGN_SYSTEM_ACCESS_TOKEN@bitbucket.org/scandic-swap/design-system.git#v0.1.0-rc.8",
"@t3-oss/env-nextjs": "^0.9.2",
"@tanstack/react-query": "^5.28.6",
"@trpc/client": "^11.0.0-next-beta.318",
"@trpc/react-query": "^11.0.0-next-beta.318",
"@trpc/server": "^11.0.0-next-beta.318",
"@trpc/client": "^11.0.0-rc.467",
"@trpc/react-query": "^11.0.0-rc.467",
"@trpc/server": "^11.0.0-rc.467",
"@vercel/otel": "^1.9.1",
"class-variance-authority": "^0.7.0",
"clean-deep": "^3.4.0",
@@ -49,7 +49,7 @@
"graphql-tag": "^2.12.6",
"libphonenumber-js": "^1.10.60",
"next": "^14.2.3",
"next-auth": "^5.0.0-beta.15",
"next-auth": "^5.0.0-beta.19",
"react": "^18",
"react-dom": "^18",
"react-feather": "^2.0.10",

View File

@@ -5,8 +5,6 @@ import { Lang } from "@/constants/languages"
import { auth } from "@/auth"
import { unauthorizedError } from "./errors/trpc"
typeof auth
type CreateContextOptions = {

View File

@@ -49,3 +49,13 @@ export function sessionExpiredError() {
cause: new SessionExpiredError(SESSION_EXPIRED),
})
}
export const PUBLIC_UNAUTHORIZED = "PUBLIC_UNAUTHORIZED"
export class PublicUnauthorizedError extends Error {}
export function publicUnauthorizedError() {
return new TRPCError({
code: "UNAUTHORIZED",
message: PUBLIC_UNAUTHORIZED,
cause: new PublicUnauthorizedError(PUBLIC_UNAUTHORIZED),
})
}

View File

@@ -6,8 +6,8 @@ import { router } from "./trpc"
export const appRouter = router({
contentstack: contentstackRouter,
user: userRouter,
hotel: hotelsRouter,
user: userRouter,
})
export type AppRouter = typeof appRouter

View File

@@ -20,16 +20,16 @@ import tempRatesData from "./tempRatesData.json"
export const hotelQueryRouter = router({
getHotel: serviceProcedure
.input(getHotelInputSchema)
.query(async ({ input, ctx }) => {
.query(async ({ ctx, input }) => {
const { hotelId, language, include } = input
const params = new URLSearchParams()
const apiLang = toApiLang(language)
params.set("language", apiLang)
const params: Record<string, string> = {
hotelId,
language: apiLang,
}
if (include) {
params.set("include", include.join(","))
params.include = include.join(",")
}
const apiResponse = await api.get(
@@ -61,16 +61,16 @@ export const hotelQueryRouter = router({
const roomCategories = included
? included
.filter((item) => item.type === "roomcategories")
.map((roomCategory) => {
const validatedRoom = roomSchema.safeParse(roomCategory)
if (!validatedRoom.success) {
console.error(`Get Room Category Data - Verified Data Error`)
console.error(validatedRoom.error)
throw badRequestError()
}
return validatedRoom.data
})
.filter((item) => item.type === "roomcategories")
.map((roomCategory) => {
const validatedRoom = roomSchema.safeParse(roomCategory)
if (!validatedRoom.success) {
console.error(`Get Room Category Data - Verified Data Error`)
console.error(validatedRoom.error)
throw badRequestError()
}
return validatedRoom.data
})
: []
return {

View File

@@ -1,7 +1,6 @@
import { z } from "zod"
import { Lang } from "@/constants/languages"
// Query
export const getUserInputSchema = z
.object({
mask: z.boolean().default(true),
@@ -10,17 +9,27 @@ export const getUserInputSchema = z
export const staysInput = z
.object({
cursor: z.number().optional(),
limit: z.number().min(0).default(6),
cursor: z
.number()
.optional()
.transform((num) => (num ? String(num) : undefined)),
limit: z
.number()
.min(0)
.default(6)
.transform((num) => String(num)),
})
.default({})
export const soonestUpcomingStaysInput = z
export const friendTransactionsInput = z
.object({
limit: z.number().int().positive(),
page: z.number().int().positive(),
})
.default({ limit: 3 })
.default({ limit: 5, page: 1 })
// Mutation
export const addCreditCardInput = z.object({
language: z.string(),
})
@@ -33,9 +42,3 @@ export const saveCreditCardInput = z.object({
transactionId: z.string(),
merchantId: z.string().optional(),
})
export const friendTransactionsInput = z
.object({
limit: z.number().int().positive(),
page: z.number().int().positive(),
})
.default({ limit: 5, page: 1 })

View File

@@ -79,6 +79,7 @@ async function getVerifiedUser({ session }: { session: Session }) {
console.error(verifiedData.error)
return null
}
return verifiedData
}
@@ -335,12 +336,9 @@ export const userQueryRouter = router({
.input(staysInput)
.query(async ({ ctx, input }) => {
const { limit, cursor } = input
const params = new URLSearchParams()
params.set("limit", limit.toString())
const params: Record<string, string> = { limit }
if (cursor) {
params.set("offset", cursor.toString())
params.offset = cursor
}
const apiResponse = await api.get(
@@ -403,11 +401,9 @@ export const userQueryRouter = router({
.query(async ({ ctx, input }) => {
const { limit, cursor } = input
const params = new URLSearchParams()
params.set("limit", limit.toString())
const params: Record<string, string> = { limit }
if (cursor) {
params.set("offset", cursor.toString())
params.offset = cursor
}
const apiResponse = await api.get(

View File

@@ -1,4 +1,6 @@
import { initTRPC } from "@trpc/server"
import { experimental_nextAppDirCaller } from "@trpc/server/adapters/next-app-dir"
import { ZodError } from "zod"
import { env } from "@/env/server"
@@ -9,15 +11,30 @@ import {
unauthorizedError,
} from "./errors/trpc"
import { fetchServiceToken } from "./tokenManager"
import { type Context, createContext } from "./context"
import { transformer } from "./transformer"
import { langInput } from "./utils"
import type { Session } from "next-auth"
import type { Meta } from "@/types/trpc/meta"
import type { Context } from "./context"
const t = initTRPC.context<Context>().meta<Meta>().create({ transformer })
const t = initTRPC
.context<Context>()
.meta<Meta>()
.create({
transformer,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
}
},
})
export const { createCallerFactory, mergeRouters, router } = t
export const publicProcedure = t.procedure
@@ -113,3 +130,30 @@ export const serviceProcedure = t.procedure.use(async (opts) => {
},
})
})
export const serverActionProcedure = t.procedure.experimental_caller(
experimental_nextAppDirCaller({
createContext,
normalizeFormData: true,
})
)
export const protectedServerActionProcedure = serverActionProcedure.use(
async (opts) => {
const session = await opts.ctx.auth()
if (!session) {
throw unauthorizedError()
}
if (session && session.error === "RefreshAccessTokenError") {
throw sessionExpiredError()
}
return opts.next({
ctx: {
...opts.ctx,
session,
},
})
}
)

View File

@@ -4,25 +4,31 @@ export type EditFormProps = {
user: User
}
type E = {
message: string
path: string
}
export const enum Status {
error = "error",
success = "success",
}
type ErrorState = {
errors?: E[]
type Data = Record<
string,
string | undefined | Record<string, string | undefined>
>
type Issue = {
field: string
message: string
status: Status.error
}
type SuccessState = {
export type State = {
data: Data
message: string
status: Status.success
}
export type State = ErrorState | SuccessState | null
} & (
| {
issues: never
status: Status.success
}
| {
issues: Issue[]
status: Status.error
}
)

4
types/jwt.d.ts vendored
View File

@@ -11,9 +11,9 @@ declare module "next-auth/jwt" {
interface JWT extends DefaultJWT, RefreshTokenError {
access_token: string
expires_at?: number
refresh_token: string
loginType: LoginType
mfa_scope: boolean
mfa_expires_at: number
mfa_scope: boolean
refresh_token: string
}
}