diff --git a/actions/editProfile.ts b/actions/editProfile.ts index 494609a82..b09e852fb 100644 --- a/actions/editProfile.ts +++ b/actions/editProfile.ts @@ -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 = 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, } - } -} + }) diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.module.css b/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.module.css index 9f19f6eac..97be74fd6 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.module.css +++ b/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.module.css @@ -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); } -} +} \ No newline at end of file diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/profileLayout.css b/app/[lang]/(live)/(protected)/my-pages/profile/profileLayout.css index 891598b37..f3549d0d2 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/profileLayout.css +++ b/app/[lang]/(live)/(protected)/my-pages/profile/profileLayout.css @@ -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 { diff --git a/auth.ts b/auth.ts index 29d470f4d..0de343626 100644 --- a/auth.ts +++ b/auth.ts @@ -233,4 +233,4 @@ export const { auth, signIn, signOut, -} = NextAuth(config) +} = NextAuth(config) \ No newline at end of file diff --git a/components/Forms/Edit/Profile/FormContent/formContent.module.css b/components/Forms/Edit/Profile/FormContent/formContent.module.css index 3400070a0..64eb85410 100644 --- a/components/Forms/Edit/Profile/FormContent/formContent.module.css +++ b/components/Forms/Edit/Profile/FormContent/formContent.module.css @@ -10,3 +10,9 @@ gap: var(--Spacing-x2); grid-template-columns: max(164px) 1fr; } + +@media (min-width: 768px) { + .divider { + display: none; + } +} diff --git a/components/Forms/Edit/Profile/FormContent/index.tsx b/components/Forms/Edit/Profile/FormContent/index.tsx index 40f9cbdbd..a4c86f225 100644 --- a/components/Forms/Edit/Profile/FormContent/index.tsx +++ b/components/Forms/Edit/Profile/FormContent/index.tsx @@ -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 ( <>
- {formatMessage({ id: "User information" })} + {intl.formatMessage({ id: "User information" })}
@@ -59,16 +60,19 @@ export default function FormContent() { + ( - 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 (
- {formatMessage({ id: "Welcome" })} + {intl.formatMessage({ id: "Welcome" })} {user.name}
-
-
+ diff --git a/components/Forms/Edit/Profile/schema.ts b/components/Forms/Edit/Profile/schema.ts index 1460c78ac..7330214ff 100644 --- a/components/Forms/Edit/Profile/schema.ts +++ b/components/Forms/Edit/Profile/schema.ts @@ -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"], }) } } diff --git a/components/TempDesignSystem/Button/button.module.css b/components/TempDesignSystem/Button/button.module.css index 9dc82ae2d..1079b6e54 100644 --- a/components/TempDesignSystem/Button/button.module.css +++ b/components/TempDesignSystem/Button/button.module.css @@ -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); -} +} \ No newline at end of file diff --git a/components/TempDesignSystem/Form/Date/index.tsx b/components/TempDesignSystem/Form/Date/index.tsx index a04ec82bf..800adcb59 100644 --- a/components/TempDesignSystem/Form/Date/index.tsx +++ b/components/TempDesignSystem/Form/Date/index.tsx @@ -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 ( diff --git a/components/TempDesignSystem/Text/Title/variants.ts b/components/TempDesignSystem/Text/Title/variants.ts index 27881b4d8..641394ceb 100644 --- a/components/TempDesignSystem/Text/Title/variants.ts +++ b/components/TempDesignSystem/Text/Title/variants.ts @@ -32,8 +32,8 @@ const config = { defaultVariants: { color: "burgundy", textAlign: "left", - type: "h1", textTransform: "uppercase", + type: "h1", }, } as const diff --git a/constants/languages.ts b/constants/languages.ts index 8652df7a6..269ae2686 100644 --- a/constants/languages.ts +++ b/constants/languages.ts @@ -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 = { @@ -56,11 +56,21 @@ export const localeToLang: Record = { "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 }, ] diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 997d4d6a2..d623f5c6f 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -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" -} +} \ No newline at end of file diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index bd9b969f9..ce07af5af 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -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" -} +} \ No newline at end of file diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 93b93b252..9725744f5 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -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" -} +} \ No newline at end of file diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 1748014f0..0f6b9c4b4 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -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" -} +} \ No newline at end of file diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 0d4ff2ce1..5f1a26a89 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -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" -} +} \ No newline at end of file diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 25bab7fc5..e578a59df 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -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" -} +} \ No newline at end of file diff --git a/lib/api/index.ts b/lib/api/index.ts index a3ec13a6c..82a75704c 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -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])) } diff --git a/lib/dt.ts b/lib/dt.ts index 083032505..b90938475 100644 --- a/lib/dt.ts +++ b/lib/dt.ts @@ -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) diff --git a/package-lock.json b/package-lock.json index 95c925618..14135cd53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index 5a3130158..c826f1e30 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/context.ts b/server/context.ts index f96e5cff5..76ea0ba34 100644 --- a/server/context.ts +++ b/server/context.ts @@ -5,8 +5,6 @@ import { Lang } from "@/constants/languages" import { auth } from "@/auth" -import { unauthorizedError } from "./errors/trpc" - typeof auth type CreateContextOptions = { diff --git a/server/errors/trpc.ts b/server/errors/trpc.ts index 0acf19819..e9884d2d9 100644 --- a/server/errors/trpc.ts +++ b/server/errors/trpc.ts @@ -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), + }) +} diff --git a/server/index.ts b/server/index.ts index b349d26cf..9b4f7ca5c 100644 --- a/server/index.ts +++ b/server/index.ts @@ -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 diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 6c2c204cb..d6f400abf 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -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 = { + 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 { diff --git a/server/routers/user/input.ts b/server/routers/user/input.ts index a3dea492e..283f12d5e 100644 --- a/server/routers/user/input.ts +++ b/server/routers/user/input.ts @@ -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 }) diff --git a/server/routers/user/query.ts b/server/routers/user/query.ts index 053bbb5dd..073ef7a04 100644 --- a/server/routers/user/query.ts +++ b/server/routers/user/query.ts @@ -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 = { 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 = { limit } if (cursor) { - params.set("offset", cursor.toString()) + params.offset = cursor } const apiResponse = await api.get( diff --git a/server/trpc.ts b/server/trpc.ts index c2f159dfe..fc4fb70b9 100644 --- a/server/trpc.ts +++ b/server/trpc.ts @@ -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().meta().create({ transformer }) +const t = initTRPC + .context() + .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, + }, + }) + } +) diff --git a/types/components/myPages/myProfile/edit.ts b/types/components/myPages/myProfile/edit.ts index 326f9d8fb..cfb61b658 100644 --- a/types/components/myPages/myProfile/edit.ts +++ b/types/components/myPages/myProfile/edit.ts @@ -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 +> + +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 + } +) diff --git a/types/jwt.d.ts b/types/jwt.d.ts index 251da471d..f0aecc2c1 100644 --- a/types/jwt.d.ts +++ b/types/jwt.d.ts @@ -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 } }