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 32b75c7b8..9f19f6eac 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 @@ -14,17 +14,6 @@ gap: var(--Spacing-x1); } -.card { - display: grid; - align-items: center; - column-gap: var(--Spacing-x1); - grid-template-columns: auto auto auto 1fr; - justify-items: flex-end; - padding: var(--Spacing-x1) var(--Spacing-x-one-and-half,); - border-radius: var(--Corner-radius-Small); - background-color: var(--Base-Background-Primary-Normal); -} - @media screen and (min-width: 768px) { .container { gap: var(--Spacing-x3); diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx index 68a19f188..400f10f2c 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx @@ -1,11 +1,9 @@ import { env } from "@/env/server" import { serverClient } from "@/lib/trpc/server" -import { CreditCard, Delete } from "@/components/Icons" import AddCreditCardButton from "@/components/Profile/AddCreditCardButton" -import Button from "@/components/TempDesignSystem/Button" +import CreditCardList from "@/components/Profile/CreditCardList" import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import { getIntl } from "@/i18n" import { setLang } from "@/i18n/serverContext" @@ -33,38 +31,8 @@ export default async function CreditCardSlot({ params }: PageArgs) { })} - {creditCards?.length ? ( -
- {creditCards.map((card, idx) => ( - - ))} -
- ) : null} + ) } - -function CreditCardRow({ - truncatedNumber, - cardType, -}: { - truncatedNumber: string - cardType: string -}) { - const maskedCardNumber = `**** ${truncatedNumber.slice(12, 16)}` - return ( -
- - {cardType} - {maskedCardNumber} - -
- ) -} diff --git a/app/api/web/add-card-callback/[lang]/route.ts b/app/api/web/add-card-callback/[lang]/route.ts index 17527a66e..871b5dd14 100644 --- a/app/api/web/add-card-callback/[lang]/route.ts +++ b/app/api/web/add-card-callback/[lang]/route.ts @@ -2,46 +2,46 @@ import { NextRequest } from "next/server" import { env } from "process" import { Lang } from "@/constants/languages" +import { profile } from "@/constants/routes/myPages" import { serverClient } from "@/lib/trpc/server" -import { badRequest, internalServerError } from "@/server/errors/next" export async function GET( request: NextRequest, { params }: { params: { lang: string } } ) { - try { - const lang = params.lang as Lang + const lang = params.lang as Lang + const returnUrl = new URL(`${env.PUBLIC_URL}/${profile[lang ?? Lang.en]}`) + try { const searchParams = request.nextUrl.searchParams const success = searchParams.get("success") const failure = searchParams.get("failure") + const cancel = searchParams.get("cancel") const trxId = searchParams.get("datatransTrxId") - const returnUrl = new URL( - `${env.PUBLIC_URL}/${lang ?? Lang.en}/scandic-friends/my-pages/profile` - ) - if (success) { - if (!trxId) { - return badRequest("Missing datatransTrxId param") - } + if (trxId) { + const saveCardSuccess = await serverClient().user.creditCard.save({ + transactionId: trxId, + }) - const saveCardSuccess = await serverClient().user.saveCard({ - transactionId: trxId, - }) - - if (saveCardSuccess) { - returnUrl.searchParams.set("success", "true") + if (saveCardSuccess) { + returnUrl.searchParams.set("success", "true") + } else { + returnUrl.searchParams.set("failure", "true") + } } else { - returnUrl.searchParams.set("failure", "true") + returnUrl.searchParams.set("error", "true") } } else if (failure) { returnUrl.searchParams.set("failure", "true") + } else if (cancel) { + returnUrl.searchParams.set("cancel", "true") } - - return Response.redirect(returnUrl, 307) } catch (error) { - console.error(error) - return internalServerError() + console.error("Error saving credit card", error) + returnUrl.searchParams.set("error", "true") } + + return Response.redirect(returnUrl, 307) } diff --git a/components/Profile/AddCreditCardButton/index.tsx b/components/Profile/AddCreditCardButton/index.tsx index 56081c20d..8bc1ca7f9 100644 --- a/components/Profile/AddCreditCardButton/index.tsx +++ b/components/Profile/AddCreditCardButton/index.tsx @@ -1,47 +1,53 @@ "use client" import { usePathname, useRouter, useSearchParams } from "next/navigation" -import { useEffect } from "react" +import { useEffect, useRef } from "react" import { useIntl } from "react-intl" -import { toast } from "sonner" import { trpc } from "@/lib/trpc/client" import { PlusCircleIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" +import { toast } from "@/components/TempDesignSystem/Toasts" import useLang from "@/hooks/useLang" import styles from "./addCreditCardButton.module.css" -let hasRunOnce = false - function useAddCardResultToast() { + const hasRunOnce = useRef(false) const intl = useIntl() const router = useRouter() const pathname = usePathname() const searchParams = useSearchParams() useEffect(() => { - if (hasRunOnce) return + if (hasRunOnce.current) return const success = searchParams.get("success") const failure = searchParams.get("failure") + const cancel = searchParams.get("cancel") + const error = searchParams.get("error") if (success) { - // setTimeout is used to make sure DOM is loaded before triggering toast. See documentation for more info: https://sonner.emilkowal.ski/toast#render-toast-on-page-load - setTimeout(() => { - toast.success( - intl.formatMessage({ id: "Your card was successfully saved!" }) - ) - }) - } else if (failure) { - setTimeout(() => { - toast.error(intl.formatMessage({ id: "Something went wrong!" })) - }) + toast.success( + intl.formatMessage({ id: "Your card was successfully saved!" }) + ) + } else if (cancel) { + toast.warning( + intl.formatMessage({ + id: "You canceled adding a new credit card.", + }) + ) + } else if (failure || error) { + toast.error( + intl.formatMessage({ + id: "Something went wrong and we couldn't add your card. Please try again later.", + }) + ) } router.replace(pathname) - hasRunOnce = true + hasRunOnce.current = true }, [intl, pathname, router, searchParams]) } @@ -51,10 +57,25 @@ export default function AddCreditCardButton() { const lang = useLang() useAddCardResultToast() - const initiateAddCard = trpc.user.initiateSaveCard.useMutation({ - onSuccess: (result) => (result ? router.push(result.attribute.link) : null), - onError: () => - toast.error(intl.formatMessage({ id: "Something went wrong!" })), + const initiateAddCard = trpc.user.creditCard.add.useMutation({ + onSuccess: (result) => { + if (result?.attribute.link) { + router.push(result.attribute.link) + } else { + toast.error( + intl.formatMessage({ + id: "We could not add a card right now, please try again later.", + }) + ) + } + }, + onError: () => { + toast.error( + intl.formatMessage({ + id: "An error occurred when adding a credit card, please try again later.", + }) + ) + }, }) return ( diff --git a/components/Profile/CreditCardList/CreditCardList.module.css b/components/Profile/CreditCardList/CreditCardList.module.css new file mode 100644 index 000000000..86929755c --- /dev/null +++ b/components/Profile/CreditCardList/CreditCardList.module.css @@ -0,0 +1,4 @@ +.cardContainer { + display: grid; + gap: var(--Spacing-x1); +} diff --git a/components/Profile/CreditCardList/index.tsx b/components/Profile/CreditCardList/index.tsx new file mode 100644 index 000000000..e97e4739b --- /dev/null +++ b/components/Profile/CreditCardList/index.tsx @@ -0,0 +1,31 @@ +"use client" + +import React from "react" + +import { trpc } from "@/lib/trpc/client" + +import CreditCardRow from "../CreditCardRow" + +import styles from "./CreditCardList.module.css" + +import type { CreditCard } from "@/types/user" + +export default function CreditCardList({ + initialData, +}: { + initialData?: CreditCard[] | null +}) { + const creditCards = trpc.user.creditCards.useQuery(undefined, { initialData }) + + if (!creditCards.data || !creditCards.data.length) { + return null + } + + return ( +
+ {creditCards.data.map((card) => ( + + ))} +
+ ) +} diff --git a/components/Profile/CreditCardRow/creditCardRow.module.css b/components/Profile/CreditCardRow/creditCardRow.module.css new file mode 100644 index 000000000..2c326c100 --- /dev/null +++ b/components/Profile/CreditCardRow/creditCardRow.module.css @@ -0,0 +1,10 @@ +.card { + display: grid; + align-items: center; + column-gap: var(--Spacing-x1); + grid-template-columns: auto auto auto 1fr; + justify-items: flex-end; + padding: var(--Spacing-x1) var(--Spacing-x-one-and-half,); + border-radius: var(--Corner-radius-Small); + background-color: var(--Base-Background-Primary-Normal); +} diff --git a/components/Profile/CreditCardRow/index.tsx b/components/Profile/CreditCardRow/index.tsx new file mode 100644 index 000000000..5aa54ab57 --- /dev/null +++ b/components/Profile/CreditCardRow/index.tsx @@ -0,0 +1,22 @@ +import { CreditCard } from "@/components/Icons" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" + +import DeleteCreditCardConfirmation from "../DeleteCreditCardConfirmation" + +import styles from "./creditCardRow.module.css" + +import type { CreditCardRowProps } from "@/types/components/myPages/myProfile/creditCards" + +export default function CreditCardRow({ card }: CreditCardRowProps) { + const maskedCardNumber = `**** ${card.truncatedNumber.slice(-4)}` + + return ( +
+ + {card.type} + {maskedCardNumber} + +
+ ) +} diff --git a/components/Profile/DeleteCreditCardButton/index.tsx b/components/Profile/DeleteCreditCardButton/index.tsx new file mode 100644 index 000000000..16448d0b1 --- /dev/null +++ b/components/Profile/DeleteCreditCardButton/index.tsx @@ -0,0 +1,40 @@ +"use client" +import { useIntl } from "react-intl" + +import { trpc } from "@/lib/trpc/client" + +import { Delete } from "@/components/Icons" +import LoadingSpinner from "@/components/LoadingSpinner" +import Button from "@/components/TempDesignSystem/Button" +import { toast } from "@/components/TempDesignSystem/Toasts" + +export default function DeleteCreditCardButton({ + creditCardId, +}: { + creditCardId: string +}) { + const { formatMessage } = useIntl() + const trpcUtils = trpc.useUtils() + + const deleteCreditCardMutation = trpc.user.creditCard.delete.useMutation({ + onSuccess() { + trpcUtils.user.creditCards.invalidate() + toast.success(formatMessage({ id: "Credit card deleted successfully" })) + }, + onError() { + toast.error( + formatMessage({ + id: "Failed to delete credit card, please try again later.", + }) + ) + }, + }) + async function handleDelete() { + deleteCreditCardMutation.mutate({ creditCardId }) + } + return ( + + ) +} diff --git a/components/Profile/DeleteCreditCardConfirmation/deleteCreditCardConfirmation.module.css b/components/Profile/DeleteCreditCardConfirmation/deleteCreditCardConfirmation.module.css new file mode 100644 index 000000000..ed6c3170d --- /dev/null +++ b/components/Profile/DeleteCreditCardConfirmation/deleteCreditCardConfirmation.module.css @@ -0,0 +1,70 @@ +.overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: var(--visual-viewport-height); + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + + &[data-entering] { + animation: modal-fade 200ms; + } + + &[data-exiting] { + animation: modal-fade 150ms reverse ease-in; + } +} + +.modal section { + background: var(--Main-Grey-White); + border-radius: var(--Corner-radius-Medium); + padding: var(--Spacing-x4); + padding-bottom: var(--Spacing-x6); +} + +.container { + display: flex; + flex-direction: column; + gap: var(--Spacing-x3); + font-family: var(--typography-Body-Regular-fontFamily); +} + +.title { + font-family: var(--typography-Subtitle-1-fontFamily); + text-align: center; + margin: 0; + padding-bottom: var(--Spacing-x1); +} + +.bodyText { + text-align: center; + max-width: 425px; + margin: 0; + padding: 0; +} + +.buttonContainer { + display: flex; + justify-content: space-between; + gap: var(--Spacing-x2); + flex-wrap: wrap; +} + +.buttonContainer button { + flex-grow: 1; + justify-content: center; +} + +@keyframes modal-fade { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} diff --git a/components/Profile/DeleteCreditCardConfirmation/index.tsx b/components/Profile/DeleteCreditCardConfirmation/index.tsx new file mode 100644 index 000000000..7d7110c1a --- /dev/null +++ b/components/Profile/DeleteCreditCardConfirmation/index.tsx @@ -0,0 +1,99 @@ +"use client" + +import { + Dialog, + DialogTrigger, + Heading, + Modal, + ModalOverlay, +} from "react-aria-components" +import { useIntl } from "react-intl" + +import { trpc } from "@/lib/trpc/client" + +import { Delete } from "@/components/Icons" +import LoadingSpinner from "@/components/LoadingSpinner" +import Button from "@/components/TempDesignSystem/Button" +import { toast } from "@/components/TempDesignSystem/Toasts" + +import styles from "./deleteCreditCardConfirmation.module.css" + +import type { DeleteCreditCardConfirmationProps } from "@/types/components/myPages/myProfile/creditCards" + +export default function DeleteCreditCardConfirmation({ + card, +}: DeleteCreditCardConfirmationProps) { + const intl = useIntl() + const trpcUtils = trpc.useUtils() + + const deleteCard = trpc.user.creditCard.delete.useMutation({ + onSuccess() { + trpcUtils.user.creditCards.invalidate() + + toast.success( + intl.formatMessage({ id: "Your card was successfully removed!" }) + ) + }, + onError() { + toast.error( + intl.formatMessage({ + id: "Something went wrong and we couldn't remove your card. Please try again later.", + }) + ) + }, + }) + + const lastFourDigits = card.truncatedNumber.slice(-4) + + return ( +
+ + + + + + {({ close }) => ( +
+ + {intl.formatMessage({ + id: "Remove card from member profile", + })} + +

+ {`${intl.formatMessage({ + id: "Are you sure you want to remove the card ending with", + })} ${lastFourDigits} ${intl.formatMessage({ id: "from your member profile?" })}`} +

+ + {deleteCard.isPending ? ( + + ) : ( +
+ + +
+ )} +
+ )} +
+
+
+
+
+ ) +} diff --git a/components/TempDesignSystem/Button/button.ts b/components/TempDesignSystem/Button/button.ts index cf391bc56..618ff8caa 100644 --- a/components/TempDesignSystem/Button/button.ts +++ b/components/TempDesignSystem/Button/button.ts @@ -1,9 +1,20 @@ import { buttonVariants } from "./variants" import type { VariantProps } from "class-variance-authority" +import type { ButtonProps as ReactAriaButtonProps } from "react-aria-components" -export interface ButtonProps +export interface ButtonPropsRAC + extends Omit, + VariantProps { + asChild?: false | undefined | never + disabled?: ReactAriaButtonProps["isDisabled"] + onClick?: ReactAriaButtonProps["onPress"] +} + +export interface ButtonPropsSlot extends React.ButtonHTMLAttributes, VariantProps { - asChild?: boolean + asChild: true } + +export type ButtonProps = ButtonPropsSlot | ButtonPropsRAC diff --git a/components/TempDesignSystem/Button/index.tsx b/components/TempDesignSystem/Button/index.tsx index 5acb884b1..38261e61a 100644 --- a/components/TempDesignSystem/Button/index.tsx +++ b/components/TempDesignSystem/Button/index.tsx @@ -1,23 +1,16 @@ "use client" import { Slot } from "@radix-ui/react-slot" +import { Button as ButtonRAC } from "react-aria-components" import { buttonVariants } from "./variants" import type { ButtonProps } from "./button" -export default function Button({ - asChild = false, - theme, - className, - disabled, - intent, - size, - variant, - wrapping, - ...props -}: ButtonProps) { - const Comp = asChild ? Slot : "button" +export default function Button(props: ButtonProps) { + const { className, intent, size, theme, wrapping, variant, ...restProps } = + props + const classNames = buttonVariants({ className, intent, @@ -26,5 +19,19 @@ export default function Button({ wrapping, variant, }) - return + + if (restProps.asChild) { + const { asChild, ...slotProps } = restProps + return + } + + const { asChild, onClick, disabled, ...racProps } = restProps + return ( + + ) } diff --git a/components/TempDesignSystem/Toasts/index.tsx b/components/TempDesignSystem/Toasts/index.tsx index 5e3ad1922..486bc4f46 100644 --- a/components/TempDesignSystem/Toasts/index.tsx +++ b/components/TempDesignSystem/Toasts/index.tsx @@ -16,7 +16,7 @@ import { toastVariants } from "./variants" import styles from "./toasts.module.css" export function ToastHandler() { - return + return } function getIcon(variant: ToastsProps["variant"]) { diff --git a/components/TempDesignSystem/Toasts/toasts.module.css b/components/TempDesignSystem/Toasts/toasts.module.css index cc7b305fb..27ebcff09 100644 --- a/components/TempDesignSystem/Toasts/toasts.module.css +++ b/components/TempDesignSystem/Toasts/toasts.module.css @@ -31,8 +31,9 @@ .iconContainer { display: flex; - background-color: var(--icon-background-color); - padding: var(--Spacing-x2); align-items: center; justify-content: center; + background-color: var(--icon-background-color); + padding: var(--Spacing-x2); + height: 100%; } diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 31af5e52f..3531fa46c 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -7,6 +7,8 @@ "All rooms comes with standard amenities": "Alle værelser er udstyret med standardfaciliteter", "Already a friend?": "Allerede en ven?", "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", "Arrival date": "Ankomstdato", "as of today": "fra idag", "As our": "Som vores", @@ -31,6 +33,7 @@ "Could not find requested resource": "Kunne ikke finde den anmodede ressource", "Country": "Land", "Country code": "Landekode", + "Credit card deleted successfully": "Kreditkort blev slettet", "Your current level": "Dit nuværende niveau", "Current password": "Nuværende kodeord", "characters": "tegn", @@ -45,10 +48,12 @@ "Extras to your booking": "Ekstra til din booking", "There are no transactions to display": "Der er ingen transaktioner at vise", "Explore all levels and benefits": "Udforsk alle niveauer og fordele", + "Failed to delete credit card, please try again later.": "Kunne ikke slette kreditkort. Prøv venligst igen senere.", "Find booking": "Find booking", "Flexibility": "Fleksibilitet", "Former Scandic Hotel": "Tidligere Scandic Hotel", "From": "Fra", + "from your member profile?": "fra din medlemsprofil?", "Get inspired": "Bliv inspireret", "Go back to overview": "Gå tilbage til oversigten", "Level 1": "Niveau 1", @@ -82,6 +87,7 @@ "Next": "Næste", "next level:": "Næste niveau:", "No content published": "Intet indhold offentliggjort", + "No, keep card": "Nej, behold kortet", "No transactions available": "Ingen tilgængelige transaktioner", "Not found": "Ikke fundet", "night": "nat", @@ -107,6 +113,7 @@ "Previous victories": "Tidligere sejre", "Read more": "Læs mere", "Read more about the hotel": "Læs mere om hotellet", + "Remove card from member profile": "Fjern kortet fra medlemsprofilen", "Restaurant & Bar": "Restaurant & Bar", "Retype new password": "Gentag den nye adgangskode", "Rooms": "Værelser", @@ -123,6 +130,8 @@ "Skip to main content": "Spring over og gå til hovedindhold", "Sign up bonus": "Tilmeldingsbonus", "Something went wrong!": "Noget gik galt!", + "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", "special character": "speciel karakter", "Total Points": "Samlet antal point", @@ -135,14 +144,18 @@ "User information": "Brugeroplysninger", "uppercase letter": "stort bogstav", "Visiting address": "Besøgsadresse", + "We could not add a card right now, please try again later.": "Vi kunne ikke tilføje et kort lige nu. Prøv venligst igen senere.", "Welcome": "Velkommen", "Welcome to": "Velkommen til", "Wellness & Exercise": "Velvære & Motion", "Where should you go next?": "Find inspiration til dit næste ophold", "Which room class suits you the best?": "Hvilken rumklasse passer bedst til dig", "Year": "År", + "You canceled adding a new credit card.": "Du har annulleret tilføjelsen af et nyt kreditkort.", + "Yes, remove my card": "Ja, fjern mit kort", "You have no previous stays.": "Du har ingen tidligere ophold.", "You have no upcoming stays.": "Du har ingen kommende ophold.", + "Your card was successfully removed!": "Dit kort blev fjernet!", "Your card was successfully saved!": "Dit kort blev gemt!", "Your Challenges Conquer & Earn!": "Dine udfordringer Overvind og tjen!", "Your level": "Dit niveau", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 7f0a26f2c..fa5a3c4ee 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -6,6 +6,8 @@ "All rooms comes with standard amenities": "Alle Zimmer sind mit den üblichen Annehmlichkeiten ausgestattet", "Already a friend?": "Sind wir schon Freunde?", "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", "Arrival date": "Ankunftsdatum", "as of today": "Ab heute", "As our": "Als unser", @@ -30,6 +32,7 @@ "Could not find requested resource": "Die angeforderte Ressource konnte nicht gefunden werden.", "Country": "Land", "Country code": "Landesvorwahl", + "Credit card deleted successfully": "Kreditkarte erfolgreich gelöscht", "Your current level": "Ihr aktuelles Level", "Current password": "Aktuelles Passwort", "characters": "figuren", @@ -44,10 +47,12 @@ "Extras to your booking": "Extras zu Ihrer Buchung", "There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden", "Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile", + "Failed to delete credit card, please try again later.": "Kreditkarte konnte nicht gelöscht werden. Bitte versuchen Sie es später noch einmal.", "Find booking": "Buchung finden", "Flexibility": "Flexibilität", "Former Scandic Hotel": "Ehemaliges Scandic Hotel", "From": "Fromm", + "from your member profile?": "wirklich aus Ihrem Mitgliedsprofil entfernen?", "Get inspired": "Lassen Sie sich inspieren", "Go back to overview": "Zurück zur Übersicht", "Level 1": "Level 1", @@ -80,6 +85,7 @@ "Next": "Nächste", "next level:": "Nächstes Level:", "No content published": "Kein Inhalt veröffentlicht", + "No, keep card": "Nein, Karte behalten", "No transactions available": "Keine Transaktionen verfügbar", "Not found": "Nicht gefunden", "night": "nacht", @@ -104,6 +110,7 @@ "Previous victories": "Bisherige Siege", "Read more": "Mehr lesen", "Read more about the hotel": "Lesen Sie mehr über das Hotel", + "Remove card from member profile": "Karte aus dem Mitgliedsprofil entfernen", "Retype new password": "Neues Passwort erneut eingeben", "Save": "Speichern", "Scandic Friends Mastercard": "Scandic Friends Mastercard", @@ -118,6 +125,8 @@ "Skip to main content": "Direkt zum Inhalt", "Sign up bonus": "Anmeldebonus", "Something went wrong!": "Etwas ist schief gelaufen!", + "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", "special character": "sonderzeichen", "Total Points": "Gesamtpunktzahl", @@ -130,13 +139,17 @@ "User information": "Nutzerinformation", "uppercase letter": "großbuchstabe", "Visiting address": "Besuchsadresse", + "We could not add a card right now, please try again later.": "Wir konnten momentan keine Karte hinzufügen. Bitte versuchen Sie es später noch einmal.", "Welcome to": "Willkommen zu", "Welcome": "Willkommen", "Where should you go next?": "Wo geht es als Nächstes hin?", "Which room class suits you the best?": "Welche Zimmerklasse passt am besten zu Ihnen?", "Year": "Jahr", + "You canceled adding a new credit card.": "Sie haben das Hinzufügen einer neuen Kreditkarte abgebrochen.", + "Yes, remove my card": "Ja, meine Karte entfernen", "You have no previous stays.": "Sie haben keine vorherigen Aufenthalte.", "You have no upcoming stays.": "Sie haben keine bevorstehenden Aufenthalte.", + "Your card was successfully removed!": "Ihre Karte wurde erfolgreich entfernt!", "Your card was successfully saved!": "Ihre Karte wurde erfolgreich gespeichert!", "Your Challenges Conquer & Earn!": "Meistern Sie Ihre Herausforderungen und verdienen Sie Geld!", "Your level": "Dein level", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 62790f4fc..93b93b252 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -7,6 +7,8 @@ "All rooms comes with standard amenities": "All rooms comes with standard amenities", "Already a friend?": "Already a friend?", "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", "Arrival date": "Arrival date", "as of today": "as of today", "As our": "As our", @@ -34,6 +36,7 @@ "Your current level": "Your current level", "Current password": "Current password", "characters": "characters", + "Credit card deleted successfully": "Credit card deleted successfully", "Date of Birth": "Date of Birth", "Day": "Day", "Description": "Description", @@ -45,12 +48,14 @@ "Email": "Email", "There are no transactions to display": "There are no transactions to display", "Explore all levels and benefits": "Explore all levels and benefits", + "Failed to delete credit card, please try again later.": "Failed to delete credit card, please try again later.", "Extras to your booking": "Extras to your booking", "FAQ": "FAQ", "Find booking": "Find booking", "Former Scandic Hotel": "Former Scandic Hotel", "Flexibility": "Flexibility", "From": "From", + "from your member profile?": "from your member profile?", "Get inspired": "Get inspired", "Go back to overview": "Go back to overview", "hotelPages.rooms.roomCard.person": "person", @@ -87,6 +92,7 @@ "Next": "Next", "next level:": "next level:", "No content published": "No content published", + "No, keep card": "No, keep card", "No transactions available": "No transactions available", "Not found": "Not found", "night": "night", @@ -112,6 +118,7 @@ "Previous victories": "Previous victories", "Read more": "Read more", "Read more about the hotel": "Read more about the hotel", + "Remove card from member profile": "Remove card from member profile", "Restaurant & Bar": "Restaurant & Bar", "Retype new password": "Retype new password", "Rooms": "Rooms", @@ -129,10 +136,13 @@ "Skip to main content": "Skip to main content", "Sign up bonus": "Sign up bonus", "Something went wrong!": "Something went wrong!", + "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", "special character": "special character", "Total Points": "Total Points", "Your points to spend": "Your points to spend", + "You canceled adding a new credit card.": "You canceled adding a new credit card.", "Transaction date": "Transaction date", "Transactions": "Transactions", "Tripadvisor reviews": "{rating} ({count} reviews on Tripadvisor)", @@ -142,13 +152,16 @@ "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 to": "Welcome to", "Wellness & Exercise": "Wellness & Exercise", "Where should you go next?": "Where should you go next?", "Which room class suits you the best?": "Which room class suits you the best?", "Year": "Year", + "Yes, remove my card": "Yes, remove my card", "You have no previous stays.": "You have no previous stays.", "You have no upcoming stays.": "You have no upcoming stays.", + "Your card was successfully removed!": "Your card was successfully removed!", "Your card was successfully saved!": "Your card was successfully saved!", "Your Challenges Conquer & Earn!": "Your Challenges Conquer & Earn!", "Your level": "Your level", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index a244f16d3..a7fac093e 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -7,6 +7,8 @@ "All rooms comes with standard amenities": "Kaikissa huoneissa on perusmukavuudet", "Already a friend?": "Oletko jo ystävä?", "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", "Arrival date": "Saapumispäivä", "as of today": "tästä päivästä lähtien", "As our": "Kuin meidän", @@ -31,6 +33,7 @@ "Could not find requested resource": "Pyydettyä resurssia ei löytynyt", "Country": "Maa", "Country code": "Maatunnus", + "Credit card deleted successfully": "Luottokortti poistettu onnistuneesti", "Your current level": "Nykyinen tasosi", "Current password": "Nykyinen salasana", "characters": "hahmoja", @@ -45,10 +48,12 @@ "Extras to your booking": "Lisävarusteet varaukseesi", "There are no transactions to display": "Näytettäviä tapahtumia ei ole", "Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin", + "Failed to delete credit card, please try again later.": "Luottokortin poistaminen epäonnistui, yritä myöhemmin uudelleen.", "Find booking": "Etsi varaus", "Flexibility": "Joustavuus", "Former Scandic Hotel": "Entinen Scandic Hotel", "From": "From", + "from your member profile?": "jäsenprofiilistasi?", "Get inspired": "Inspiroidu", "Go back to overview": "Palaa yleiskatsaukseen", "Level 1": "Taso 1", @@ -82,6 +87,7 @@ "Next": "Seuraava", "next level:": "Seuraava taso:", "No content published": "Ei julkaistua sisältöä", + "No, keep card": "Ei, pidä kortti", "No transactions available": "Ei tapahtumia saatavilla", "Not found": "Ei löydetty", "night": "yö", @@ -107,6 +113,7 @@ "Previous victories": "Edelliset voitot", "Read more": "Lue lisää", "Read more about the hotel": "Lue lisää hotellista", + "Remove card from member profile": "Poista kortti jäsenprofiilista", "Restaurant & Bar": "Ravintola & Baari", "Retype new password": "Kirjoita uusi salasana uudelleen", "Rooms": "Huoneet", @@ -123,6 +130,8 @@ "Skip to main content": "Siirry pääsisältöön", "Sign up bonus": "Rekisteröidy bonus", "Something went wrong!": "Jotain meni pieleen!", + "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", "special character": "erikoishahmo", "Total Points": "Kokonaispisteet", @@ -135,15 +144,19 @@ "User information": "Käyttäjän tiedot", "uppercase letter": "iso kirjain", "Visiting address": "Käyntiosoite", + "We could not add a card right now, please try again later.": "Emme voineet lisätä korttia juuri nyt. Yritä myöhemmin uudelleen.", "Welcome": "Tervetuloa", "Welcome to": "Tervetuloa", "Wellness & Exercise": "Hyvinvointi & Liikunta", "Where should you go next?": "Mihin menisit seuraavaksi?", "Which room class suits you the best?": "Mikä huoneluokka sopii sinulle parhaiten?", "Year": "Vuosi", + "Yes, remove my card": "Kyllä, poista korttini", "You have no previous stays.": "Sinulla ei ole aiempaa oleskelua.", "You have no upcoming stays.": "Sinulla ei ole tulevia oleskeluja.", + "Your card was successfully removed!": "Korttisi poistettiin onnistuneesti!", "Your card was successfully saved!": "Korttisi tallennettu onnistuneesti!", + "You canceled adding a new credit card.": "Peruutit uuden luottokortin lisäämisen.", "Your Challenges Conquer & Earn!": "Voita ja ansaitse haasteesi!", "Your level": "Tasosi", "Zip code": "Postinumero", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index cc79768ee..33c1916c2 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -7,6 +7,8 @@ "All rooms comes with standard amenities": "Alle rommene har standard fasiliteter", "Already a friend?": "Allerede Friend?", "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å", "Arrival date": "Ankomstdato", "as of today": "per idag", "As our": "Som vår", @@ -34,6 +36,7 @@ "Your current level": "Ditt nåværende nivå", "Current password": "Nåværende passord", "characters": "tegn", + "Credit card deleted successfully": "Kredittkort slettet", "Date of Birth": "Fødselsdato", "Day": "Dag", "Description": "Beskrivelse", @@ -45,10 +48,12 @@ "Extras to your booking": "Ekstra til din bestilling", "There are no transactions to display": "Det er ingen transaksjoner å vise", "Explore all levels and benefits": "Utforsk alle nivåer og fordeler", + "Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.", "Find booking": "Finn booking", "Flexibility": "Fleksibilitet", "Former Scandic Hotel": "Tidligere Scandic Hotel", "From": "Fra", + "from your member profile?": "fra medlemsprofilen din?", "Get inspired": "Bli inspirert", "Go back to overview": "Gå tilbake til oversikten", "Level 1": "Nivå 1", @@ -82,6 +87,7 @@ "Next": "Neste", "next level:": "Neste nivå:", "No content published": "Ingen innhold publisert", + "No, keep card": "Nei, behold kortet", "No transactions available": "Ingen transaksjoner tilgjengelig", "Not found": "Ikke funnet", "night": "natt", @@ -107,6 +113,7 @@ "Previous victories": "Tidligere seire", "Read more": "Les mer", "Read more about the hotel": "Les mer om hotellet", + "Remove card from member profile": "Fjern kortet fra medlemsprofilen", "Restaurant & Bar": "Restaurant & Bar", "Retype new password": "Skriv inn nytt passord på nytt", "Rooms": "Rom", @@ -123,6 +130,8 @@ "Skip to main content": "Gå videre til hovedsiden", "Sign up bonus": "Registreringsbonus", "Something went wrong!": "Noe gikk galt!", + "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", "special character": "spesiell karakter", "Total Points": "Totale poeng", @@ -135,14 +144,18 @@ "User information": "Brukerinformasjon", "uppercase letter": "stor bokstav", "Visiting address": "Besøksadresse", + "We could not add a card right now, please try again later.": "Vi kunne ikke legge til et kort akkurat nå. Prøv igjen senere.", "Welcome": "Velkommen", "Welcome to": "Velkommen til", "Wellness & Exercise": "Velvære & Trening", "Where should you go next?": "Hvor ønsker du å reise neste gang?", "Which room class suits you the best?": "Hvilken romklasse passer deg best?", "Year": "År", + "You canceled adding a new credit card.": "Du kansellerte å legge til et nytt kredittkort.", + "Yes, remove my card": "Ja, fjern kortet mitt", "You have no previous stays.": "Du har ingen tidligere opphold.", "You have no upcoming stays.": "Du har ingen kommende opphold.", + "Your card was successfully removed!": "Kortet ditt ble fjernet!", "Your card was successfully saved!": "Kortet ditt ble lagret!", "Your Challenges Conquer & Earn!": "Dine utfordringer Erobre og tjen!", "Your level": "Ditt nivå", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 0e8f4695f..b40f77323 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -7,6 +7,8 @@ "All rooms comes with standard amenities": "Alla rum har standardbekvämligheter", "Already a friend?": "Är du redan en vän?", "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", "Arrival date": "Ankomstdatum", "as of today": "från och med idag", "As our": "Som vår", @@ -31,6 +33,7 @@ "Could not find requested resource": "Det gick inte att hitta den begärda resursen", "Country": "Land", "Country code": "Landskod", + "Credit card deleted successfully": "Kreditkort har tagits bort", "Your current level": "Din nuvarande nivå", "Current password": "Nuvarande lösenord", "characters": "tecken", @@ -45,10 +48,12 @@ "Extras to your booking": "Extra till din bokning", "There are no transactions to display": "Det finns inga transaktioner att visa", "Explore all levels and benefits": "Utforska alla nivåer och fördelar", + "Failed to delete credit card, please try again later.": "Det gick inte att ta bort kreditkortet, försök igen senare.", "Find booking": "Hitta bokning", "Flexibility": "Flexibilitet", "Former Scandic Hotel": "Tidigare Scandic Hotel", "From": "Från", + "from your member profile?": "från din medlemsprofil?", "Get inspired": "Bli inspirerad", "Go back to overview": "Gå tillbaka till översikten", "Level 1": "Nivå 1", @@ -85,6 +90,7 @@ "Next": "Nästa", "next level:": "Nästa nivå:", "No content published": "Inget innehåll publicerat", + "No, keep card": "Nej, behåll kortet", "No transactions available": "Inga transaktioner tillgängliga", "Not found": "Hittades inte", "night": "natt", @@ -110,6 +116,7 @@ "Previous victories": "Tidigare segrar", "Read more": "Läs mer", "Read more about the hotel": "Läs mer om hotellet", + "Remove card from member profile": "Ta bort kortet från medlemsprofilen", "Restaurant & Bar": "Restaurang & Bar", "Retype new password": "Upprepa nytt lösenord", "Rooms": "Rum", @@ -126,6 +133,8 @@ "Skip to main content": "Fortsätt till huvudinnehåll", "Sign up bonus": "Registreringsbonus", "Something went wrong!": "Något gick fel!", + "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", "special character": "speciell karaktär", "Total Points": "Poäng totalt", @@ -138,13 +147,17 @@ "User information": "Användar information", "uppercase letter": "stor bokstav", "Visiting address": "Besöksadress", + "We could not add a card right now, please try again later.": "Vi kunde inte lägga till ett kort just nu, vänligen försök igen senare.", "Welcome": "Välkommen", "Wellness & Exercise": "Hälsa & Träning", "Where should you go next?": "Låter inte en spontanweekend härligt?", "Which room class suits you the best?": "Vilken rumsklass passar dig bäst?", "Year": "År", + "You canceled adding a new credit card.": "Du avbröt att lägga till ett nytt kreditkort.", + "Yes, remove my card": "Ja, ta bort mitt kort", "You have no previous stays.": "Du har inga tidigare vistelser.", "You have no upcoming stays.": "Du har inga planerade resor.", + "Your card was successfully removed!": "Ditt kort har tagits bort!", "Your card was successfully saved!": "Ditt kort har sparats!", "Your Challenges Conquer & Earn!": "Dina utmaningar Erövra och tjäna!", "Your level": "Din nivå", diff --git a/lib/api/endpoints.ts b/lib/api/endpoints.ts index 7235b9df9..7fb7e5e9f 100644 --- a/lib/api/endpoints.ts +++ b/lib/api/endpoints.ts @@ -13,6 +13,8 @@ export namespace endpoints { upcomingStays = "booking/v1/Stays/future", previousStays = "booking/v1/Stays/past", hotels = "hotel/v1/Hotels", + intiateSaveCard = `${creditCards}/initiateSaveCard`, + deleteCreditCard = `${profile}/creditCards`, } } diff --git a/lib/api/index.ts b/lib/api/index.ts index 97d24039b..a3ec13a6c 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -27,7 +27,7 @@ const fetch = fetchRetry(global.fetch, { }) export async function get( - endpoint: Endpoint | `${endpoints.v1.hotels}/${string}`, + endpoint: Endpoint | `${Endpoint}/${string}`, options: RequestOptionsWithOutBody, params?: URLSearchParams ) { @@ -38,7 +38,7 @@ export async function get( } export async function patch( - endpoint: Endpoint, + endpoint: Endpoint | `${Endpoint}/${string}`, options: RequestOptionsWithJSONBody ) { const { body, ...requestOptions } = options @@ -54,11 +54,12 @@ export async function patch( export async function post( endpoint: Endpoint | `${Endpoint}/${string}`, - options: RequestOptionsWithJSONBody + options: RequestOptionsWithJSONBody, + params?: URLSearchParams ) { const { body, ...requestOptions } = options return fetch( - `${env.API_BASEURL}/${endpoint}`, + `${env.API_BASEURL}/${endpoint}${params ? `?${params.toString()}` : ""}`, merge.all([ defaultOptions, { body: JSON.stringify(body), method: "POST" }, @@ -68,11 +69,12 @@ export async function post( } export async function remove( - endpoint: Endpoint, - options: RequestOptionsWithOutBody + endpoint: Endpoint | `${Endpoint}/${string}`, + options: RequestOptionsWithOutBody, + params?: URLSearchParams ) { return fetch( - `${env.API_BASEURL}/${endpoint}`, + `${env.API_BASEURL}/${endpoint}${params ? `?${params.toString()}` : ""}`, merge.all([defaultOptions, { method: "DELETE" }, options]) ) } diff --git a/lib/trpc/client.ts b/lib/trpc/client.ts index d3ba9ef68..46d50e91a 100644 --- a/lib/trpc/client.ts +++ b/lib/trpc/client.ts @@ -1,5 +1,9 @@ import { createTRPCReact } from "@trpc/react-query" +import { inferRouterInputs, inferRouterOutputs } from "@trpc/server" import type { AppRouter } from "@/server" export const trpc = createTRPCReact() + +export type RouterInput = inferRouterInputs +export type RouterOutput = inferRouterOutputs diff --git a/server/routers/user/index.ts b/server/routers/user/index.ts index 6e21035fe..21941006d 100644 --- a/server/routers/user/index.ts +++ b/server/routers/user/index.ts @@ -1,5 +1,6 @@ import { mergeRouters } from "@/server/trpc" +import { userMutationRouter } from "./mutation" import { userQueryRouter } from "./query" -export const userRouter = mergeRouters(userQueryRouter) +export const userRouter = mergeRouters(userQueryRouter, userMutationRouter) diff --git a/server/routers/user/input.ts b/server/routers/user/input.ts index db9fe1f2f..a3dea492e 100644 --- a/server/routers/user/input.ts +++ b/server/routers/user/input.ts @@ -1,5 +1,7 @@ import { z } from "zod" +import { Lang } from "@/constants/languages" + export const getUserInputSchema = z .object({ mask: z.boolean().default(true), @@ -19,11 +21,15 @@ export const soonestUpcomingStaysInput = z }) .default({ limit: 3 }) -export const initiateSaveCardInput = z.object({ +export const addCreditCardInput = z.object({ language: z.string(), }) -export const saveCardInput = z.object({ +export const deleteCreditCardInput = z.object({ + creditCardId: z.string(), +}) + +export const saveCreditCardInput = z.object({ transactionId: z.string(), merchantId: z.string().optional(), }) diff --git a/server/routers/user/mutation.ts b/server/routers/user/mutation.ts new file mode 100644 index 000000000..cf568725c --- /dev/null +++ b/server/routers/user/mutation.ts @@ -0,0 +1,85 @@ +import * as api from "@/lib/api" +import { initiateSaveCardSchema } from "@/server/routers/user/output" +import { protectedProcedure, router } from "@/server/trpc" + +import { + addCreditCardInput, + deleteCreditCardInput, + saveCreditCardInput, +} from "./input" + +export const userMutationRouter = router({ + creditCard: router({ + add: protectedProcedure.input(addCreditCardInput).mutation(async function ({ + ctx, + input, + }) { + const apiResponse = await api.post(api.endpoints.v1.intiateSaveCard, { + headers: { + Authorization: `Bearer ${ctx.session.token.access_token}`, + }, + body: { + language: input.language, + mobileToken: false, + redirectUrl: `api/web/add-card-callback/${input.language}`, + }, + }) + + if (!apiResponse.ok) { + console.info(`API Response Failed - Initiating add Creadit Card flow`) + console.error(apiResponse) + return null + } + + const apiJson = await apiResponse.json() + const verifiedData = initiateSaveCardSchema.safeParse(apiJson) + if (!verifiedData.success) { + console.error(`Failed to initiate save card data`) + console.error(verifiedData.error) + return null + } + + return verifiedData.data.data + }), + save: protectedProcedure + .input(saveCreditCardInput) + .mutation(async function ({ ctx, input }) { + const apiResponse = await api.post( + `${api.endpoints.v1.creditCards}/${input.transactionId}`, + { + headers: { + Authorization: `Bearer ${ctx.session.token.access_token}`, + }, + } + ) + + if (!apiResponse.ok) { + console.error(`API Response Failed - Save card`) + console.error(apiResponse) + return false + } + + return true + }), + delete: protectedProcedure + .input(deleteCreditCardInput) + .mutation(async function ({ ctx, input }) { + const apiResponse = await api.remove( + `${api.endpoints.v1.creditCards}/${input.creditCardId}`, + { + headers: { + Authorization: `Bearer ${ctx.session.token.access_token}`, + }, + } + ) + + if (!apiResponse.ok) { + console.error(`API Response Failed - Delete credit card`) + console.error(apiResponse) + return false + } + + return true + }), + }), +}) diff --git a/server/routers/user/output.ts b/server/routers/user/output.ts index d079b8323..e135ace77 100644 --- a/server/routers/user/output.ts +++ b/server/routers/user/output.ts @@ -176,20 +176,28 @@ type GetFriendTransactionsData = z.infer export type FriendTransaction = GetFriendTransactionsData["data"][number] -export const getCreditCardsSchema = z.object({ - data: z.array( - z.object({ - attribute: z.object({ - cardName: z.string().optional(), - alias: z.string(), - truncatedNumber: z.string(), - expirationDate: z.string(), - cardType: z.string(), - }), - id: z.string(), - type: z.string(), - }) - ), +export const creditCardSchema = z + .object({ + attribute: z.object({ + cardName: z.string().optional(), + alias: z.string(), + truncatedNumber: z.string(), + expirationDate: z.string(), + cardType: z.string(), + }), + id: z.string(), + type: z.string(), + }) + .transform((apiResponse) => { + return { + id: apiResponse.id, + type: apiResponse.attribute.cardType, + truncatedNumber: apiResponse.attribute.truncatedNumber, + } + }) + +export const creditCardsSchema = z.object({ + data: z.array(creditCardSchema), }) export const getMembershipCardsSchema = z.array( diff --git a/server/routers/user/query.ts b/server/routers/user/query.ts index 47247658e..c2cad2400 100644 --- a/server/routers/user/query.ts +++ b/server/routers/user/query.ts @@ -1,12 +1,6 @@ import { Lang } from "@/constants/languages" import { env } from "@/env/server" import * as api from "@/lib/api" -import { internalServerError } from "@/server/errors/next" -import { - badRequestError, - forbiddenError, - unauthorizedError, -} from "@/server/errors/trpc" import { protectedProcedure, router, @@ -21,18 +15,15 @@ import encryptValue from "../utils/encryptValue" import { friendTransactionsInput, getUserInputSchema, - initiateSaveCardInput, - saveCardInput, staysInput, } from "./input" import { + creditCardsSchema, FriendTransaction, - getCreditCardsSchema, getFriendTransactionsSchema, getMembershipCardsSchema, getStaysSchema, getUserSchema, - initiateSaveCardSchema, Stay, } from "./output" import { benefits, extendedUser, nextLevelPerks } from "./temp" @@ -566,7 +557,7 @@ export const userQueryRouter = router({ } const apiJson = await apiResponse.json() - const verifiedData = getCreditCardsSchema.safeParse(apiJson) + const verifiedData = creditCardsSchema.safeParse(apiJson) if (!verifiedData.success) { console.error(`Failed to validate Credit Cards Data`) console.error(`User: (${JSON.stringify(ctx.session.user)})`) @@ -577,69 +568,6 @@ export const userQueryRouter = router({ return verifiedData.data.data }), - initiateSaveCard: protectedProcedure - .input(initiateSaveCardInput) - .mutation(async function ({ ctx, input }) { - const apiResponse = await api.post(api.endpoints.v1.initiateSaveCard, { - headers: { - Authorization: `Bearer ${ctx.session.token.access_token}`, - }, - body: { - language: input.language, - mobileToken: false, - redirectUrl: `api/web/add-card-callback/${input.language}`, - }, - }) - - if (!apiResponse.ok) { - switch (apiResponse.status) { - case 400: - throw badRequestError(apiResponse) - case 401: - throw unauthorizedError(apiResponse) - case 403: - throw forbiddenError(apiResponse) - default: - throw internalServerError(apiResponse) - } - } - - const apiJson = await apiResponse.json() - const verifiedData = initiateSaveCardSchema.safeParse(apiJson) - if (!verifiedData.success) { - console.error(`Failed to initiate save card data`) - console.error(`User: (${JSON.stringify(ctx.session.user)})`) - console.error(verifiedData.error) - return null - } - - return verifiedData.data.data - }), - - saveCard: protectedProcedure.input(saveCardInput).mutation(async function ({ - ctx, - input, - }) { - const apiResponse = await api.post( - `${api.endpoints.v1.creditCards}/${input.transactionId}`, - { - headers: { - Authorization: `Bearer ${ctx.session.token.access_token}`, - }, - body: {}, - } - ) - - if (!apiResponse.ok) { - console.error(`API Response Failed - Save card`) - console.error(`User: (${JSON.stringify(ctx.session.user)})`) - console.error(apiResponse) - return null - } - - return true - }), - membershipCards: protectedProcedure.query(async function ({ ctx }) { const apiResponse = await api.get(api.endpoints.v1.profile, { cache: "no-store", diff --git a/types/components/myPages/myProfile/card.ts b/types/components/myPages/myProfile/card.ts deleted file mode 100644 index cece9fef6..000000000 --- a/types/components/myPages/myProfile/card.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface CardProps extends React.HTMLAttributes { - tag?: "article" | "div" | "section" -} diff --git a/types/components/myPages/myProfile/creditCards.ts b/types/components/myPages/myProfile/creditCards.ts new file mode 100644 index 000000000..6290f5288 --- /dev/null +++ b/types/components/myPages/myProfile/creditCards.ts @@ -0,0 +1,9 @@ +import type { CreditCard } from "@/types/user" + +export type CreditCardRowProps = { + card: CreditCard +} + +export type DeleteCreditCardConfirmationProps = { + card: CreditCard +} diff --git a/types/fetch.ts b/types/fetch.ts index 5f71dc355..a727709a9 100644 --- a/types/fetch.ts +++ b/types/fetch.ts @@ -1,7 +1,7 @@ export interface RequestOptionsWithJSONBody extends Omit { - body: Record + body?: Record } export interface RequestOptionsWithOutBody - extends Omit { } + extends Omit {} diff --git a/types/user.ts b/types/user.ts index c4599e579..a90602661 100644 --- a/types/user.ts +++ b/types/user.ts @@ -1,6 +1,6 @@ import { z } from "zod" -import { getUserSchema } from "@/server/routers/user/output" +import { creditCardSchema, getUserSchema } from "@/server/routers/user/output" type Journey = { tag: string @@ -28,3 +28,5 @@ export interface User extends z.infer { shortcuts: ShortcutLink[] victories: Victory[] } + +export type CreditCard = z.output