@graphcommerce/magento-cart 8.1.0-canary.9 → 9.0.0-canary.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/Api/CartItemCountChanged.graphql +1 -1
  2. package/CHANGELOG.md +247 -54
  3. package/Config.graphqls +17 -0
  4. package/components/ApolloCartError/ApolloCartErrorAlert.tsx +1 -46
  5. package/components/CartAddress/CartAddress.graphql +1 -0
  6. package/components/CartAgreementsForm/CartAgreementsForm.tsx +60 -33
  7. package/components/CartFab/CartFab.tsx +8 -2
  8. package/components/CartStartCheckout/CartStartCheckout.graphql +1 -1
  9. package/components/CartStartCheckout/CartStartCheckout.tsx +23 -7
  10. package/components/CartStartCheckout/CartStartCheckoutLinkOrButton.tsx +4 -2
  11. package/components/CartTotals/CartTotals.graphql +8 -4
  12. package/components/CartTotals/CartTotals.tsx +9 -6
  13. package/components/EmptyCart/EmptyCart.tsx +8 -4
  14. package/components/InlineAccount/InlineAccount.tsx +6 -6
  15. package/components/OrderSucces/OrderSuccesPage.graphql +1 -1
  16. package/hooks/index.ts +5 -3
  17. package/hooks/useCartPermissions.ts +21 -0
  18. package/hooks/useCartQuery.ts +24 -3
  19. package/hooks/useCheckoutPermissions.ts +21 -0
  20. package/hooks/useFormGqlMutationCart.ts +40 -3
  21. package/index.ts +4 -3
  22. package/link/cartLink.ts +127 -0
  23. package/link/isProtectedCartOperation.ts +5 -0
  24. package/package.json +16 -15
  25. package/plugins/MagentoCartGraphqlProvider.tsx +15 -3
  26. package/plugins/useSignInFormMergeCart.ts +8 -7
  27. package/typePolicies.ts +1 -0
  28. package/utils/cartPermissions.ts +23 -0
  29. package/utils/checkoutPermissions.ts +13 -0
  30. package/utils/index.ts +2 -0
  31. package/hooks/CurrentCartId.graphqls +0 -12
  32. package/link/createCartErrorLink.ts +0 -71
@@ -11,6 +11,7 @@ import { i18n } from '@lingui/core'
11
11
  import { alpha, Fab, FabProps, styled, useTheme, Box, SxProps, Theme } from '@mui/material'
12
12
  import { m, useTransform } from 'framer-motion'
13
13
  import React from 'react'
14
+ import { useCartEnabled, useCartShouldLoginToContinue } from '../../hooks'
14
15
  import { useCartQuery } from '../../hooks/useCartQuery'
15
16
  import { CartFabDocument } from './CartFab.gql'
16
17
  import { CartTotalQuantityFragment } from './CartTotalQuantity.gql'
@@ -24,7 +25,7 @@ type CartFabContentProps = CartFabProps & CartTotalQuantityFragment
24
25
 
25
26
  const MotionDiv = styled(m.div)({})
26
27
 
27
- const MotionFab = m(
28
+ const MotionFab = m.create(
28
29
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
30
  React.forwardRef<any, Omit<FabProps, 'style' | 'onDrag'>>((props, ref) => (
30
31
  <Fab {...props} ref={ref} />
@@ -100,7 +101,12 @@ function CartFabContent(props: CartFabContentProps) {
100
101
  }
101
102
 
102
103
  export function CartFab(props: CartFabProps) {
103
- const cartQuery = useCartQuery(CartFabDocument)
104
+ const cartEnabled = useCartEnabled()
105
+ const shouldLoginToContinue = useCartShouldLoginToContinue()
106
+ const cartQuery = useCartQuery(CartFabDocument, {
107
+ skip: shouldLoginToContinue,
108
+ })
109
+ if (!cartEnabled) return null
104
110
 
105
111
  return (
106
112
  <WaitForQueries waitFor={cartQuery} fallback={<CartFabContent {...props} total_quantity={0} />}>
@@ -1,4 +1,4 @@
1
- fragment CartStartCheckout on Cart @injectable {
1
+ fragment CartStartCheckout on Cart {
2
2
  prices {
3
3
  grand_total {
4
4
  ...Money
@@ -1,8 +1,9 @@
1
1
  import { Money } from '@graphcommerce/magento-store'
2
2
  import { iconChevronRight, IconSvg, extendableComponent } from '@graphcommerce/next-ui'
3
- import { Trans } from '@lingui/react'
4
- import { Box, Button, ButtonProps, SxProps, Theme } from '@mui/material'
3
+ import { Trans } from '@lingui/macro'
4
+ import { Box, Button, ButtonProps, Link, SxProps, Theme } from '@mui/material'
5
5
  import React from 'react'
6
+ import { useCheckoutShouldLoginToContinue } from '../../hooks'
6
7
  import { CartStartCheckoutFragment } from './CartStartCheckout.gql'
7
8
 
8
9
  export type CartStartCheckoutProps = {
@@ -17,12 +18,13 @@ export type CartStartCheckoutProps = {
17
18
  ) => void
18
19
  }
19
20
 
20
- const name = 'CartStartCheckout' as const
21
+ const name = 'CartStartCheckout'
21
22
  const parts = [
22
23
  'checkoutButtonContainer',
23
24
  'checkoutButton',
24
25
  'checkoutButtonTotal',
25
26
  'checkoutMoney',
27
+ 'loginContainer',
26
28
  ] as const
27
29
  const { classes } = extendableComponent(name, parts)
28
30
 
@@ -36,6 +38,7 @@ export function CartStartCheckout(props: CartStartCheckoutProps) {
36
38
  cart,
37
39
  } = props
38
40
 
41
+ const shouldLoginToContinue = useCheckoutShouldLoginToContinue()
39
42
  const hasTotals = (cart?.prices?.grand_total?.value ?? 0) > 0
40
43
  const hasErrors = cart?.items?.some((item) => (item?.errors?.length ?? 0) > 0)
41
44
 
@@ -43,10 +46,21 @@ export function CartStartCheckout(props: CartStartCheckoutProps) {
43
46
  <Box
44
47
  className={classes.checkoutButtonContainer}
45
48
  sx={[
46
- (theme) => ({ textAlign: 'center', my: theme.spacings.md }),
49
+ (theme) => ({
50
+ textAlign: 'center',
51
+ my: theme.spacings.md,
52
+ }),
47
53
  ...(Array.isArray(sx) ? sx : [sx]),
48
54
  ]}
49
55
  >
56
+ {shouldLoginToContinue && (
57
+ <Box sx={{ mb: 1 }} className={classes.loginContainer}>
58
+ <Link href='/account/signin'>
59
+ <Trans>You must first login before you can continue</Trans>
60
+ </Link>
61
+ </Box>
62
+ )}
63
+
50
64
  <Button
51
65
  href='/checkout'
52
66
  id='cart-start-checkout'
@@ -60,7 +74,7 @@ export function CartStartCheckout(props: CartStartCheckoutProps) {
60
74
  onStart?.(e, cart)
61
75
  return onClick?.(e)
62
76
  }}
63
- disabled={disabled || !hasTotals || hasErrors}
77
+ disabled={disabled || !hasTotals || hasErrors || shouldLoginToContinue}
64
78
  {...buttonProps}
65
79
  >
66
80
  <Box
@@ -71,7 +85,7 @@ export function CartStartCheckout(props: CartStartCheckoutProps) {
71
85
  '& ~ span.MuiButton-endIcon': { marginLeft: '6px' },
72
86
  })}
73
87
  >
74
- <Trans id='Start Checkout' />
88
+ <Trans>Start Checkout</Trans>
75
89
  </Box>{' '}
76
90
  {hasTotals && (
77
91
  <span className={classes.checkoutMoney}>
@@ -84,7 +98,9 @@ export function CartStartCheckout(props: CartStartCheckoutProps) {
84
98
 
85
99
  {hasErrors && (
86
100
  <Box sx={(theme) => ({ color: 'error.main', mt: theme.spacings.xs })}>
87
- <Trans id='Some items in your cart contain errors, please update or remove them, then try again.' />
101
+ <Trans>
102
+ Some items in your cart contain errors, please update or remove them, then try again.
103
+ </Trans>
88
104
  </Box>
89
105
  )}
90
106
  </Box>
@@ -2,6 +2,7 @@ import { iconChevronRight, IconSvg, LinkOrButton, LinkOrButtonProps } from '@gra
2
2
  import { Trans } from '@lingui/react'
3
3
  import { SxProps, Theme } from '@mui/material'
4
4
  import React from 'react'
5
+ import { useCheckoutShouldLoginToContinue } from '../../hooks'
5
6
  import { CartStartCheckoutFragment } from './CartStartCheckout.gql'
6
7
 
7
8
  export type CartStartCheckoutLinkOrButtonProps = {
@@ -18,13 +19,14 @@ export type CartStartCheckoutLinkOrButtonProps = {
18
19
 
19
20
  export function CartStartCheckoutLinkOrButton(props: CartStartCheckoutLinkOrButtonProps) {
20
21
  const {
21
- children,
22
22
  onStart,
23
23
  disabled,
24
24
  linkOrButtonProps: { onClick, button, ...linkOrButtonProps } = {},
25
25
  cart,
26
26
  } = props
27
27
 
28
+ const shouldLoginToContinue = useCheckoutShouldLoginToContinue()
29
+
28
30
  const hasTotals = (cart?.prices?.grand_total?.value ?? 0) > 0
29
31
  const hasErrors = cart?.items?.some((item) => (item?.errors?.length ?? 0) > 0)
30
32
 
@@ -38,7 +40,7 @@ export function CartStartCheckoutLinkOrButton(props: CartStartCheckoutLinkOrButt
38
40
  onStart?.(e, cart)
39
41
  }}
40
42
  button={{ variant: 'pill', ...button }}
41
- disabled={disabled || !hasTotals || hasErrors}
43
+ disabled={disabled || !hasTotals || hasErrors || shouldLoginToContinue}
42
44
  color='secondary'
43
45
  endIcon={<IconSvg src={iconChevronRight} />}
44
46
  {...linkOrButtonProps}
@@ -1,6 +1,4 @@
1
- fragment CartTotals on Cart
2
- @injectable
3
- @inject(into: ["CartItemCountChanged", "PaymentMethodUpdated"]) {
1
+ fragment CartTotals on Cart @inject(into: ["CartItemCountChanged", "PaymentMethodUpdated"]) {
4
2
  shipping_addresses {
5
3
  selected_shipping_method {
6
4
  carrier_code
@@ -10,8 +8,14 @@ fragment CartTotals on Cart
10
8
  amount {
11
9
  ...Money
12
10
  }
11
+ price_excl_tax {
12
+ ...Money
13
+ }
14
+ price_incl_tax {
15
+ ...Money
16
+ }
13
17
  }
14
- # todo: https://github.com/magento/magento2/pull/31322
18
+ # todo: When 245 is not supported anymore, remove available_shipping_methods from fragment.
15
19
  available_shipping_methods {
16
20
  carrier_code
17
21
  method_code
@@ -42,12 +42,15 @@ export function CartTotals(props: CartTotalsProps) {
42
42
  const { shipping_addresses, prices } = data.cart
43
43
  const shippingMethod = shipping_addresses?.[0]?.selected_shipping_method
44
44
 
45
- const shippingMethodPrices = shipping_addresses?.[0]?.available_shipping_methods?.find(
46
- (avail) =>
47
- (shippingMethod?.amount?.value ?? 0) > 0 &&
48
- avail?.carrier_code === shippingMethod?.carrier_code &&
49
- avail?.method_code === shippingMethod?.method_code,
50
- )
45
+ const shippingMethodPrices =
46
+ import.meta.graphCommerce.magentoVersion >= 246
47
+ ? shippingMethod
48
+ : shipping_addresses?.[0]?.available_shipping_methods?.find(
49
+ (avail) =>
50
+ (shippingMethod?.amount?.value ?? 0) > 0 &&
51
+ avail?.carrier_code === shippingMethod?.carrier_code &&
52
+ avail?.method_code === shippingMethod?.method_code,
53
+ )
51
54
 
52
55
  return (
53
56
  <Box
@@ -5,16 +5,19 @@ import {
5
5
  FullPageMessageProps,
6
6
  } from '@graphcommerce/next-ui'
7
7
  import { Trans } from '@lingui/react'
8
- import { Button } from '@mui/material'
8
+ import { Button, SxProps, Theme } from '@mui/material'
9
9
  import React from 'react'
10
10
 
11
- type EmptyCartProps = { children?: React.ReactNode } & Pick<FullPageMessageProps, 'button'>
11
+ type EmptyCartProps = {
12
+ children?: React.ReactNode
13
+ sx?: SxProps<Theme>
14
+ } & Pick<FullPageMessageProps, 'button' | 'disableMargin'>
15
+
12
16
  export function EmptyCart(props: EmptyCartProps) {
13
- const { children, button } = props
17
+ const { children, button, ...rest } = props
14
18
 
15
19
  return (
16
20
  <FullPageMessage
17
- sx={(theme) => ({ mt: { md: theme.spacings.md } })}
18
21
  title={<Trans id='Your cart is empty' />}
19
22
  icon={<IconSvg src={iconShoppingBag} size='xxl' />}
20
23
  button={
@@ -24,6 +27,7 @@ export function EmptyCart(props: EmptyCartProps) {
24
27
  </Button>
25
28
  )
26
29
  }
30
+ {...rest}
27
31
  >
28
32
  {children ?? <Trans id='Discover our collection and add items to your cart!' />}
29
33
  </FullPageMessage>
@@ -1,13 +1,12 @@
1
- import { useQuery } from '@graphcommerce/graphql'
2
1
  import {
3
2
  SignUpFormInline,
4
3
  IsEmailAvailableDocument,
5
4
  useCustomerSession,
6
5
  useGuestQuery,
6
+ useCustomerAccountCanSignIn,
7
7
  } from '@graphcommerce/magento-customer'
8
8
  import { Button, FormRow, extendableComponent } from '@graphcommerce/next-ui'
9
9
  import { Trans } from '@lingui/react'
10
- // eslint-disable-next-line @typescript-eslint/no-restricted-imports
11
10
  import { Box, SxProps, TextField, Theme, Typography } from '@mui/material'
12
11
  import React, { useState } from 'react'
13
12
  import { useCartQuery } from '../../hooks/useCartQuery'
@@ -23,13 +22,15 @@ export type InlineAccountProps = {
23
22
  sx?: SxProps<Theme>
24
23
  }
25
24
 
26
- const name = 'InlineAccount' as const
25
+ const name = 'InlineAccount'
27
26
  const parts = ['root', 'innerContainer', 'form', 'button', 'title'] as const
28
27
  const { classes } = extendableComponent(name, parts)
29
28
 
30
29
  export function InlineAccount(props: InlineAccountProps) {
31
30
  const { title, description, sx = [] } = props
32
31
 
32
+ const canLogin = useCustomerAccountCanSignIn()
33
+
33
34
  const [toggled, setToggled] = useState<boolean>(false)
34
35
 
35
36
  const { loading, data } = useCartQuery(InlineAccountDocument)
@@ -44,7 +45,7 @@ export function InlineAccount(props: InlineAccountProps) {
44
45
  const { firstname, lastname } = cart?.shipping_addresses?.[0] ?? {}
45
46
  const canSignUp = isEmailAvailableData?.isEmailAvailable?.is_email_available === true
46
47
 
47
- if (loggedIn || !canSignUp) return null
48
+ if (loggedIn || !canSignUp || !canLogin) return null
48
49
 
49
50
  return (
50
51
  <div>
@@ -101,8 +102,7 @@ export function InlineAccount(props: InlineAccountProps) {
101
102
  <FormRow>
102
103
  <TextField
103
104
  variant='outlined'
104
- type='email'
105
- label='Email'
105
+ label={<Trans id='Email address' />}
106
106
  value={cart?.email}
107
107
  InputProps={{
108
108
  readOnly: true,
@@ -1,4 +1,4 @@
1
- fragment OrderSuccesPage on Cart @injectable {
1
+ fragment OrderSuccesPage on Cart {
2
2
  ...CartItems
3
3
  ...CartTotals
4
4
  }
package/hooks/index.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  export * from './useAssignCurrentCartId'
2
- export * from './useCurrentCartId'
2
+ export * from './useCartIdCreate'
3
+ export * from './useCartPermissions'
3
4
  export * from './useCartQuery'
5
+ export * from './useCheckoutPermissions'
4
6
  export * from './useClearCurrentCartId'
5
- export * from './useCartIdCreate'
7
+ export * from './useCurrentCartId'
8
+ export * from './useDisplayInclTax'
6
9
  export * from './useFormGqlMutationCart'
7
10
  export * from './useMergeCustomerCart'
8
- export * from './useDisplayInclTax'
@@ -0,0 +1,21 @@
1
+ import { useCustomerSession } from '@graphcommerce/magento-customer/hooks/useCustomerSession'
2
+ import { useStorefrontConfig } from '@graphcommerce/next-ui'
3
+
4
+ function useCartPermissions() {
5
+ return (
6
+ useStorefrontConfig().permissions?.cart ??
7
+ import.meta.graphCommerce.permissions?.cart ??
8
+ 'ENABLED'
9
+ )
10
+ }
11
+
12
+ export function useCartEnabled() {
13
+ return useCartPermissions() !== 'DISABLED'
14
+ }
15
+
16
+ export function useCartShouldLoginToContinue() {
17
+ const { loggedIn } = useCustomerSession()
18
+ const permission = useCartPermissions()
19
+ if (permission === 'ENABLED') return false
20
+ return !loggedIn
21
+ }
@@ -1,5 +1,7 @@
1
- import { useQuery, TypedDocumentNode, QueryHookOptions } from '@graphcommerce/graphql'
1
+ import { useQuery, TypedDocumentNode, QueryHookOptions, ApolloError } from '@graphcommerce/graphql'
2
+ import { GraphQLError } from 'graphql'
2
3
  import { useRouter } from 'next/router'
4
+ import { useCartShouldLoginToContinue } from './useCartPermissions'
3
5
  import { useCurrentCartId } from './useCurrentCartId'
4
6
 
5
7
  /**
@@ -22,12 +24,14 @@ export function useCartQuery<Q, V extends { cartId: string; [index: string]: unk
22
24
  } = {},
23
25
  ) {
24
26
  const { allowUrl, ...queryOptions } = options
27
+
25
28
  const router = useRouter()
26
29
  const { currentCartId, locked } = useCurrentCartId()
27
30
 
28
31
  const urlCartId = router.query.cart_id
29
32
  const usingUrl = typeof urlCartId === 'string'
30
33
  const cartId = usingUrl ? urlCartId : currentCartId
34
+ const shouldLoginToContinue = useCartShouldLoginToContinue()
31
35
 
32
36
  if (usingUrl || locked) queryOptions.fetchPolicy = 'cache-only'
33
37
 
@@ -35,7 +39,24 @@ export function useCartQuery<Q, V extends { cartId: string; [index: string]: unk
35
39
  queryOptions.returnPartialData = true
36
40
 
37
41
  queryOptions.variables = { cartId, ...options?.variables } as V
38
- queryOptions.skip = queryOptions?.skip || !cartId
39
42
 
40
- return useQuery(document, queryOptions as QueryHookOptions<Q, V>)
43
+ const query = useQuery(document, {
44
+ ...(queryOptions as QueryHookOptions<Q, V>),
45
+ skip: queryOptions.skip || !cartId || shouldLoginToContinue,
46
+ })
47
+
48
+ if (shouldLoginToContinue && !queryOptions?.skip) {
49
+ return {
50
+ ...query,
51
+ error: new ApolloError({
52
+ graphQLErrors: [
53
+ new GraphQLError('Action can not be performed by the current user', {
54
+ extensions: { category: 'graphql-authorization' },
55
+ }),
56
+ ],
57
+ }),
58
+ }
59
+ }
60
+
61
+ return query
41
62
  }
@@ -0,0 +1,21 @@
1
+ import { useCustomerSession } from '@graphcommerce/magento-customer/hooks/useCustomerSession'
2
+ import { useStorefrontConfig } from '@graphcommerce/next-ui'
3
+
4
+ function useCheckoutPermission() {
5
+ return (
6
+ useStorefrontConfig().permissions?.checkout ??
7
+ import.meta.graphCommerce.permissions?.checkout ??
8
+ 'ENABLED'
9
+ )
10
+ }
11
+
12
+ export function useCheckoutGuestEnabled() {
13
+ return useCheckoutPermission() === 'ENABLED'
14
+ }
15
+
16
+ export function useCheckoutShouldLoginToContinue() {
17
+ const { loggedIn } = useCustomerSession()
18
+ const permission = useCheckoutPermission()
19
+ if (permission === 'ENABLED') return false
20
+ return !loggedIn
21
+ }
@@ -1,37 +1,74 @@
1
- import { MutationHookOptions, TypedDocumentNode, useApolloClient } from '@graphcommerce/graphql'
1
+ import {
2
+ ApolloError,
3
+ MutationHookOptions,
4
+ TypedDocumentNode,
5
+ useApolloClient,
6
+ } from '@graphcommerce/graphql'
2
7
  import {
3
8
  useFormGqlMutation,
4
9
  UseFormGqlMutationReturn,
5
10
  UseFormGraphQlOptions,
6
11
  } from '@graphcommerce/react-hook-form'
12
+ import { GraphQLError, Kind } from 'graphql'
13
+ import { isProtectedCartOperation } from '../link/isProtectedCartOperation'
7
14
  import { CurrentCartIdDocument } from './CurrentCartId.gql'
8
15
  import { useCartIdCreate } from './useCartIdCreate'
16
+ import { useCartShouldLoginToContinue } from './useCartPermissions'
9
17
 
10
18
  export function useFormGqlMutationCart<
11
19
  Q extends Record<string, unknown>,
12
20
  V extends { cartId: string; [index: string]: unknown },
13
21
  >(
14
22
  document: TypedDocumentNode<Q, V>,
15
- options: UseFormGraphQlOptions<Q, V> = {},
23
+ options: UseFormGraphQlOptions<Q, V> & { submitWhileLocked?: boolean } = {},
16
24
  operationOptions?: MutationHookOptions<Q, V>,
17
25
  ): UseFormGqlMutationReturn<Q, V> {
18
26
  const cartId = useCartIdCreate()
19
27
  const client = useApolloClient()
28
+ const shouldLoginToContinue = useCartShouldLoginToContinue()
29
+
30
+ let shouldBlockOperation = false
31
+ document.definitions.forEach((defenition) => {
32
+ if (defenition.kind === Kind.OPERATION_DEFINITION) {
33
+ shouldBlockOperation = !isProtectedCartOperation(defenition.name?.value ?? '')
34
+ }
35
+ })
20
36
 
21
37
  const result = useFormGqlMutation<Q, V>(
22
38
  document,
23
39
  {
24
40
  ...options,
25
41
  onBeforeSubmit: async (variables) => {
42
+ if (shouldLoginToContinue && shouldBlockOperation) {
43
+ return false
44
+ }
26
45
  const vars = { ...variables, cartId: await cartId() }
27
46
 
28
47
  const res = client.cache.readQuery({ query: CurrentCartIdDocument })
29
- if (res?.currentCartId?.locked) return false
48
+ if (!options.submitWhileLocked && res?.currentCartId?.locked) {
49
+ throw Error('Could not submit form, cart is locked')
50
+ // console.log('Could not submit form, cart is locked', res.currentCartId.locked)
51
+ // return false
52
+ }
53
+
30
54
  return options.onBeforeSubmit ? options.onBeforeSubmit(vars) : vars
31
55
  },
32
56
  },
33
57
  { errorPolicy: 'all', ...operationOptions },
34
58
  )
35
59
 
60
+ if (shouldLoginToContinue && result.formState.isSubmitted && shouldBlockOperation) {
61
+ return {
62
+ ...result,
63
+ error: new ApolloError({
64
+ graphQLErrors: [
65
+ new GraphQLError('Action can not be performed by the current user', {
66
+ extensions: { category: 'graphql-authorization' },
67
+ }),
68
+ ],
69
+ }),
70
+ }
71
+ }
72
+
36
73
  return result
37
74
  }
package/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './Api/CartItemCountChanged.gql'
2
- export * from './hooks'
3
2
  export * from './components'
4
- export * from './typePolicies'
5
- export * from './link/createCartErrorLink'
6
3
  export * from './components/CartDebugger/CartDebugger'
4
+ export * from './hooks'
5
+ export * from './link/cartLink'
6
+ export * from './typePolicies'
7
+ export * from './utils'
@@ -0,0 +1,127 @@
1
+ import { fromPromise, globalApolloClient, Operation } from '@graphcommerce/graphql'
2
+ import { ApolloLink, Observable, onError } from '@graphcommerce/graphql/apollo'
3
+ import { CustomerTokenDocument, getCustomerAccountCanSignIn } from '@graphcommerce/magento-customer'
4
+ import { PushRouter, pushWithPromise } from '@graphcommerce/magento-customer/link/customerLink'
5
+ import { ErrorCategory } from '@graphcommerce/magento-graphql'
6
+ import { t } from '@lingui/macro'
7
+ import { GraphQLError, GraphQLFormattedError } from 'graphql'
8
+ import { writeCartId } from '../hooks'
9
+ import { CreateEmptyCartDocument } from '../hooks/CreateEmptyCart.gql'
10
+ import { getCartEnabledForUser } from '../utils'
11
+ import { isProtectedCartOperation } from './isProtectedCartOperation'
12
+
13
+ type CartOperation = Operation & { variables: { cartId: string } }
14
+ function isCartOperation(operation: Operation): operation is CartOperation {
15
+ return typeof operation.variables.cartId === 'string'
16
+ }
17
+
18
+ function errorIsIncluded(errorPath: readonly (string | number)[] | undefined, keys: string[]) {
19
+ const error = errorPath?.join()
20
+ return keys.some((value) => value === error)
21
+ }
22
+
23
+ const cartErrorLink = onError(({ graphQLErrors, operation, forward }) => {
24
+ if (!globalApolloClient.current) return undefined
25
+
26
+ const client = globalApolloClient.current
27
+ const { cache } = client
28
+
29
+ if (!isCartOperation(operation) || !graphQLErrors) return undefined
30
+
31
+ const isErrorCategory = (err: GraphQLFormattedError, category: ErrorCategory) =>
32
+ err.extensions?.category === category
33
+
34
+ const isNoSuchEntityError = (err: GraphQLFormattedError) =>
35
+ isErrorCategory(err, 'graphql-no-such-entity') &&
36
+ errorIsIncluded(err.path, [
37
+ 'cart',
38
+ 'addProductsToCart',
39
+ /**
40
+ * These mutations can also throw the graphql-no-such-entity exception, however, we're not
41
+ * sure if it also throws for other types of entities.
42
+ */
43
+ // 'removeItemFromCart',
44
+ // 'setBillingAddressOnCart',
45
+ // 'setGuestEmailOnCart',
46
+ // 'setPaymentMethodOnCart',
47
+ // 'setShippingAddressesOnCart',
48
+ // 'setShippingMethodsOnCart',
49
+ // 'updateCartItems',
50
+ // 'applyCouponToCart',
51
+ // 'removeCouponFromCart'
52
+ ])
53
+ const cartErr = graphQLErrors.find((err) => isNoSuchEntityError(err))
54
+
55
+ if (!cartErr) return undefined
56
+
57
+ if (globalThis.location?.search) {
58
+ const urlParams = new URLSearchParams(window.location.search)
59
+ if (urlParams.get('cart_id')) return forward(operation)
60
+ }
61
+
62
+ return fromPromise(client?.mutate({ mutation: CreateEmptyCartDocument }))
63
+ .filter((value) => Boolean(value))
64
+ .flatMap((cartData) => {
65
+ const cartId = cartData.data?.createEmptyCart
66
+ if (!cartId) return forward(operation)
67
+
68
+ writeCartId(cache, cartId)
69
+ operation.variables = { ...operation.variables, cartId }
70
+
71
+ // retry the request, returning the new observable
72
+ return forward(operation)
73
+ })
74
+ })
75
+
76
+ const cartPermissionLink = (router: PushRouter) =>
77
+ new ApolloLink((operation, forward) => {
78
+ const { locale } = router
79
+ const { cache } = operation.getContext()
80
+
81
+ if (!isProtectedCartOperation(operation.operationName)) return forward(operation)
82
+
83
+ const check = () => Boolean(cache?.readQuery({ query: CustomerTokenDocument }))
84
+ if (getCartEnabledForUser(locale, check)) return forward(operation)
85
+
86
+ if (!getCustomerAccountCanSignIn(locale))
87
+ throw new Error(
88
+ 'Permission error: permissions.customerAccount is DISABLED, while permissions.cart is set to CUSTOMER_ONLY',
89
+ )
90
+
91
+ const oldHeaders = operation.getContext().headers
92
+ const signInAgainPromise = pushWithPromise(router, '/account/signin')
93
+
94
+ return fromPromise(signInAgainPromise).flatMap(() => {
95
+ const tokenQuery = cache?.readQuery({ query: CustomerTokenDocument })
96
+
97
+ if (tokenQuery?.customerToken?.valid) {
98
+ // Customer is authenticated, retrying request.
99
+ operation.setContext({
100
+ headers: {
101
+ ...oldHeaders,
102
+ authorization: `Bearer ${tokenQuery?.customerToken?.token}`,
103
+ },
104
+ })
105
+ return forward(operation)
106
+ }
107
+
108
+ return Observable.of({
109
+ data: null,
110
+ errors: [
111
+ new GraphQLError(t`Please login to add products to your cart`, {
112
+ extensions: { category: 'graphql-authorization' },
113
+ }),
114
+ ],
115
+ })
116
+ })
117
+ })
118
+
119
+ export const cartLink = (router: PushRouter) => {
120
+ const links = [cartErrorLink]
121
+
122
+ if (!(import.meta.graphCommerce.permissions?.cart === 'ENABLED')) {
123
+ links.push(cartPermissionLink(router))
124
+ }
125
+
126
+ return ApolloLink.from(links)
127
+ }
@@ -0,0 +1,5 @@
1
+ export function isProtectedCartOperation(name: string): boolean {
2
+ /* Todo: Determine what operations should be added here */
3
+ const mutations = ['AddProductsToCart', 'CreateEmptyCart']
4
+ return mutations.includes(name)
5
+ }