diff --git a/apps/store/src/components/CartInventory/CartInventory.helpers.tsx b/apps/store/src/components/CartInventory/CartInventory.helpers.tsx index 80d77aa5a1..e853d34279 100644 --- a/apps/store/src/components/CartInventory/CartInventory.helpers.tsx +++ b/apps/store/src/components/CartInventory/CartInventory.helpers.tsx @@ -10,32 +10,6 @@ import { Money } from '@/utils/formatter' import { useFormatter } from '@/utils/useFormatter' import { CartEntry } from './CartInventory.types' -export const useGetDiscountExplanation = () => { - const { t } = useTranslation('cart') - const formatter = useFormatter() - - return (discount: CampaignDiscount) => { - switch (discount.type) { - case CampaignDiscountType.MonthlyCost: - return `-${formatter.monthlyPrice(discount.amount)}` - - case CampaignDiscountType.FreeMonths: - return t('DISCOUNT_STATE_FREE_MONTHS', { count: discount.months }) - - case CampaignDiscountType.MonthlyPercentage: - return t('DISCOUNT_STATE_MONTHLY_PERCENTAGE', { - percentage: discount.percentage, - count: discount.months, - }) - - case CampaignDiscountType.IndefinitePercentage: - return t('DISCOUNT_STATE_INDEFINITE_PERCENTAGE', { - percentage: discount.percentage, - }) - } - } -} - export const useGetDiscountDurationExplanation = () => { const { t } = useTranslation('cart') const formatter = useFormatter() diff --git a/apps/store/src/components/CartInventory/CartInventory.tsx b/apps/store/src/components/CartInventory/CartInventory.tsx index 50d62779df..0b072e3774 100644 --- a/apps/store/src/components/CartInventory/CartInventory.tsx +++ b/apps/store/src/components/CartInventory/CartInventory.tsx @@ -2,13 +2,11 @@ import styled from '@emotion/styled' import { Space, theme } from 'ui' import { CampaignDiscountType, CartFragmentFragment } from '@/services/apollo/generated' import { convertToDate } from '@/utils/date' +import { useGetDiscountExplanation } from '@/utils/useDiscountExplanation' import { CampaignsSection } from './CampaignsSection' import { CartEntryItem } from './CartEntryItem/CartEntryItem' import { CartEntryList } from './CartEntryList' -import { - useGetDiscountDurationExplanation, - useGetDiscountExplanation, -} from './CartInventory.helpers' +import { useGetDiscountDurationExplanation } from './CartInventory.helpers' import { CostSummary } from './CostSummary' import { ReadOnlyCampaignCodeList } from './ReadOnlyCampaignCodeList' diff --git a/apps/store/src/components/ConfirmationPage/ConfirmationPage.stories.tsx b/apps/store/src/components/ConfirmationPage/ConfirmationPage.stories.tsx index 4c67a10a61..ab64fcd9be 100644 --- a/apps/store/src/components/ConfirmationPage/ConfirmationPage.stories.tsx +++ b/apps/store/src/components/ConfirmationPage/ConfirmationPage.stories.tsx @@ -100,6 +100,20 @@ const cart = { amount: 125, currencyCode: CurrencyCode.Sek, }, + cost: { + gross: { + amount: 125, + currencyCode: CurrencyCode.Sek, + }, + net: { + amount: 125, + currencyCode: CurrencyCode.Sek, + }, + discount: { + amount: 0, + currencyCode: CurrencyCode.Sek, + }, + }, cancellation: { option: ExternalInsuranceCancellationOption.None, requested: false, diff --git a/apps/store/src/components/ProductPage/PurchaseForm/OfferPresenter.tsx b/apps/store/src/components/ProductPage/PurchaseForm/OfferPresenter.tsx index 194a65df4f..a4265921ad 100644 --- a/apps/store/src/components/ProductPage/PurchaseForm/OfferPresenter.tsx +++ b/apps/store/src/components/ProductPage/PurchaseForm/OfferPresenter.tsx @@ -13,12 +13,14 @@ import { SpaceFlex } from '@/components/SpaceFlex/SpaceFlex' import { ExternalInsuranceCancellationOption, ProductOfferFragment, + RedeemedCampaign, } from '@/services/apollo/generated' import { PriceIntent } from '@/services/priceIntent/priceIntent.types' import { ShopSession } from '@/services/shopSession/ShopSession.types' import { useTracking } from '@/services/Tracking/useTracking' import { convertToDate } from '@/utils/date' import { PageLink } from '@/utils/PageLink' +import { useGetDiscountExplanation } from '@/utils/useDiscountExplanation' import { useFormatter } from '@/utils/useFormatter' import { CancellationForm, CancellationOption } from './CancellationForm/CancellationForm' import { ComparisonTableModal } from './ComparisonTableModal' @@ -51,6 +53,7 @@ export const OfferPresenter = (props: Props) => { const [, setSelectedOffer] = useSelectedOffer() const { t } = useTranslation('purchase-form') const formatter = useFormatter() + const getDiscountExplanation = useGetDiscountExplanation() const [addToCartRedirect, setAddToCartRedirect] = useState(null) const handleOfferChange = (offerId: string) => { @@ -100,7 +103,9 @@ export const OfferPresenter = (props: Props) => { const [handleUpdateCancellation, updateCancellationInfo] = useUpdateCancellation({ priceIntent }) - const displayPrice = formatter.monthlyPrice(selectedOffer.price) + const crossedOutPrice = + selectedOffer.cost.discount.amount > 0 ? formatter.monthlyPrice(selectedOffer.cost.gross) : null + const displayPrice = formatter.monthlyPrice(selectedOffer.cost.net) const cancellationOption = getCancellationOption({ priceIntent, @@ -111,28 +116,39 @@ export const OfferPresenter = (props: Props) => { loadingAddToCart || updateCancellationInfo.loading || updateStartDateResult.loading const discountTooltipProps = useMemo(() => { - if (!selectedOffer.priceMatch) return null + if (selectedOffer.priceMatch) { + const company = selectedOffer.priceMatch.externalInsurer.displayName + + if (selectedOffer.priceMatch.priceReduction.amount < 1) { + // No price reduction due to incomparable offers + const amount = formatter.monthlyPrice(selectedOffer.priceMatch.externalPrice) + return { + children: t('PRICE_MATCH_BUBBLE_INCOMPARABLE_TITLE', { amount, company }), + subtitle: t('PRICE_MATCH_BUBBLE_INCOMPARABLE_SUBTITLE'), + color: 'gray', + } as const + } - const company = selectedOffer.priceMatch.externalInsurer.displayName + const priceReduction = formatter.monthlyPrice(selectedOffer.priceMatch.priceReduction) - if (selectedOffer.priceMatch.priceReduction.amount < 1) { - // No price reduction due to incomparable offers - const amount = formatter.monthlyPrice(selectedOffer.priceMatch.externalPrice) return { - children: t('PRICE_MATCH_BUBBLE_INCOMPARABLE_TITLE', { amount, company }), - subtitle: t('PRICE_MATCH_BUBBLE_INCOMPARABLE_SUBTITLE'), - color: 'gray', + children: t('PRICE_MATCH_BUBBLE_SUCCESS_TITLE', { amount: priceReduction }), + subtitle: t('PRICE_MATCH_BUBBLE_SUCCESS_SUBTITLE', { company }), + color: 'green', } as const } - const priceReduction = formatter.monthlyPrice(selectedOffer.priceMatch.priceReduction) - - return { - children: t('PRICE_MATCH_BUBBLE_SUCCESS_TITLE', { amount: priceReduction }), - subtitle: t('PRICE_MATCH_BUBBLE_SUCCESS_SUBTITLE', { company }), - color: 'green', - } as const - }, [selectedOffer.priceMatch, formatter, t]) + const redeemedCampaign = shopSession.cart.redeemedCampaigns[0] as RedeemedCampaign | undefined + if (redeemedCampaign && selectedOffer.cost.discount.amount > 0) { + return { + children: getDiscountExplanation({ + ...redeemedCampaign.discount, + amount: selectedOffer.cost.discount, + }), + color: 'green', + } as const + } + }, [selectedOffer, formatter, getDiscountExplanation, shopSession, t]) const startDate = convertToDate(selectedOffer.startDate) @@ -182,9 +198,16 @@ export const OfferPresenter = (props: Props) => { {discountTooltipProps && } - - {displayPrice} - + + {crossedOutPrice && ( + + {crossedOutPrice} + + )} + + {displayPrice} + + diff --git a/apps/store/src/graphql/ProductOfferFragment.graphql b/apps/store/src/graphql/ProductOfferFragment.graphql index e403164eb4..26c08f7d89 100644 --- a/apps/store/src/graphql/ProductOfferFragment.graphql +++ b/apps/store/src/graphql/ProductOfferFragment.graphql @@ -26,6 +26,20 @@ fragment ProductOffer on ProductOffer { amount currencyCode } + cost { + gross { + amount + currencyCode + } + net { + amount + currencyCode + } + discount { + amount + currencyCode + } + } startDate cancellation { option diff --git a/apps/store/src/pages/cart.tsx b/apps/store/src/pages/cart.tsx index c411b01951..63aaf488e2 100644 --- a/apps/store/src/pages/cart.tsx +++ b/apps/store/src/pages/cart.tsx @@ -6,7 +6,6 @@ import { useMemo } from 'react' import { getCrossOut, useGetDiscountDurationExplanation, - useGetDiscountExplanation, getTotal, getCartEntry, } from '@/components/CartInventory/CartInventory.helpers' @@ -21,6 +20,7 @@ import { useShopSession } from '@/services/shopSession/ShopSessionContext' import { getGlobalStory } from '@/services/storyblok/storyblok' import { GLOBAL_STORY_PROP_NAME } from '@/services/storyblok/Storyblok.constant' import { isRoutingLocale } from '@/utils/l10n/localeUtils' +import { useGetDiscountExplanation } from '@/utils/useDiscountExplanation' const NextCartPage: NextPageWithLayout = (props) => { const { shopSession } = useShopSession() diff --git a/apps/store/src/pages/checkout/index.tsx b/apps/store/src/pages/checkout/index.tsx index 1f4bac3e87..07b3c297d3 100644 --- a/apps/store/src/pages/checkout/index.tsx +++ b/apps/store/src/pages/checkout/index.tsx @@ -7,7 +7,6 @@ import { getCrossOut, getTotal, useGetDiscountDurationExplanation, - useGetDiscountExplanation, } from '@/components/CartInventory/CartInventory.helpers' import { CheckoutStep } from '@/components/CheckoutHeader/Breadcrumbs' import { fetchCheckoutSteps } from '@/components/CheckoutHeader/CheckoutHeader.helpers' @@ -23,6 +22,7 @@ import { useShopSession } from '@/services/shopSession/ShopSessionContext' import { getShouldCollectEmail, getShouldCollectName } from '@/utils/customer' import { isRoutingLocale } from '@/utils/l10n/localeUtils' import { PageLink } from '@/utils/PageLink' +import { useGetDiscountExplanation } from '@/utils/useDiscountExplanation' type NextPageProps = Omit diff --git a/apps/store/src/utils/useDiscountExplanation.ts b/apps/store/src/utils/useDiscountExplanation.ts new file mode 100644 index 0000000000..5a5001ff8d --- /dev/null +++ b/apps/store/src/utils/useDiscountExplanation.ts @@ -0,0 +1,29 @@ +import { useTranslation } from 'next-i18next' +import { CampaignDiscount, CampaignDiscountType } from '@/services/apollo/generated' +import { useFormatter } from './useFormatter' + +export const useGetDiscountExplanation = () => { + const { t } = useTranslation('cart') + const formatter = useFormatter() + + return (discount: CampaignDiscount) => { + switch (discount.type) { + case CampaignDiscountType.MonthlyCost: + return `-${formatter.monthlyPrice(discount.amount)}` + + case CampaignDiscountType.FreeMonths: + return t('DISCOUNT_STATE_FREE_MONTHS', { count: discount.months }) + + case CampaignDiscountType.MonthlyPercentage: + return t('DISCOUNT_STATE_MONTHLY_PERCENTAGE', { + percentage: discount.percentage, + count: discount.months, + }) + + case CampaignDiscountType.IndefinitePercentage: + return t('DISCOUNT_STATE_INDEFINITE_PERCENTAGE', { + percentage: discount.percentage, + }) + } + } +} diff --git a/packages/ui/src/components/Text/Text.tsx b/packages/ui/src/components/Text/Text.tsx index fe61851a12..68044dbfd4 100644 --- a/packages/ui/src/components/Text/Text.tsx +++ b/packages/ui/src/components/Text/Text.tsx @@ -29,6 +29,7 @@ export type TextProps = { children?: ReactNode className?: string uppercase?: boolean + strikethrough?: boolean } const elementConfig = { @@ -38,11 +39,12 @@ const elementConfig = { export const TextBase = styled( Space, elementConfig, -)(({ align, color, size = 'md', uppercase = false }) => ({ +)(({ align, color, size = 'md', uppercase = false, strikethrough = false }) => ({ color: color ? theme.colors[color] : 'inherit', ...getFontSize(size), ...(align && { textAlign: align }), ...(uppercase && { textTransform: 'uppercase' }), + ...(strikethrough && { textDecorationLine: 'line-through' }), })) export const Text = ({ as, balance, children, className, ...rest }: TextProps) => (