Skip to content

Commit

Permalink
fix(new-price-calculator): proper reset after adding offer to cart (#…
Browse files Browse the repository at this point in the history
…4860)

## Describe your changes

_Reset_ new price calculator after adding offer to cart

## Justify why they are needed

### The issue

As can be seen in the following video, when you navigate back to price calculator page after adding an offer to the cart you end up in a misleading state of price calculator which might make you add multiple insurances for the same dog. Observe that for being able to add multiple products to the same dog, the user would need to take some actions that would make a **different** offer to be added to the cart like changing deductible for example. Changing _start date_ doesn't causes that, that's why you can still see only one offer in your cart when _add to cart_ button get's clicked:

https://github.com/user-attachments/assets/3df963db-ccf4-48bc-a344-88d32489ca8e

The same occurs for non multi products as well - like rent. The difference tho is that you can only have a single product of that type in your cart in all scenarios. So repeating the steps for rent would show a error dialog, which makes sense.

### The solution

What this solution does is:

* Update new price calculator so it works the same way as the old one when it comes about price intent reseting: whenever a new product get's added to the cart we create a new price intent for that product.
* `PurchaseSummary` is not rendered as a step of `PurchaseFormV2`. Instead we alternate between then inside `PriceCalculatorCmsPageContent`. With that, reseting the price intent and synchronizing price calculator atoms which it would cause any issues with `PurchaseSummary`.
* Code scout: since now old and new price calculator have the same "reset" price intent logic we don't need `useResetPriceIntent` hook anymore so I'm deleting it.

https://github.com/user-attachments/assets/4e8c085f-ecde-4c10-b8d7-43d74120574b
  • Loading branch information
guilhermespopolin authored Oct 21, 2024
2 parents bc0c74a + 3ea0a2e commit 0736c3e
Show file tree
Hide file tree
Showing 9 changed files with 58 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { ComparisonTableModal } from './ComparisonTableModal'
import { DeductibleSelector } from './DeductibleSelector'
import { DiscountTooltip } from './DiscountTooltip/DiscountTooltip'
import { useDiscountTooltipProps } from './DiscountTooltip/useDiscountTooltipProps'
import { usePriceIntent, useResetPriceIntent } from './priceIntentAtoms'
import { usePriceIntent } from './priceIntentAtoms'
import { ProductTierSelector } from './ProductTierSelector'
import { useSelectedOffer } from './useSelectedOffer'
import { useTiersAndDeductibles } from './useTiersAndDeductibles'
Expand Down Expand Up @@ -149,14 +149,12 @@ export const OfferPresenter = memo((props: Props) => {
[priceIntent.offers],
)

const resetPriceIntent = useResetPriceIntent()
const handleAddToCart: MouseEventHandler = (event) => {
event.preventDefault()
datadogRum.addAction(`PriceCalculator AddToCart Cart`)
setAddToCartRedirect(AddToCartRedirect.Cart)
onSuccessRef.current = (addedProductOffer: ProductOfferFragment) => {
props.notifyProductAdded(addedProductOffer)
resetPriceIntent()
}
addToCart(selectedOffer.id)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import { atom, useAtom, useAtomValue, useSetAtom, useStore } from 'jotai'
import { atomFamily } from 'jotai/utils'
import { useCallback, useEffect } from 'react'
import { useProductData } from '@/components/ProductData/ProductDataProvider'
import {
priceTemplateAtom,
usePriceTemplate,
} from '@/components/ProductPage/PurchaseForm/priceTemplateAtom'
import { priceTemplateAtom } from '@/components/ProductPage/PurchaseForm/priceTemplateAtom'
import { useSelectedOffer } from '@/components/ProductPage/PurchaseForm/useSelectedOffer'
import { useCartEntryToReplace } from '@/components/ProductPage/useCartEntryToReplace'
import {
Expand All @@ -19,7 +16,7 @@ import {
import { setupForm } from '@/services/PriceCalculator/PriceCalculator.helpers'
import type { Form } from '@/services/PriceCalculator/PriceCalculator.types'
import { priceIntentServiceInitClientSide } from '@/services/priceIntent/PriceIntentService'
import { useShopSession, useShopSessionId } from '@/services/shopSession/ShopSessionContext'
import { useShopSession } from '@/services/shopSession/ShopSessionContext'
import { getOffersByPrice } from '@/utils/getOffersByPrice'
import { getAtomValueOrThrow } from '@/utils/jotaiUtils'

Expand Down Expand Up @@ -182,8 +179,7 @@ export const useSyncPriceIntentState = ({
} else {
// This is a workaround as 'priceIntentAtoms' is used for new and old price calculator.
// For the old price calculator, we need to created a new price intent if there's a cart entry
// for that price intent. When we remove support for the old price calculator, we'll be able to
// remove of the config paramenters that can be provided to 'useSyncPriceIntentState'
// for that price intent.
if (replacePriceIntentWhenCurrentIsAddedToCart) {
const cartPriceIntentIds = new Set(cart?.entries.map((item) => item.priceIntentId))
if (cartPriceIntentIds.has(savedId)) {
Expand Down Expand Up @@ -291,22 +287,3 @@ export const usePriceIntentId = (): string => {
}
return priceIntentId
}

export const useResetPriceIntent = () => {
const shopSessionId = useShopSessionId()
// When we start using TemplateV2 exclusively, we can remove this
// and retrieve the name from the template directly
const productName = useProductData().name
const apolloClient = useApolloClient()
const priceTemplate = usePriceTemplate()
const setCurrentPriceIntentId = useSetAtom(currentPriceIntentIdAtom)
const priceIntentService = priceIntentServiceInitClientSide(apolloClient)

return useCallback(() => {
if (shopSessionId == null) return
setCurrentPriceIntentId(null)
priceIntentService.create({ productName, priceTemplate, shopSessionId }).then((priceIntent) => {
setCurrentPriceIntentId(priceIntent.id)
})
}, [priceIntentService, setCurrentPriceIntentId, shopSessionId, priceTemplate, productName])
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { style } from '@vanilla-extract/css'
import { responsiveStyles, tokens, yStack } from 'ui'
import { HEADER_HEIGHT_DESKTOP } from '@/components/Header/Header.css'
import { HEADER_HEIGHT_MOBILE } from '@/components/Header/HeaderMenuMobile/HeaderMenuMobile.css'
import { CONTENT_MAX_WIDTH } from '@/features/priceCalculator/PurchaseFormV2/PurchaseFormV2.css'

// Offset for fixed formFooter
export const FORM_FOOTER = '7rem'
Expand Down Expand Up @@ -65,3 +66,18 @@ export const productHero = style({
},
}),
})

export const purchaseSummaryWrapper = style([
yStack({ alignItems: 'center', justifyContent: 'center' }),
{ height: '100%' },
])

export const purchaseSummary = style({
width: `min(100%, ${CONTENT_MAX_WIDTH})`,
marginInline: 'auto',
...responsiveStyles({
lg: {
width: `max(100%, ${CONTENT_MAX_WIDTH})`,
},
}),
})
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
'use client'

import { useAtomValue } from 'jotai'
import { priceCalculatorShowPurchaseSummaryAtom } from '@/features/priceCalculator/priceCalculatorAtoms'
import { PurchaseSummary } from '@/features/priceCalculator/PurchaseFormV2/PurchaseSummary/PurchaseSummary'
import { useResponsiveVariant } from '@/utils/useResponsiveVariant'
import { priceCalculatorStepAtom } from '../../priceCalculatorAtoms'
import { ProductHeroV2 } from '../../ProductHeroV2/ProductHeroV2'
import { PurchaseFormV2 } from '../../PurchaseFormV2/PurchaseFormV2'
import { CartToast } from './CartToast/CartToast'
Expand All @@ -11,13 +12,15 @@ import {
priceCalculatorSection,
productHero,
productHeroSection,
purchaseSummaryWrapper,
purchaseSummary,
} from './PriceCalculatorCmsPageContent.css'

export function PriceCalculatorCmsPageContent() {
const variant = useResponsiveVariant('lg')
const step = useAtomValue(priceCalculatorStepAtom)
const showPurchaseSummary = useAtomValue(priceCalculatorShowPurchaseSummaryAtom)

const showProductHero = variant === 'desktop' || step !== 'purchaseSummary'
const showProductHero = variant === 'desktop' || !showPurchaseSummary

return (
<div className={pageGrid}>
Expand All @@ -27,7 +30,13 @@ export function PriceCalculatorCmsPageContent() {
</section>
)}
<section className={priceCalculatorSection}>
<PurchaseFormV2 />
{showPurchaseSummary ? (
<div className={purchaseSummaryWrapper}>
<PurchaseSummary className={purchaseSummary} />
</div>
) : (
<PurchaseFormV2 />
)}
</section>
{/* We don't mount CartToast on mobile as we hide ShoppingCartMenuItem at that level */}
{variant === 'desktop' && <CartToast />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ import {
import { useTiersAndDeductibles } from '@/components/ProductPage/PurchaseForm/useTiersAndDeductibles'
import { useCartEntryToReplace } from '@/components/ProductPage/useCartEntryToReplace'
import { DiscountFieldContainer } from '@/components/ShopBreakdown/DiscountFieldContainer'
import { priceCalculatorStepAtom } from '@/features/priceCalculator/priceCalculatorAtoms'
import {
priceCalculatorStepAtom,
priceCalculatorShowPurchaseSummaryAtom,
} from '@/features/priceCalculator/priceCalculatorAtoms'
import { DeductibleSelectorV2 } from '@/features/priceCalculator/PurchaseFormV2/OfferPresenterV2/DeductibleSelectorV2/DeductibleSelectorV2'
import { ProductCardSmall } from '@/features/priceCalculator/PurchaseFormV2/OfferPresenterV2/ProductCardSmall/ProductCardSmall'
import { ProductTierSelectorV2 } from '@/features/priceCalculator/PurchaseFormV2/OfferPresenterV2/ProductTierSelectorV2/ProductTierSelectorV2'
Expand Down Expand Up @@ -160,8 +163,7 @@ function OfferSummary() {
const shopSessionId = useShopSessionIdOrThrow()
const selectedOffer = useSelectedOfferValueOrThrow()
const priceIntent = usePriceIntent()

const setPriceCalculatorStep = useSetAtom(priceCalculatorStepAtom)
const setShowPurchaseSummary = useSetAtom(priceCalculatorShowPurchaseSummaryAtom)

const entryToReplace = useCartEntryToReplace()
const tracking = useTracking()
Expand Down Expand Up @@ -191,9 +193,9 @@ function OfferSummary() {
const handleAddToCart: MouseEventHandler = async (event) => {
event.preventDefault()
await addToCart(selectedOffer.id)
setPriceCalculatorStep('purchaseSummary')
// Make sure user views "added to cart" notification and/or bundle discount banner
window.scrollTo({ top: 0, behavior: 'instant' })
setShowPurchaseSummary(true)
}

const productData = useProductData()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,3 @@ export const priceLoaderWrapper = style([
])

export const viewOffersWrapper = style([yStack(), centered, { gap: '2.75rem' }])

export const purchaseSummaryWrapper = style([
yStack({ alignItems: 'center', justifyContent: 'center' }),
{ height: '100%' },
])

export const purchaseSummary = style({
width: `min(100%, ${CONTENT_MAX_WIDTH})`,
marginInline: 'auto',
...responsiveStyles({
lg: {
width: `max(100%, ${CONTENT_MAX_WIDTH})`,
},
}),
})
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,10 @@ import {
} from '@/features/priceCalculator/priceCalculatorAtoms'
import { OfferPresenterV2 } from '@/features/priceCalculator/PurchaseFormV2/OfferPresenterV2/OfferPresenterV2'
import { InsuranceDataForm } from './InsuranceDataForm/InsuranceDataForm'
import {
centered,
priceLoaderWrapper,
viewOffersWrapper,
purchaseSummaryWrapper,
purchaseSummary,
} from './PurchaseFormV2.css'
import { PurchaseSummary } from './PurchaseSummary/PurchaseSummary'
import { centered, priceLoaderWrapper, viewOffersWrapper } from './PurchaseFormV2.css'

export function PurchaseFormV2() {
useSyncPriceIntentState()
useSyncPriceIntentState({ replacePriceIntentWhenCurrentIsAddedToCart: true })
useWarnOnPreloadedPriceIntentId()

const isReady = useIsPriceIntentStateReady()
Expand Down Expand Up @@ -57,12 +50,7 @@ export function PurchaseFormV2() {
<OfferPresenterV2 />
</div>
)
case 'purchaseSummary':
return (
<div className={purchaseSummaryWrapper}>
<PurchaseSummary className={purchaseSummary} />
</div>
)

default:
throw new Error(`Unexpected step: ${step}`)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import clsx from 'clsx'
import { useSetAtom } from 'jotai'
import { useTranslation } from 'next-i18next'
import { useCallback, useEffect } from 'react'
import { useEffect } from 'react'
import { Text, Card, Divider, Button, yStack } from 'ui'
import type { Banner } from '@/components/Banner/Banner.types'
import { ButtonNextLink } from '@/components/ButtonNextLink'
import { useSetGlobalBanner, useDismissBanner } from '@/components/GlobalBanner/globalBannerState'
import { Pillow } from '@/components/Pillow/Pillow'
import { useResetPriceIntent } from '@/components/ProductPage/PurchaseForm/priceIntentAtoms'
import { usePriceTemplate } from '@/components/ProductPage/PurchaseForm/priceTemplateAtom'
import { useSelectedOfferValueOrThrow } from '@/components/ProductPage/PurchaseForm/useSelectedOffer'
import { TextWithLink } from '@/components/TextWithLink'
Expand All @@ -16,6 +16,7 @@ import {
hasBundleDiscount,
hasCartItemsEligibleForBundleDiscount,
} from '@/features/bundleDiscount/bundleDiscount.utils'
import { priceCalculatorShowPurchaseSummaryAtom } from '@/features/priceCalculator/priceCalculatorAtoms'
import type { TemplateV2 } from '@/services/PriceCalculator/PriceCalculator.types'
import { useShopSessionValueOrThrow } from '@/services/shopSession/ShopSessionContext'
import { getOfferPrice } from '@/utils/getOfferPrice'
Expand All @@ -26,15 +27,20 @@ import { actions } from './PurchaseSummary.css'
export function PurchaseSummary({ className }: { className?: string }) {
const { t } = useTranslation('purchase-form')
const locale = useRoutingLocale()
// Selected offer is not price intent dependent but defined at page level. So even
// though at this point a new price intent is loaded the selected offer will remain
// referencing to an offer created by the previous price intent
const offer = useSelectedOfferValueOrThrow()
const priceTemplate = usePriceTemplate() as TemplateV2
const resetPriceIntent = useResetPriceIntent()
const setShowPurchaseSummary = useSetAtom(priceCalculatorShowPurchaseSummaryAtom)

useNotifyAboutBundleDiscounts()

const handleAddMore = useCallback(() => {
resetPriceIntent()
}, [resetPriceIntent])
const handleAddMore = () => {
// Price intent is reset whenever a product is added to the cart so the only thing
// we need to do in order to add another product is to close the purchase summary
setShowPurchaseSummary(false)
}

const showAddMoreButton = offer.product.multiple && priceTemplate.addMultiple

Expand Down Expand Up @@ -64,7 +70,7 @@ export function PurchaseSummary({ className }: { className?: string }) {
})}
</Button>
)}
<ButtonNextLink href={PageLink.checkout({ locale })} variant="primary">
<ButtonNextLink href={PageLink.checkout({ locale }).toRelative()} variant="primary">
{t('GO_TO_CART_LABEL')}
</ButtonNextLink>
<ButtonNextLink href={PageLink.store({ locale })} variant="ghost">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,7 @@ import {
} from '@/components/ProductPage/PurchaseForm/priceIntentAtoms'
import { getAtomValueOrThrow } from '@/utils/jotaiUtils'

type PriceCalculatorStep =
| 'loadingForm'
| 'fillForm'
| 'calculatingPrice'
| 'viewOffers'
| 'purchaseSummary'
type PriceCalculatorStep = 'loadingForm' | 'fillForm' | 'calculatingPrice' | 'viewOffers'

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const priceCalculatorStepAtomFamily = atomFamily((priceIntentId: string) =>
Expand Down Expand Up @@ -57,3 +52,5 @@ export const usePriceCalculatorDeductibleInfo = () => {
}

export const priceCalculatorShowEditSsnWarningAtom = atom(false)

export const priceCalculatorShowPurchaseSummaryAtom = atom(false)

0 comments on commit 0736c3e

Please sign in to comment.