Skip to content

Commit

Permalink
Merge pull request #2205 from graphcommerce-org/fix/customer-cart-ass…
Browse files Browse the repository at this point in the history
…ignment

Solve an issue where the user would be presented with the Session expired dialog when the user would be logging in during the checkout process.
  • Loading branch information
paales authored Feb 19, 2024
2 parents cf50472 + 5a62623 commit eedcd58
Show file tree
Hide file tree
Showing 25 changed files with 175 additions and 122 deletions.
5 changes: 5 additions & 0 deletions .changeset/late-cats-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphcommerce/ecommerce-ui': patch
---

`<WaitForQueries/>` will default to loading, restoring the previous behavior. This might introduce , this might introduce an additional spinner but prevents a flash where it is shown that there is no cart
7 changes: 7 additions & 0 deletions .changeset/mighty-humans-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@graphcommerce/magento-cart-shipping-address': patch
'@graphcommerce/magento-newsletter': patch
'@graphcommerce/magento-cart': patch
---

Deprecate the allowUrl option for useCartQuery, it was already enabled by default and should never be set to false.
6 changes: 6 additions & 0 deletions .changeset/nice-bugs-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@graphcommerce/magento-cart": patch
"@graphcommerce/magento-customer": patch
---

Solve an issue where the user would be presented with the Session expired dialog when the user would be logging in during the checkout process.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useIsomorphicLayoutEffect } from '@graphcommerce/framer-utils'
import { QueryResult } from '@graphcommerce/graphql'
import React, { startTransition, useEffect, useState } from 'react'

Expand All @@ -10,13 +11,13 @@ export type WaitForQueriesProps = {

/** Shows the fallback during: SSR, Hydration and Query Loading. */
export const WaitForQueries = (props: WaitForQueriesProps) => {
const { waitFor, fallback, children, noSsr = false } = props
const { waitFor, fallback, children, noSsr = true } = props

// Make sure the first render is always the same as the server.
// Make sure we we use startTransition to make sure we don't get into trouble with Suspense.
const [mounted, setMounted] = useState(!noSsr)
useEffect(() => {
if (noSsr) startTransition(() => setMounted(true))
if (noSsr) setMounted(true)
}, [noSsr])

// We are done when all queries either have data or an error.
Expand Down
10 changes: 7 additions & 3 deletions packages/magento-cart-payment-method/hooks/useCartLock.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useCurrentCartId } from '@graphcommerce/magento-cart'
import { useApolloClient } from '@graphcommerce/graphql'
import { cartLock, useCurrentCartId } from '@graphcommerce/magento-cart'
import { useUrlQuery } from '@graphcommerce/next-ui'
import { useEffect, useState } from 'react'

Expand All @@ -19,9 +20,10 @@ let justLocked = false
* Todo: Block all operations on the cart while the cart is being blocked.
*/
export function useCartLock<E extends CartLockState>() {
const { currentCartId } = useCurrentCartId()
const { currentCartId, locked } = useCurrentCartId()
const [queryState, setRouterQuery] = useUrlQuery<E>()
const [, setForceRender] = useState(0)
const client = useApolloClient()

useEffect(() => {
const pageshow = (e: PageTransitionEvent) => {
Expand All @@ -38,6 +40,7 @@ export function useCartLock<E extends CartLockState>() {
const lock = (params: Omit<E, 'locked' | 'cart_id'>) => {
if (!currentCartId) return undefined
justLocked = true
cartLock(client.cache, true)
return setRouterQuery({
locked: '1',
cart_id: currentCartId,
Expand All @@ -46,13 +49,14 @@ export function useCartLock<E extends CartLockState>() {
}

const unlock = async (params: Omit<E, 'locked' | 'cart_id' | 'method'>) => {
cartLock(client.cache, false)
await setRouterQuery({ cart_id: null, locked: null, method: null, ...params } as E)
return queryState
}

const resulting: Omit<E, 'locked'> & { locked: boolean; justLocked: boolean } = {
...queryState,
locked: queryState.locked === '1' || Boolean(queryState.PayerID),
locked: locked || queryState.locked === '1' || Boolean(queryState.PayerID),
justLocked,
}

Expand Down
18 changes: 4 additions & 14 deletions packages/magento-cart/components/CartDebugger/CartDebugger.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useApolloClient } from '@graphcommerce/graphql'
import { Button } from '@mui/material'
import { CurrentCartIdDocument } from '../../hooks/CurrentCartId.gql'
import { readCartId, writeCartId } from '../../hooks'

export function CartDebugger() {
const client = useApolloClient()
Expand All @@ -12,24 +12,14 @@ export function CartDebugger() {
variant='text'
size='small'
onClick={() => {
const currentCardId = client.readQuery({ query: CurrentCartIdDocument })
if (!currentCardId?.currentCartId) {
const currentCartId = readCartId(client.cache)
if (!currentCartId) {
// eslint-disable-next-line no-console
console.log('No customerToken available, nothing to break')
} else {
// eslint-disable-next-line no-console
console.log(`Changing current token to a random one`)

client.writeQuery({
query: CurrentCartIdDocument,
data: {
currentCartId: {
...currentCardId.currentCartId,
id: `${Math.random().toString(36).slice(2)}random-cardId`,
},
},
broadcast: true,
})
writeCartId(client.cache, `${Math.random().toString(36).slice(2)}random-cardId`)
}
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,7 @@ type OrderSummaryProps = ActionCardLayoutProps & {

export function CartItemSummary(props: OrderSummaryProps) {
const { sx = [], size, layout = 'list', itemProps, ...cardLayout } = props
const { data } = useCartQuery(CartItemSummaryDocument, {
allowUrl: true,
fetchPolicy: 'cache-only',
})
const { data } = useCartQuery(CartItemSummaryDocument, { fetchPolicy: 'cache-only' })

if (!data?.cart) return null

Expand Down Expand Up @@ -85,7 +82,7 @@ export function CartItemSummary(props: OrderSummaryProps) {
className={classes.scrollerContainer}
{...cardLayout}
>
{items?.filter(nonNullable).map((item) => (
{(items ?? []).filter(nonNullable).map((item) => (
<CartItemActionCard
readOnly
key={item.uid}
Expand Down
2 changes: 1 addition & 1 deletion packages/magento-cart/components/CartTotals/CartTotals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const { withState } = extendableComponent<OwnerProps, typeof name, typeof parts>
* @see https://github.com/magento/magento2/issues/33848
*/
export function CartTotals(props: CartTotalsProps) {
const { data } = useCartQuery(GetCartTotalsDocument, { allowUrl: true })
const { data } = useCartQuery(GetCartTotalsDocument)
const { containerMargin, additionalSubtotals, additionalTotals, sx = [] } = props

const classes = withState({ containerMargin })
Expand Down
1 change: 1 addition & 0 deletions packages/magento-cart/hooks/CurrentCartId.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ query CurrentCartId {
currentCartId @client {
__typename
id
locked
}
}
1 change: 1 addition & 0 deletions packages/magento-cart/hooks/CurrentCartId.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ extend type Query {

type CurrentCartId {
id: String
locked: Boolean
}

input RegisterCartIdInput {
Expand Down
1 change: 0 additions & 1 deletion packages/magento-cart/hooks/CustomerCart.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@ query CustomerCart {
customerCart {
id
__typename
...CartItemCountChanged
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
mutation UseMergeCustomerCart($sourceCartId: String!, $destinationCartId: String!) {
mutation MergeCarts($sourceCartId: String!, $destinationCartId: String!) {
mergeCarts(source_cart_id: $sourceCartId, destination_cart_id: $destinationCartId) {
__typename
id
...CartItemCountChanged
}
}
17 changes: 16 additions & 1 deletion packages/magento-cart/hooks/useAssignCurrentCartId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,26 @@ export const CART_ID_COOKIE = 'cart'
export function writeCartId(cache: ApolloCache<object>, id: string | null = null) {
cache.writeQuery({
query: CurrentCartIdDocument,
data: { currentCartId: { __typename: 'CurrentCartId', id } },
data: { currentCartId: { __typename: 'CurrentCartId', locked: false, id } },
broadcast: true,
})
}

export function readCartId(cache: ApolloCache<object>) {
return cache.readQuery({ query: CurrentCartIdDocument })?.currentCartId
}

export function cartLock(cache: ApolloCache<object>, locked: boolean) {
const currentCartId = cache.readQuery({ query: CurrentCartIdDocument })?.currentCartId
if (currentCartId?.id && currentCartId.locked !== locked) {
cache.writeQuery({
query: CurrentCartIdDocument,
data: { currentCartId: { ...currentCartId, locked } },
broadcast: true,
})
}
}

export function useAssignCurrentCartId() {
const { cache } = useApolloClient()

Expand Down
6 changes: 2 additions & 4 deletions packages/magento-cart/hooks/useCartIdCreate.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { useApolloClient } from '@graphcommerce/graphql'
import { i18n } from '@lingui/core'
import { CreateEmptyCartDocument } from './CreateEmptyCart.gql'
import { CurrentCartIdDocument } from './CurrentCartId.gql'
import { useAssignCurrentCartId } from './useAssignCurrentCartId'
import { readCartId, useAssignCurrentCartId } from './useAssignCurrentCartId'

export function useCartIdCreate() {
const client = useApolloClient()
const assignCurrentCartId = useAssignCurrentCartId()

return async (): Promise<string> => {
const currentCartId = client.cache.readQuery({ query: CurrentCartIdDocument })?.currentCartId
?.id
const currentCartId = readCartId(client.cache)?.id

if (currentCartId) return currentCartId

Expand Down
18 changes: 8 additions & 10 deletions packages/magento-cart/hooks/useCartQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,27 @@ import { useCurrentCartId } from './useCurrentCartId'
export function useCartQuery<Q, V extends { cartId: string; [index: string]: unknown }>(
document: TypedDocumentNode<Q, V>,
options: QueryHookOptions<Q, Omit<V, 'cartId'>> & {
/**
* @deprecated Not used anymore, when the cart_id is in the URL, it will always be used.
*/
allowUrl?: boolean
} = {},
) {
const { allowUrl = true, ...queryOptions } = options
const { allowUrl, ...queryOptions } = options
const router = useRouter()
const { currentCartId } = useCurrentCartId()
const { currentCartId, locked } = useCurrentCartId()

const urlCartId = router.query.cart_id
const usingUrl = allowUrl && typeof urlCartId === 'string'
const usingUrl = typeof urlCartId === 'string'
const cartId = usingUrl ? urlCartId : currentCartId

if (usingUrl) queryOptions.fetchPolicy = 'cache-first'
if (usingUrl || locked) queryOptions.fetchPolicy = 'cache-only'

if (usingUrl && typeof queryOptions.returnPartialData === 'undefined')
queryOptions.returnPartialData = true

queryOptions.variables = { cartId, ...options?.variables } as V
queryOptions.skip = queryOptions?.skip || !cartId

const result = useQuery(document, queryOptions as QueryHookOptions<Q, V>)

return {
...result,
// error: called && !currentCartId ? noCartError : result.error,
}
return useQuery(document, queryOptions as QueryHookOptions<Q, V>)
}
10 changes: 1 addition & 9 deletions packages/magento-cart/hooks/useClearCurrentCartId.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
import { useApolloClient } from '@graphcommerce/graphql'
import { cookie } from '@graphcommerce/next-ui'
import { CurrentCartIdDocument } from './CurrentCartId.gql'
import { CART_ID_COOKIE } from './useAssignCurrentCartId'

export function useClearCurrentCartId() {
const { cache } = useApolloClient()

return () => {
const id = cache.readQuery({ query: CurrentCartIdDocument })?.currentCartId?.id
if (!id) return

cache.writeQuery({
query: CurrentCartIdDocument,
data: { currentCartId: { __typename: 'CurrentCartId', id: null } },
broadcast: true,
})
cache.evict({ fieldName: 'currentCartId', broadcast: true })
cookie(CART_ID_COOKIE, null)
}
}
7 changes: 6 additions & 1 deletion packages/magento-cart/hooks/useCurrentCartId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,10 @@ export function useCurrentCartId<
V extends CurrentCartIdQueryVariables,
>(options: QueryHookOptions<Q, V> = {}) {
const queryResults = useQuery<Q, V>(CurrentCartIdDocument, options)
return { currentCartId: queryResults.data?.currentCartId?.id || '', ...queryResults }

return {
currentCartId: queryResults.data?.currentCartId?.id || '',
locked: queryResults.data?.currentCartId?.locked || false,
...queryResults,
}
}
41 changes: 2 additions & 39 deletions packages/magento-cart/hooks/useMergeCustomerCart.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,6 @@
import { useMutation } from '@graphcommerce/graphql'
import { useCustomerQuery } from '@graphcommerce/magento-customer'
import { useEffect } from 'react'
import { CustomerCartDocument } from './CustomerCart.gql'
import { UseMergeCustomerCartDocument } from './UseMergeCustomerCart.gql'
import { useAssignCurrentCartId } from './useAssignCurrentCartId'
import { useCurrentCartId } from './useCurrentCartId'

/**
* - Automatically assign the customer cart as the current cart
* - Merge the guest cart into the customer cart
* @deprecated Is replaced by the useSignInFormMergeCart plugin.
*/
export function useMergeCustomerCart() {
const { currentCartId } = useCurrentCartId()
const assignCurrentCartId = useAssignCurrentCartId()
const [merge] = useMutation(UseMergeCustomerCartDocument, { errorPolicy: 'all' })

const destinationCartId = useCustomerQuery(CustomerCartDocument, { fetchPolicy: 'network-only' })
?.data?.customerCart.id

useEffect(() => {
// If we don't have a customer cart, we're done
// If the vistor cart is the same as the customer cart, we're done
if (!destinationCartId || currentCartId === destinationCartId) return

// If the visitor has a guest cart, try merging it into the customer cart
if (currentCartId) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
merge({ variables: { sourceCartId: currentCartId, destinationCartId } })
// We're not handling exceptions here:
// If the merge returns an error, we'll use the customer cart without merging the guest cart.
.catch((e) => {
console.error('Error merging carts', e)
})
.finally(() => {
// Assign the customer cart as the new cart id
assignCurrentCartId(destinationCartId)
})
} else {
assignCurrentCartId(destinationCartId)
}
}, [assignCurrentCartId, destinationCartId, merge, currentCartId])
return null
}
50 changes: 50 additions & 0 deletions packages/magento-cart/plugins/useSignInFormMergeCart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useApolloClient } from '@graphcommerce/graphql'
import type { useSignInForm } from '@graphcommerce/magento-customer/hooks/useSignInForm'
import type { MethodPlugin } from '@graphcommerce/next-config'
import { cartLock, readCartId, useAssignCurrentCartId } from '../hooks'
import { CustomerCartDocument } from '../hooks/CustomerCart.gql'
import { MergeCartsDocument } from '../hooks/MergeCarts.gql'

export const func = 'useSignInForm'
export const exported = '@graphcommerce/magento-customer/hooks/useSignInForm'

const useSignInFormMergeCart: MethodPlugin<typeof useSignInForm> = (useSignInForm, options) => {
const client = useApolloClient()
const assignCurrentCartId = useAssignCurrentCartId()

return useSignInForm({
...options,
onComplete: async (data, variables) => {
await options.onComplete?.(data, variables)

cartLock(client.cache, true)

const destinationCartId = (
await client.query({
query: CustomerCartDocument,
fetchPolicy: 'network-only',
})
).data.customerCart.id

try {
const sourceCartId = readCartId(client.cache)?.id
if (sourceCartId && sourceCartId !== destinationCartId) {
await client.mutate({
mutation: MergeCartsDocument,
variables: { sourceCartId, destinationCartId },
})
}
} catch (error) {
console.error(
'Error merging carts, continuing without merging, this might cause issues.',
error,
)
} finally {
// Assign the customer cart as the new cart id
assignCurrentCartId(destinationCartId)
}
},
})
}

export const plugin = useSignInFormMergeCart
Loading

0 comments on commit eedcd58

Please sign in to comment.