Merged in feat/SW-245-delete-card (pull request #407)

Feat/SW-245 delete card

Approved-by: Michael Zetterberg
Approved-by: Christel Westerberg
This commit is contained in:
Tobias Johansson
2024-08-21 14:26:11 +00:00
committed by Michael Zetterberg
33 changed files with 603 additions and 208 deletions

View File

@@ -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);

View File

@@ -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<LangParams>) {
})}
</Body>
</article>
{creditCards?.length ? (
<div className={styles.cardContainer}>
{creditCards.map((card, idx) => (
<CreditCardRow
key={idx}
cardType={card.attribute.cardType}
truncatedNumber={card.attribute.truncatedNumber}
/>
))}
</div>
) : null}
<CreditCardList initialData={creditCards} />
<AddCreditCardButton />
</section>
)
}
function CreditCardRow({
truncatedNumber,
cardType,
}: {
truncatedNumber: string
cardType: string
}) {
const maskedCardNumber = `**** ${truncatedNumber.slice(12, 16)}`
return (
<div className={styles.card}>
<CreditCard color="black" />
<Body textTransform="bold">{cardType}</Body>
<Caption color="textMediumContrast">{maskedCardNumber}</Caption>
<Button variant="icon" theme="base" intent="text">
<Delete color="burgundy" />
</Button>
</div>
)
}

View File

@@ -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)
}

View File

@@ -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 (

View File

@@ -0,0 +1,4 @@
.cardContainer {
display: grid;
gap: var(--Spacing-x1);
}

View File

@@ -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 (
<div className={styles.cardContainer}>
{creditCards.data.map((card) => (
<CreditCardRow key={card.id} card={card} />
))}
</div>
)
}

View File

@@ -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);
}

View File

@@ -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 (
<div className={styles.card}>
<CreditCard color="black" />
<Body textTransform="bold">{card.type}</Body>
<Caption color="textMediumContrast">{maskedCardNumber}</Caption>
<DeleteCreditCardConfirmation card={card} />
</div>
)
}

View File

@@ -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 (
<Button variant="icon" theme="base" intent="text" onClick={handleDelete}>
<Delete color="burgundy" />
</Button>
)
}

View File

@@ -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;
}
}

View File

@@ -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 (
<div>
<DialogTrigger>
<Button variant="icon" theme="base" intent="text">
<Delete color="burgundy" />
</Button>
<ModalOverlay className={styles.overlay} isDismissable>
<Modal className={styles.modal}>
<Dialog role="alertdialog">
{({ close }) => (
<div className={styles.container}>
<Heading slot="title" className={styles.title}>
{intl.formatMessage({
id: "Remove card from member profile",
})}
</Heading>
<p className={styles.bodyText}>
{`${intl.formatMessage({
id: "Are you sure you want to remove the card ending with",
})} ${lastFourDigits} ${intl.formatMessage({ id: "from your member profile?" })}`}
</p>
{deleteCard.isPending ? (
<LoadingSpinner />
) : (
<div className={styles.buttonContainer}>
<Button intent="secondary" theme="base" onClick={close}>
{intl.formatMessage({ id: "No, keep card" })}
</Button>
<Button
intent="primary"
theme="base"
onClick={() => {
deleteCard.mutate(
{ creditCardId: card.id },
{ onSettled: close }
)
}}
>
{intl.formatMessage({ id: "Yes, remove my card" })}
</Button>
</div>
)}
</div>
)}
</Dialog>
</Modal>
</ModalOverlay>
</DialogTrigger>
</div>
)
}

View File

@@ -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<ReactAriaButtonProps, "isDisabled">,
VariantProps<typeof buttonVariants> {
asChild?: false | undefined | never
disabled?: ReactAriaButtonProps["isDisabled"]
onClick?: ReactAriaButtonProps["onPress"]
}
export interface ButtonPropsSlot
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
asChild: true
}
export type ButtonProps = ButtonPropsSlot | ButtonPropsRAC

View File

@@ -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 <Comp className={classNames} disabled={disabled} {...props} />
if (restProps.asChild) {
const { asChild, ...slotProps } = restProps
return <Slot className={classNames} {...slotProps} />
}
const { asChild, onClick, disabled, ...racProps } = restProps
return (
<ButtonRAC
className={classNames}
isDisabled={disabled}
onPress={onClick}
{...racProps}
/>
)
}

View File

@@ -16,7 +16,7 @@ import { toastVariants } from "./variants"
import styles from "./toasts.module.css"
export function ToastHandler() {
return <Toaster />
return <Toaster position="bottom-right" />
}
function getIcon(variant: ToastsProps["variant"]) {

View File

@@ -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%;
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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å",

View File

@@ -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å",

View File

@@ -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`,
}
}

View File

@@ -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])
)
}

View File

@@ -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<AppRouter>()
export type RouterInput = inferRouterInputs<AppRouter>
export type RouterOutput = inferRouterOutputs<AppRouter>

View File

@@ -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)

View File

@@ -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(),
})

View File

@@ -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
}),
}),
})

View File

@@ -176,20 +176,28 @@ type GetFriendTransactionsData = z.infer<typeof getFriendTransactionsSchema>
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(

View File

@@ -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",

View File

@@ -1,3 +0,0 @@
export interface CardProps extends React.HTMLAttributes<HTMLElement> {
tag?: "article" | "div" | "section"
}

View File

@@ -0,0 +1,9 @@
import type { CreditCard } from "@/types/user"
export type CreditCardRowProps = {
card: CreditCard
}
export type DeleteCreditCardConfirmationProps = {
card: CreditCard
}

View File

@@ -1,7 +1,7 @@
export interface RequestOptionsWithJSONBody
extends Omit<RequestInit, "body" | "method"> {
body: Record<string, unknown>
body?: Record<string, unknown>
}
export interface RequestOptionsWithOutBody
extends Omit<RequestInit, "body" | "method"> { }
extends Omit<RequestInit, "body" | "method"> {}

View File

@@ -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<typeof getUserSchema> {
shortcuts: ShortcutLink[]
victories: Victory[]
}
export type CreditCard = z.output<typeof creditCardSchema>