@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.
- package/Api/CartItemCountChanged.graphql +1 -1
- package/CHANGELOG.md +247 -54
- package/Config.graphqls +17 -0
- package/components/ApolloCartError/ApolloCartErrorAlert.tsx +1 -46
- package/components/CartAddress/CartAddress.graphql +1 -0
- package/components/CartAgreementsForm/CartAgreementsForm.tsx +60 -33
- package/components/CartFab/CartFab.tsx +8 -2
- package/components/CartStartCheckout/CartStartCheckout.graphql +1 -1
- package/components/CartStartCheckout/CartStartCheckout.tsx +23 -7
- package/components/CartStartCheckout/CartStartCheckoutLinkOrButton.tsx +4 -2
- package/components/CartTotals/CartTotals.graphql +8 -4
- package/components/CartTotals/CartTotals.tsx +9 -6
- package/components/EmptyCart/EmptyCart.tsx +8 -4
- package/components/InlineAccount/InlineAccount.tsx +6 -6
- package/components/OrderSucces/OrderSuccesPage.graphql +1 -1
- package/hooks/index.ts +5 -3
- package/hooks/useCartPermissions.ts +21 -0
- package/hooks/useCartQuery.ts +24 -3
- package/hooks/useCheckoutPermissions.ts +21 -0
- package/hooks/useFormGqlMutationCart.ts +40 -3
- package/index.ts +4 -3
- package/link/cartLink.ts +127 -0
- package/link/isProtectedCartOperation.ts +5 -0
- package/package.json +16 -15
- package/plugins/MagentoCartGraphqlProvider.tsx +15 -3
- package/plugins/useSignInFormMergeCart.ts +8 -7
- package/typePolicies.ts +1 -0
- package/utils/cartPermissions.ts +23 -0
- package/utils/checkoutPermissions.ts +13 -0
- package/utils/index.ts +2 -0
- package/hooks/CurrentCartId.graphqls +0 -12
- 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
|
|
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,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/
|
|
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'
|
|
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) => ({
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 = {
|
|
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'
|
|
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
|
-
|
|
105
|
-
label='Email'
|
|
105
|
+
label={<Trans id='Email address' />}
|
|
106
106
|
value={cart?.email}
|
|
107
107
|
InputProps={{
|
|
108
108
|
readOnly: true,
|
package/hooks/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export * from './useAssignCurrentCartId'
|
|
2
|
-
export * from './
|
|
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 './
|
|
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
|
+
}
|
package/hooks/useCartQuery.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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)
|
|
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'
|
package/link/cartLink.ts
ADDED
|
@@ -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
|
+
}
|