@graphcommerce/magento-cart 8.1.0-canary.8 → 9.0.0-canary.100

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 (31) 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 +7 -1
  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/{createCartErrorLink.ts → cartLink.ts} +60 -4
  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
@@ -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'
@@ -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'
@@ -1,9 +1,14 @@
1
1
  import { fromPromise, globalApolloClient, Operation } from '@graphcommerce/graphql'
2
- import { onError } from '@graphcommerce/graphql/apollo'
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'
3
5
  import { ErrorCategory } from '@graphcommerce/magento-graphql'
4
- import type { GraphQLError } from 'graphql'
6
+ import { t } from '@lingui/macro'
7
+ import { GraphQLError } from 'graphql'
5
8
  import { writeCartId } from '../hooks'
6
9
  import { CreateEmptyCartDocument } from '../hooks/CreateEmptyCart.gql'
10
+ import { getCartEnabledForUser } from '../utils'
11
+ import { isProtectedCartOperation } from './isProtectedCartOperation'
7
12
 
8
13
  type CartOperation = Operation & { variables: { cartId: string } }
9
14
  function isCartOperation(operation: Operation): operation is CartOperation {
@@ -15,7 +20,7 @@ function errorIsIncluded(errorPath: readonly (string | number)[] | undefined, ke
15
20
  return keys.some((value) => value === error)
16
21
  }
17
22
 
18
- export const cartErrorLink = onError(({ graphQLErrors, operation, forward }) => {
23
+ const cartErrorLink = onError(({ graphQLErrors, operation, forward }) => {
19
24
  if (!globalApolloClient.current) return undefined
20
25
 
21
26
  const client = globalApolloClient.current
@@ -68,4 +73,55 @@ export const cartErrorLink = onError(({ graphQLErrors, operation, forward }) =>
68
73
  })
69
74
  })
70
75
 
71
- export const createCartErrorLink = () => cartErrorLink
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
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@graphcommerce/magento-cart",
3
3
  "homepage": "https://www.graphcommerce.org/",
4
4
  "repository": "github:graphcommerce-org/graphcommerce",
5
- "version": "8.1.0-canary.8",
5
+ "version": "9.0.0-canary.100",
6
6
  "sideEffects": false,
7
7
  "prettier": "@graphcommerce/prettier-config-pwa",
8
8
  "eslintConfig": {
@@ -12,25 +12,26 @@
12
12
  }
13
13
  },
14
14
  "peerDependencies": {
15
- "@graphcommerce/ecommerce-ui": "^8.1.0-canary.8",
16
- "@graphcommerce/eslint-config-pwa": "^8.1.0-canary.8",
17
- "@graphcommerce/framer-next-pages": "^8.1.0-canary.8",
18
- "@graphcommerce/framer-scroller": "^8.1.0-canary.8",
19
- "@graphcommerce/framer-utils": "^8.1.0-canary.8",
20
- "@graphcommerce/graphql": "^8.1.0-canary.8",
21
- "@graphcommerce/image": "^8.1.0-canary.8",
22
- "@graphcommerce/magento-customer": "^8.1.0-canary.8",
23
- "@graphcommerce/magento-graphql": "^8.1.0-canary.8",
24
- "@graphcommerce/magento-store": "^8.1.0-canary.8",
25
- "@graphcommerce/next-ui": "^8.1.0-canary.8",
26
- "@graphcommerce/prettier-config-pwa": "^8.1.0-canary.8",
27
- "@graphcommerce/react-hook-form": "^8.1.0-canary.8",
28
- "@graphcommerce/typescript-config-pwa": "^8.1.0-canary.8",
15
+ "@graphcommerce/ecommerce-ui": "^9.0.0-canary.100",
16
+ "@graphcommerce/eslint-config-pwa": "^9.0.0-canary.100",
17
+ "@graphcommerce/framer-next-pages": "^9.0.0-canary.100",
18
+ "@graphcommerce/framer-scroller": "^9.0.0-canary.100",
19
+ "@graphcommerce/framer-utils": "^9.0.0-canary.100",
20
+ "@graphcommerce/graphql": "^9.0.0-canary.100",
21
+ "@graphcommerce/image": "^9.0.0-canary.100",
22
+ "@graphcommerce/magento-customer": "^9.0.0-canary.100",
23
+ "@graphcommerce/magento-graphql": "^9.0.0-canary.100",
24
+ "@graphcommerce/magento-store": "^9.0.0-canary.100",
25
+ "@graphcommerce/next-ui": "^9.0.0-canary.100",
26
+ "@graphcommerce/prettier-config-pwa": "^9.0.0-canary.100",
27
+ "@graphcommerce/react-hook-form": "^9.0.0-canary.100",
28
+ "@graphcommerce/typescript-config-pwa": "^9.0.0-canary.100",
29
29
  "@lingui/core": "^4.2.1",
30
30
  "@lingui/macro": "^4.2.1",
31
31
  "@lingui/react": "^4.2.1",
32
32
  "@mui/material": "^5.10.16",
33
33
  "framer-motion": "^10.0.0",
34
+ "graphql": "^16.0.0",
34
35
  "next": "*",
35
36
  "react": "^18.2.0",
36
37
  "react-dom": "^18.2.0"
@@ -1,6 +1,9 @@
1
1
  import { GraphQLProviderProps } from '@graphcommerce/graphql'
2
2
  import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
3
- import { cartErrorLink } from '../link/createCartErrorLink'
3
+ import { useEventCallback } from '@mui/material'
4
+ import { NextRouter } from 'next/router'
5
+ import { useMemo } from 'react'
6
+ import { cartLink } from '../link/cartLink'
4
7
  import { cartTypePolicies, migrateCart } from '../typePolicies'
5
8
 
6
9
  export const config: PluginConfig = {
@@ -9,11 +12,20 @@ export const config: PluginConfig = {
9
12
  }
10
13
 
11
14
  export function GraphQLProvider(props: PluginProps<GraphQLProviderProps>) {
12
- const { Prev, links = [], policies = [], migrations = [], ...rest } = props
15
+ const { Prev, router, links = [], policies = [], migrations = [], ...rest } = props
16
+
17
+ const push = useEventCallback<NextRouter['push']>((...args) => router.push(...args))
18
+
19
+ const cartLinkMemo = useMemo(
20
+ () => cartLink({ push, events: router.events, locale: router.locale }),
21
+ [push, router.events, router.locale],
22
+ )
23
+
13
24
  return (
14
25
  <Prev
15
26
  {...rest}
16
- links={[...links, cartErrorLink]}
27
+ router={router}
28
+ links={[...links, cartLinkMemo]}
17
29
  policies={[...policies, cartTypePolicies]}
18
30
  migrations={[...migrations, migrateCart]}
19
31
  />