Merged in feat/SW-1149-handle-status-polling (pull request #1562)
Feat/SW-1149 handle status polling * feat(SW-1149): move terms and conditions sections to separate component and added copy * feat(SW-1149): Added client component to handle success callback for payment flow * fix: check for bookingCompleted status as well * feat(SW-1587): use alert instead of toast for showing payment errors * fix: added enum for payment callback status * fix: proper way of checking for multiple statuses * fix: update schema type * fix: use localised link to customer service * fix: update to use enum for status strings Approved-by: Arvid Norlin
This commit is contained in:
@@ -29,7 +29,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
padding: 0 var(--Spacing-x2);
|
||||
width: min(800px, 100%);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,20 +8,15 @@ import { Button } from "@scandic-hotels/design-system/Button"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { PaymentMethodEnum } from "@/constants/booking"
|
||||
import {
|
||||
bookingTermsAndConditions,
|
||||
privacyPolicy,
|
||||
} from "@/constants/currentWebHrefs"
|
||||
|
||||
import { InfoCircleIcon } from "@/components/Icons"
|
||||
import Modal from "@/components/Modal"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import MySavedCards from "../Payment/MySavedCards"
|
||||
import PaymentOption from "../Payment/PaymentOption"
|
||||
import TermsAndConditions from "../Payment/TermsAndConditions"
|
||||
|
||||
import styles from "./confirm.module.css"
|
||||
|
||||
@@ -35,7 +30,6 @@ export default function ConfirmBooking({
|
||||
savedCreditCards,
|
||||
}: ConfirmBookingProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const [isModalOpen, setModalOpen] = useState(false)
|
||||
|
||||
const { watch } = useFormContext()
|
||||
@@ -130,50 +124,7 @@ export default function ConfirmBooking({
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.checkboxContainer}>
|
||||
<Checkbox name="smsConfirmation">
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "I would like to get my booking confirmation via sms",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</Checkbox>
|
||||
<div className={styles.checkbox}>
|
||||
<Checkbox name="termsAndConditions" topAlign>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.",
|
||||
},
|
||||
{
|
||||
termsAndConditionsLink: (str) => (
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="underscored"
|
||||
href={bookingTermsAndConditions[lang]}
|
||||
target="_blank"
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
privacyPolicyLink: (str) => (
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="underscored"
|
||||
href={privacyPolicy[lang]}
|
||||
target="_blank"
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
</Checkbox>
|
||||
</div>
|
||||
<TermsAndConditions />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
"use client"
|
||||
|
||||
import { usePathname, useSearchParams } from "next/navigation"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { BookingErrorCodeEnum } from "@/constants/booking"
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
|
||||
import Alert from "@/components/TempDesignSystem/Alert"
|
||||
|
||||
import styles from "./paymentAlert.module.css"
|
||||
|
||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||
|
||||
function useBookingErrorAlert() {
|
||||
const updateSearchParams = useEnterDetailsStore(
|
||||
(state) => state.actions.updateSeachParamString
|
||||
)
|
||||
const intl = useIntl()
|
||||
const searchParams = useSearchParams()
|
||||
const pathname = usePathname()
|
||||
|
||||
const errorCode = searchParams.get("errorCode")
|
||||
const errorMessage = getErrorMessage(errorCode)
|
||||
const severityLevel =
|
||||
errorCode === BookingErrorCodeEnum.TransactionCancelled
|
||||
? AlertTypeEnum.Warning
|
||||
: AlertTypeEnum.Alarm
|
||||
|
||||
const [showAlert, setShowAlert] = useState(!!errorCode)
|
||||
|
||||
function getErrorMessage(errorCode: string | null) {
|
||||
switch (errorCode) {
|
||||
case BookingErrorCodeEnum.TransactionCancelled:
|
||||
return intl.formatMessage({
|
||||
id: "You have now cancelled your payment.",
|
||||
})
|
||||
default:
|
||||
return intl.formatMessage({
|
||||
id: "We had an issue processing your booking. Please try again. No charges have been made.",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function discardAlert() {
|
||||
setShowAlert(false)
|
||||
const queryParams = new URLSearchParams(searchParams.toString())
|
||||
queryParams.delete("errorCode")
|
||||
updateSearchParams(queryParams.toString())
|
||||
|
||||
window.history.replaceState({}, "", `${pathname}?${queryParams.toString()}`)
|
||||
}
|
||||
|
||||
return { showAlert, errorMessage, severityLevel, discardAlert, setShowAlert }
|
||||
}
|
||||
|
||||
interface PaymentAlertProps {
|
||||
isVisible?: boolean
|
||||
}
|
||||
|
||||
export default function PaymentAlert({ isVisible = false }: PaymentAlertProps) {
|
||||
const { showAlert, errorMessage, severityLevel, discardAlert, setShowAlert } =
|
||||
useBookingErrorAlert()
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
setShowAlert(true)
|
||||
}
|
||||
}, [isVisible, setShowAlert])
|
||||
|
||||
if (!showAlert) return null
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Alert
|
||||
type={severityLevel}
|
||||
variant="inline"
|
||||
text={errorMessage}
|
||||
close={discardAlert}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.wrapper {
|
||||
margin-top: var(--Spacing-x3);
|
||||
max-width: min(100%, 620px);
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useEffect } from "react"
|
||||
|
||||
import { PaymentCallbackStatusEnum } from "@/constants/booking"
|
||||
import { detailsStorageName } from "@/stores/enter-details"
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
@@ -11,7 +12,7 @@ import { convertObjToSearchParams } from "@/utils/url"
|
||||
|
||||
import type { PersistedState } from "@/types/stores/enter-details"
|
||||
|
||||
export default function PaymentCallback({
|
||||
export default function HandleErrorCallback({
|
||||
returnUrl,
|
||||
searchObject,
|
||||
status,
|
||||
@@ -19,7 +20,7 @@ export default function PaymentCallback({
|
||||
}: {
|
||||
returnUrl: string
|
||||
searchObject: URLSearchParams
|
||||
status: "error" | "success" | "cancel"
|
||||
status: PaymentCallbackStatusEnum
|
||||
errorMessage?: string
|
||||
}) {
|
||||
const router = useRouter()
|
||||
@@ -34,14 +35,14 @@ export default function PaymentCallback({
|
||||
searchObject
|
||||
)
|
||||
|
||||
if (status === "cancel") {
|
||||
if (status === PaymentCallbackStatusEnum.Cancel) {
|
||||
trackPaymentEvent({
|
||||
event: "paymentCancel",
|
||||
hotelId: detailsStorage.booking.hotelId,
|
||||
status: "cancelled",
|
||||
})
|
||||
}
|
||||
if (status === "error") {
|
||||
if (status === PaymentCallbackStatusEnum.Error) {
|
||||
trackPaymentEvent({
|
||||
event: "paymentFail",
|
||||
hotelId: detailsStorage.booking.hotelId,
|
||||
@@ -0,0 +1,68 @@
|
||||
"use client"
|
||||
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useEffect } from "react"
|
||||
|
||||
import { BookingStatusEnum, MEMBERSHIP_FAILED_ERROR } from "@/constants/booking"
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"
|
||||
|
||||
import TimeoutSpinner from "./TimeoutSpinner"
|
||||
|
||||
const validBookingStatuses = [
|
||||
BookingStatusEnum.PaymentSucceeded,
|
||||
BookingStatusEnum.BookingCompleted,
|
||||
]
|
||||
|
||||
interface HandleStatusPollingProps {
|
||||
confirmationNumber: string
|
||||
successRedirectUrl: string
|
||||
}
|
||||
|
||||
export default function HandleSuccessCallback({
|
||||
confirmationNumber,
|
||||
successRedirectUrl,
|
||||
}: HandleStatusPollingProps) {
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
data: bookingStatus,
|
||||
error,
|
||||
isTimeout,
|
||||
} = useHandleBookingStatus({
|
||||
confirmationNumber,
|
||||
expectedStatuses: validBookingStatuses,
|
||||
maxRetries: 10,
|
||||
retryInterval: 2000,
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!bookingStatus?.reservationStatus) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
validBookingStatuses.includes(
|
||||
bookingStatus.reservationStatus as BookingStatusEnum
|
||||
)
|
||||
) {
|
||||
// a successful booking can still have membership errors
|
||||
const membershipFailedError = bookingStatus.errors.find(
|
||||
(e) => e.errorCode === MEMBERSHIP_FAILED_ERROR
|
||||
)
|
||||
const errorParam = membershipFailedError
|
||||
? `&errorCode=${membershipFailedError.errorCode}`
|
||||
: ""
|
||||
|
||||
router.replace(`${successRedirectUrl}${errorParam}`)
|
||||
}
|
||||
}, [bookingStatus, successRedirectUrl, router])
|
||||
|
||||
if (isTimeout || error) {
|
||||
return <TimeoutSpinner />
|
||||
}
|
||||
|
||||
return <LoadingSpinner fullPage />
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { customerService } from "@/constants/currentWebHrefs"
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./timeoutSpinner.module.css"
|
||||
|
||||
export default function TimeoutSpinner() {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<LoadingSpinner />
|
||||
<Subtitle className={styles.heading}>
|
||||
{intl.formatMessage({ id: "Taking longer than usual" })}
|
||||
</Subtitle>
|
||||
<Body textAlign="center" className={styles.messageContainer}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "We are still confirming your booking. This is usually a matter of minutes and we do apologise for the wait. Please check your inbox for a booking confirmation email and if you still haven't received it by end of day, please contact our <link>customer support</link>.",
|
||||
},
|
||||
{
|
||||
link: (text) => (
|
||||
<Link
|
||||
href={customerService[lang]}
|
||||
variant="underscored"
|
||||
target="_blank"
|
||||
>
|
||||
{text}
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: var(--Spacing-x2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container .heading {
|
||||
margin-bottom: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.messageContainer {
|
||||
max-width: 435px;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -13,10 +13,6 @@ import {
|
||||
PAYMENT_METHOD_TITLES,
|
||||
PaymentMethodEnum,
|
||||
} from "@/constants/booking"
|
||||
import {
|
||||
bookingTermsAndConditions,
|
||||
privacyPolicy,
|
||||
} from "@/constants/currentWebHrefs"
|
||||
import {
|
||||
bookingConfirmation,
|
||||
selectRate,
|
||||
@@ -27,15 +23,10 @@ import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||
import { useAvailablePaymentOptions } from "@/hooks/booking/useAvailablePaymentOptions"
|
||||
import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"
|
||||
import { usePaymentFailedToast } from "@/hooks/booking/usePaymentFailedToast"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { trackPaymentEvent } from "@/utils/tracking"
|
||||
|
||||
@@ -46,8 +37,10 @@ import GuaranteeDetails from "./GuaranteeDetails"
|
||||
import { hasFlexibleRate, hasPrepaidRate, isPaymentMethodEnum } from "./helpers"
|
||||
import MixedRatePaymentBreakdown from "./MixedRatePaymentBreakdown"
|
||||
import MySavedCards from "./MySavedCards"
|
||||
import PaymentAlert from "./PaymentAlert"
|
||||
import PaymentOption from "./PaymentOption"
|
||||
import { type PaymentFormData, paymentSchema } from "./schema"
|
||||
import TermsAndConditions from "./TermsAndConditions"
|
||||
|
||||
import styles from "./payment.module.css"
|
||||
|
||||
@@ -74,6 +67,8 @@ export default function PaymentClient({
|
||||
const intl = useIntl()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const [showPaymentAlert, setShowPaymentAlert] = useState(false)
|
||||
|
||||
const { booking, canProceedToPayment, rooms, totalPrice } =
|
||||
useEnterDetailsStore((state) => ({
|
||||
booking: state.booking,
|
||||
@@ -109,8 +104,6 @@ export default function PaymentClient({
|
||||
const hasFlexRates = rooms.some(hasFlexibleRate)
|
||||
const hasMixedRates = hasPrepaidRates && hasFlexRates
|
||||
|
||||
usePaymentFailedToast()
|
||||
|
||||
const methods = useForm<PaymentFormData>({
|
||||
defaultValues: {
|
||||
paymentMethod: savedCreditCards?.length
|
||||
@@ -181,7 +174,7 @@ export default function PaymentClient({
|
||||
|
||||
const bookingStatus = useHandleBookingStatus({
|
||||
confirmationNumber: bookingNumber,
|
||||
expectedStatus: BookingStatusEnum.BookingCompleted,
|
||||
expectedStatuses: [BookingStatusEnum.BookingCompleted],
|
||||
maxRetries,
|
||||
retryInterval,
|
||||
enabled: isPollingForBookingStatus,
|
||||
@@ -189,11 +182,8 @@ export default function PaymentClient({
|
||||
|
||||
const handlePaymentError = useCallback(
|
||||
(errorMessage: string) => {
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "We had an issue processing your booking. Please try again. No charges have been made.",
|
||||
})
|
||||
)
|
||||
setShowPaymentAlert(true)
|
||||
|
||||
const currentPaymentMethod = methods.getValues("paymentMethod")
|
||||
const smsEnable = methods.getValues("smsConfirmation")
|
||||
const isSavedCreditCard = savedCreditCards?.some(
|
||||
@@ -210,7 +200,7 @@ export default function PaymentClient({
|
||||
status: "failed",
|
||||
})
|
||||
},
|
||||
[intl, methods, savedCreditCards, hotelId]
|
||||
[methods, savedCreditCards, hotelId]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -396,6 +386,7 @@ export default function PaymentClient({
|
||||
? confirm
|
||||
: payment}
|
||||
</Title>
|
||||
<PaymentAlert isVisible={showPaymentAlert} />
|
||||
</header>
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
@@ -466,49 +457,7 @@ export default function PaymentClient({
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<Caption>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.",
|
||||
},
|
||||
{
|
||||
termsAndConditionsLink: (str) => (
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="underscored"
|
||||
href={bookingTermsAndConditions[lang]}
|
||||
target="_blank"
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
privacyPolicyLink: (str) => (
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="underscored"
|
||||
href={privacyPolicy[lang]}
|
||||
target="_blank"
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
<Checkbox name="termsAndConditions">
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
id: "I accept the terms and conditions",
|
||||
})}
|
||||
</Caption>
|
||||
</Checkbox>
|
||||
<Checkbox name="smsConfirmation">
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
id: "I would like to get my booking confirmation via sms",
|
||||
})}
|
||||
</Caption>
|
||||
</Checkbox>
|
||||
<TermsAndConditions />
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import {
|
||||
bookingTermsAndConditions,
|
||||
privacyPolicy,
|
||||
} from "@/constants/currentWebHrefs"
|
||||
|
||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "../payment.module.css"
|
||||
|
||||
export default function TermsAndConditions() {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Caption>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.",
|
||||
},
|
||||
{
|
||||
termsAndConditionsLink: (str) => (
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="underscored"
|
||||
href={bookingTermsAndConditions[lang]}
|
||||
target="_blank"
|
||||
weight="bold"
|
||||
size="small"
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
privacyPolicyLink: (str) => (
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="underscored"
|
||||
href={privacyPolicy[lang]}
|
||||
target="_blank"
|
||||
weight="bold"
|
||||
size="small"
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
<Checkbox name="termsAndConditions">
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
id: "I accept the terms and conditions",
|
||||
})}
|
||||
</Caption>
|
||||
</Checkbox>
|
||||
<Checkbox name="smsConfirmation">
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
id: "I would like to get my booking confirmation via sms",
|
||||
})}
|
||||
</Caption>
|
||||
</Checkbox>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -98,7 +98,7 @@ export default function GuaranteeLateArrival({
|
||||
|
||||
const bookingStatus = useHandleBookingStatus({
|
||||
confirmationNumber: booking.confirmationNumber,
|
||||
expectedStatus: BookingStatusEnum.BookingCompleted,
|
||||
expectedStatuses: [BookingStatusEnum.BookingCompleted],
|
||||
maxRetries,
|
||||
retryInterval,
|
||||
enabled: isPollingForBookingStatus,
|
||||
@@ -195,7 +195,6 @@ export default function GuaranteeLateArrival({
|
||||
{
|
||||
termsAndConditionsLink: (str) => (
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="underscored"
|
||||
href={bookingTermsAndConditions[lang]}
|
||||
target="_blank"
|
||||
@@ -205,7 +204,6 @@ export default function GuaranteeLateArrival({
|
||||
),
|
||||
privacyPolicyLink: (str) => (
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="underscored"
|
||||
href={privacyPolicy[lang]}
|
||||
target="_blank"
|
||||
|
||||
Reference in New Issue
Block a user