@graphcommerce/magento-customer 7.0.0-canary.13 → 7.0.0

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/CHANGELOG.md +236 -2
  2. package/components/AccountSignInUpForm/AccountSignInUpForm.tsx +4 -1
  3. package/components/AddressFields/AddressFields.tsx +1 -1
  4. package/components/ApolloCustomerError/ApolloCustomerErrorAlert.tsx +0 -1
  5. package/components/ChangePasswordForm/ChangePasswordForm.tsx +44 -30
  6. package/components/CreateCustomerAddressForm/CreateCustomerAddress.graphql +2 -2
  7. package/components/CreateCustomerAddressForm/CreateCustomerAddressForm.tsx +2 -1
  8. package/components/CreateCustomerAddressForm/CustomerAddress.graphql +1 -0
  9. package/components/CustomerFab/CustomerFab.tsx +3 -0
  10. package/components/CustomerMenuFabItem/CustomerMenuFabItem.tsx +4 -2
  11. package/components/EditAddressForm/EditAddressForm.tsx +2 -0
  12. package/components/ForgotPasswordForm/ForgotPasswordForm.tsx +1 -0
  13. package/components/NameFields/NameFields.tsx +2 -1
  14. package/components/OrderCard/OrderCard.tsx +1 -1
  15. package/components/OrderDetails/OrderDetails.tsx +6 -18
  16. package/components/ResetPasswordForm/ResetPasswordForm.tsx +5 -9
  17. package/components/SessionDebugger/SessionDebugger.tsx +2 -1
  18. package/components/SignInForm/SignInForm.tsx +9 -10
  19. package/components/SignInForm/SignInFormInline.tsx +17 -17
  20. package/components/SignUpForm/SignUpForm.tsx +14 -27
  21. package/components/SignUpForm/SignUpFormInline.tsx +39 -52
  22. package/components/TrackingLink/TrackingLink.tsx +14 -5
  23. package/components/UpdateCustomerEmailForm/UpdateCustomerEmailForm.tsx +28 -21
  24. package/components/UpdateDefaultAddressForm/UpdateDefaultAddressForm.tsx +1 -0
  25. package/components/ValidatedPasswordElement/ValidatedPasswordElement.tsx +47 -0
  26. package/hooks/CustomerInfo.graphql +1 -0
  27. package/hooks/UseCustomerValidateToken.graphql +5 -0
  28. package/hooks/useCustomerValidateToken.ts +23 -0
  29. package/link/createCustomerTokenLink.ts +2 -7
  30. package/package.json +16 -15
  31. package/test/authentication.playwright.ts +1 -0
  32. package/link/onAuthenticationError.ts +0 -39
@@ -1,9 +1,10 @@
1
+ import { PasswordElement } from '@graphcommerce/ecommerce-ui'
1
2
  import { useApolloClient } from '@graphcommerce/graphql'
2
3
  import { graphqlErrorByCategory } from '@graphcommerce/magento-graphql'
3
4
  import { Button, FormRow, FormActions } from '@graphcommerce/next-ui'
4
5
  import { useFormGqlMutation } from '@graphcommerce/react-hook-form'
5
6
  import { Trans } from '@lingui/react'
6
- import { Box, FormControl, Link, SxProps, TextField, Theme } from '@mui/material'
7
+ import { Box, FormControl, Link, SxProps, Theme } from '@mui/material'
7
8
  import { CustomerDocument } from '../../hooks'
8
9
  import { ApolloCustomerErrorAlert } from '../ApolloCustomerError/ApolloCustomerErrorAlert'
9
10
  import { SignInDocument } from './SignIn.gql'
@@ -12,7 +13,6 @@ type SignInFormProps = { email: string; sx?: SxProps<Theme> }
12
13
 
13
14
  export function SignInForm(props: SignInFormProps) {
14
15
  const { email, sx } = props
15
-
16
16
  const client = useApolloClient()
17
17
  const form = useFormGqlMutation(
18
18
  SignInDocument,
@@ -32,7 +32,7 @@ export function SignInForm(props: SignInFormProps) {
32
32
  { errorPolicy: 'all' },
33
33
  )
34
34
 
35
- const { muiRegister, handleSubmit, required, formState, error } = form
35
+ const { handleSubmit, required, formState, error, control } = form
36
36
  const [remainingError, authError] = graphqlErrorByCategory({
37
37
  category: 'graphql-authentication',
38
38
  error,
@@ -41,18 +41,19 @@ export function SignInForm(props: SignInFormProps) {
41
41
 
42
42
  return (
43
43
  <Box component='form' onSubmit={submitHandler} noValidate sx={sx}>
44
- <FormRow>
45
- <TextField
46
- key='password'
44
+ <FormRow sx={{ gridTemplateColumns: 'none' }}>
45
+ <PasswordElement
47
46
  variant='outlined'
48
- type='password'
49
47
  error={!!formState.errors.password || !!authError}
48
+ control={control}
49
+ name='password'
50
50
  label={<Trans id='Password' />}
51
51
  autoFocus
52
52
  autoComplete='current-password'
53
53
  id='current-password'
54
54
  required={required.password}
55
- {...muiRegister('password', { required: required.password })}
55
+ disabled={formState.isSubmitting}
56
+ helperText={!!formState.errors.password || authError?.message}
56
57
  InputProps={{
57
58
  endAdornment: (
58
59
  <Link href='/account/forgot-password' underline='hover' sx={{ whiteSpace: 'nowrap' }}>
@@ -60,8 +61,6 @@ export function SignInForm(props: SignInFormProps) {
60
61
  </Link>
61
62
  ),
62
63
  }}
63
- helperText={formState.errors.password?.message || authError?.message}
64
- disabled={formState.isSubmitting}
65
64
  />
66
65
  </FormRow>
67
66
 
@@ -1,7 +1,8 @@
1
+ import { PasswordElement } from '@graphcommerce/ecommerce-ui'
1
2
  import { Button, extendableComponent } from '@graphcommerce/next-ui'
2
3
  import { useFormGqlMutation } from '@graphcommerce/react-hook-form'
3
4
  import { Trans } from '@lingui/react'
4
- import { Box, SxProps, TextField, Theme } from '@mui/material'
5
+ import { Box, Link, SxProps, Theme } from '@mui/material'
5
6
  import { SignInDocument, SignInMutationVariables } from './SignIn.gql'
6
7
 
7
8
  type InlineSignInFormProps = Omit<SignInMutationVariables, 'password'> & {
@@ -12,13 +13,13 @@ type InlineSignInFormProps = Omit<SignInMutationVariables, 'password'> & {
12
13
  const { classes } = extendableComponent('SignInFormInline', ['form', 'button'] as const)
13
14
 
14
15
  export function SignInFormInline(props: InlineSignInFormProps) {
15
- const { email, sx = [] } = props
16
+ const { email, children, sx = [] } = props
16
17
  const form = useFormGqlMutation(
17
18
  SignInDocument,
18
19
  { defaultValues: { email }, onBeforeSubmit: (values) => ({ ...values, email }) },
19
20
  { errorPolicy: 'all' },
20
21
  )
21
- const { muiRegister, handleSubmit, required, formState, error } = form
22
+ const { handleSubmit, required, formState, control } = form
22
23
  const submitHandler = handleSubmit(() => {})
23
24
 
24
25
  return (
@@ -41,35 +42,34 @@ export function SignInFormInline(props: InlineSignInFormProps) {
41
42
  ...(Array.isArray(sx) ? sx : [sx]),
42
43
  ]}
43
44
  >
44
- <TextField
45
+ <PasswordElement
46
+ control={control}
45
47
  variant='outlined'
46
- type='password'
47
- error={!!formState.errors.password || !!error?.message}
48
+ name='password'
48
49
  label={<Trans id='Password' />}
49
50
  autoFocus
50
51
  autoComplete='current-password'
51
52
  id='current-password'
52
53
  required={required.password}
53
- {...muiRegister('password', { required: required.password })}
54
- helperText={error?.message}
55
54
  disabled={formState.isSubmitting}
56
55
  InputProps={{
57
56
  endAdornment: (
58
- <Button
59
- href='/account/forgot-password'
60
- color='secondary'
61
- variant='text'
62
- className={classes.button}
63
- sx={{ minWidth: 'max-content' }}
64
- >
57
+ <Link href='/account/forgot-password' underline='hover' sx={{ whiteSpace: 'nowrap' }}>
65
58
  <Trans id='Forgot password?' />
66
- </Button>
59
+ </Link>
67
60
  ),
68
61
  }}
69
62
  />
70
- <Button type='submit' loading={formState.isSubmitting} color='secondary' variant='pill'>
63
+ <Button
64
+ type='submit'
65
+ loading={formState.isSubmitting}
66
+ color='secondary'
67
+ variant='pill'
68
+ sx={{ alignSelf: 'start', marginTop: (theme) => `calc(${theme.spacings.xxs} * .33)` }}
69
+ >
71
70
  <Trans id='Sign in' />
72
71
  </Button>
72
+ {children}
73
73
  </Box>
74
74
  )
75
75
  }
@@ -1,13 +1,13 @@
1
- import { useQuery } from '@graphcommerce/graphql'
1
+ import { PasswordRepeatElement } from '@graphcommerce/ecommerce-ui'
2
2
  import { graphqlErrorByCategory } from '@graphcommerce/magento-graphql'
3
- import { StoreConfigDocument } from '@graphcommerce/magento-store'
4
3
  import { Button, FormActions, FormRow } from '@graphcommerce/next-ui'
5
4
  import { useFormGqlMutation, useFormPersist } from '@graphcommerce/react-hook-form'
6
- import { i18n } from '@lingui/core'
7
5
  import { Trans } from '@lingui/react'
8
- import { Alert, FormControlLabel, Switch, TextField } from '@mui/material'
6
+ // eslint-disable-next-line @typescript-eslint/no-restricted-imports
7
+ import { Alert, FormControlLabel, Switch } from '@mui/material'
9
8
  import { ApolloCustomerErrorSnackbar } from '../ApolloCustomerError/ApolloCustomerErrorSnackbar'
10
9
  import { NameFields } from '../NameFields/NameFields'
10
+ import { ValidatedPasswordElement } from '../ValidatedPasswordElement/ValidatedPasswordElement'
11
11
  import { SignUpDocument, SignUpMutation, SignUpMutationVariables } from './SignUp.gql'
12
12
  import { SignUpConfirmDocument } from './SignUpConfirm.gql'
13
13
 
@@ -18,8 +18,6 @@ const requireEmailValidation = import.meta.graphCommerce.customerRequireEmailCon
18
18
  export function SignUpForm(props: SignUpFormProps) {
19
19
  const { email } = props
20
20
 
21
- const storeConfig = useQuery(StoreConfigDocument).data?.storeConfig
22
-
23
21
  const Mutation = requireEmailValidation ? SignUpConfirmDocument : SignUpDocument
24
22
 
25
23
  const form = useFormGqlMutation<
@@ -31,11 +29,10 @@ export function SignUpForm(props: SignUpFormProps) {
31
29
  { errorPolicy: 'all' },
32
30
  )
33
31
 
34
- const { muiRegister, handleSubmit, required, watch, formState, error } = form
32
+ const { muiRegister, handleSubmit, required, formState, error, control } = form
35
33
  const [remainingError, inputError] = graphqlErrorByCategory({ category: 'graphql-input', error })
36
34
 
37
35
  const submitHandler = handleSubmit(() => {})
38
- const watchPassword = watch('password')
39
36
 
40
37
  useFormPersist({ form, name: 'SignUp', exclude: ['password', 'confirmPassword'] })
41
38
 
@@ -50,37 +47,27 @@ export function SignUpForm(props: SignUpFormProps) {
50
47
  return (
51
48
  <form onSubmit={submitHandler} noValidate>
52
49
  <FormRow>
53
- <TextField
50
+ <ValidatedPasswordElement
51
+ control={control}
52
+ name='password'
54
53
  variant='outlined'
55
- type='password'
56
54
  error={!!formState.errors.password || !!inputError}
57
55
  label={<Trans id='Password' />}
58
56
  autoFocus
59
57
  autoComplete='new-password'
60
58
  required={required.password}
61
- {...muiRegister('password', {
62
- required: required.password,
63
- minLength: {
64
- value: Number(storeConfig?.minimum_password_length ?? 8),
65
- message: i18n._(/* i18n */ 'Password must have at least 8 characters'),
66
- },
67
- })}
68
- helperText={formState.errors.password?.message || inputError?.message}
69
59
  disabled={formState.isSubmitting}
60
+ helperText={inputError?.message}
70
61
  />
71
- <TextField
62
+ <PasswordRepeatElement
63
+ control={control}
64
+ name='confirmPassword'
65
+ passwordFieldName='password'
72
66
  variant='outlined'
73
- type='password'
74
- error={!!formState.errors.confirmPassword}
67
+ error={!!formState.errors.confirmPassword || !!inputError}
75
68
  label={<Trans id='Confirm password' />}
76
69
  autoComplete='new-password'
77
70
  required
78
- {...muiRegister('confirmPassword', {
79
- required: true,
80
- validate: (value) =>
81
- value === watchPassword || i18n._(/* i18n */ "Passwords don't match"),
82
- })}
83
- helperText={formState.errors.confirmPassword?.message}
84
71
  disabled={formState.isSubmitting}
85
72
  />
86
73
  </FormRow>
@@ -1,11 +1,11 @@
1
- import { useQuery } from '@graphcommerce/graphql'
2
- import { StoreConfigDocument } from '@graphcommerce/magento-store'
1
+ import { ApolloErrorAlert, PasswordRepeatElement } from '@graphcommerce/ecommerce-ui'
2
+ import { graphqlErrorByCategory } from '@graphcommerce/magento-graphql'
3
3
  import { Button, extendableComponent, Form, FormRow } from '@graphcommerce/next-ui'
4
4
  import { useFormGqlMutation } from '@graphcommerce/react-hook-form'
5
- import { i18n } from '@lingui/core'
6
5
  import { Trans } from '@lingui/react'
7
- import { Alert, Box, TextField } from '@mui/material'
6
+ import { Alert, Box } from '@mui/material'
8
7
  import React from 'react'
8
+ import { ValidatedPasswordElement } from '../ValidatedPasswordElement/ValidatedPasswordElement'
9
9
  import { SignUpMutationVariables, SignUpMutation, SignUpDocument } from './SignUp.gql'
10
10
  import { SignUpConfirmDocument } from './SignUpConfirm.gql'
11
11
 
@@ -25,37 +25,35 @@ const { classes } = extendableComponent('SignUpFormInline', [
25
25
 
26
26
  const requireEmailValidation = import.meta.graphCommerce.customerRequireEmailConfirmation ?? false
27
27
 
28
- export function SignUpFormInline({
29
- email,
30
- children,
31
- firstname,
32
- lastname,
33
- onSubmitted = () => {},
34
- }: SignUpFormInlineProps) {
28
+ export function SignUpFormInline(props: SignUpFormInlineProps) {
29
+ const { email, children, firstname, lastname, onSubmitted = () => {} } = props
35
30
  const Mutation = requireEmailValidation ? SignUpConfirmDocument : SignUpDocument
36
31
 
37
32
  const form = useFormGqlMutation<
38
33
  SignUpMutation,
39
34
  SignUpMutationVariables & { confirmPassword?: string }
40
- >(Mutation, {
41
- // todo(paales): This causes dirty data to be send to the backend.
42
- defaultValues: {
43
- email,
44
- prefix: '-',
45
- firstname: firstname ?? '-',
46
- lastname: lastname ?? '-',
35
+ >(
36
+ Mutation,
37
+ {
38
+ // todo(paales): This causes dirty data to be send to the backend.
39
+ defaultValues: {
40
+ email,
41
+ prefix: '-',
42
+ firstname: firstname ?? '-',
43
+ lastname: lastname ?? '-',
44
+ },
45
+ onBeforeSubmit: (values) => ({ ...values, email }),
46
+ onComplete: (result) => {
47
+ if (!result.errors) onSubmitted()
48
+ },
47
49
  },
48
- onBeforeSubmit: (values) => ({ ...values, email }),
49
- })
50
-
51
- const { muiRegister, watch, handleSubmit, required, formState, error } = form
52
- const submitHandler = handleSubmit(onSubmitted)
53
- const watchPassword = watch('password')
54
-
55
- const minPasswordLength = Number(
56
- useQuery(StoreConfigDocument).data?.storeConfig?.minimum_password_length ?? 8,
50
+ { errorPolicy: 'all' },
57
51
  )
58
52
 
53
+ const { handleSubmit, formState, control, error, required } = form
54
+ const [remainingError, inputError] = graphqlErrorByCategory({ category: 'graphql-input', error })
55
+ const submitHandler = handleSubmit(() => {})
56
+
59
57
  if (requireEmailValidation && form.formState.isSubmitSuccessful) {
60
58
  return (
61
59
  <Alert>
@@ -66,43 +64,31 @@ export function SignUpFormInline({
66
64
 
67
65
  return (
68
66
  <Form onSubmit={submitHandler} noValidate className={classes.form} sx={{ padding: 0 }}>
69
- <FormRow key='inline-signup' className={classes.row} sx={{ padding: 0 }}>
70
- <TextField
67
+ <FormRow className={classes.row} sx={{ padding: 0 }}>
68
+ <ValidatedPasswordElement
69
+ control={control}
70
+ name='password'
71
+ autoComplete='new-password'
71
72
  variant='outlined'
72
- type='password'
73
- error={!!formState.errors.password || !!error?.message}
74
73
  label={<Trans id='Password' />}
75
- autoFocus
76
- autoComplete='new-password'
77
- id='new-password'
78
74
  required={required.password}
79
- {...muiRegister('password', {
80
- required: required.password,
81
- minLength: {
82
- value: minPasswordLength,
83
- message: i18n._(/* i18n */ 'Password must have at least 8 characters'),
84
- },
85
- })}
86
- helperText={error?.message}
87
75
  disabled={formState.isSubmitting}
76
+ error={!!inputError}
77
+ helperText={inputError?.message}
88
78
  />
89
- <TextField
79
+ <PasswordRepeatElement
80
+ control={control}
81
+ name='confirmPassword'
82
+ passwordFieldName='password'
83
+ autoComplete='new-password'
90
84
  variant='outlined'
91
- type='password'
92
- error={!!formState.errors.confirmPassword || !!error?.message}
93
85
  label={<Trans id='Confirm password' />}
94
- autoComplete='new-password'
95
86
  required
96
- {...muiRegister('confirmPassword', {
97
- required: true,
98
- validate: (value) => value === watchPassword,
99
- })}
100
- helperText={!!formState.errors.confirmPassword && <Trans id='Passwords should match' />}
101
87
  disabled={formState.isSubmitting}
102
88
  />
103
89
  </FormRow>
104
90
 
105
- <FormRow key='signup-submit'>
91
+ <FormRow>
106
92
  <FormRow
107
93
  className={classes.buttonFormRow}
108
94
  sx={(theme) => ({
@@ -126,6 +112,7 @@ export function SignUpFormInline({
126
112
  </Box>
127
113
  </FormRow>
128
114
  </FormRow>
115
+ <ApolloErrorAlert error={remainingError} />
129
116
  </Form>
130
117
  )
131
118
  }
@@ -1,7 +1,6 @@
1
1
  import { IconSvg, iconLocation } from '@graphcommerce/next-ui'
2
2
  import { Trans } from '@lingui/react'
3
- import { Box, SxProps, Theme } from '@mui/material'
4
- import React from 'react'
3
+ import { Box, Link, SxProps, Theme, Typography } from '@mui/material'
5
4
  import { TrackingLinkFragment } from './TrackingLink.gql'
6
5
 
7
6
  export type TrackingLinkProps = TrackingLinkFragment & { sx?: SxProps<Theme> }
@@ -9,6 +8,8 @@ export type TrackingLinkProps = TrackingLinkFragment & { sx?: SxProps<Theme> }
9
8
  export function TrackingLink(props: TrackingLinkProps) {
10
9
  const { number, sx = [] } = props
11
10
 
11
+ const validUrl = number?.startsWith('http://') || number?.startsWith('https://')
12
+
12
13
  return (
13
14
  <Box
14
15
  className='TrackingLink-root'
@@ -21,12 +22,20 @@ export function TrackingLink(props: TrackingLinkProps) {
21
22
  ...(Array.isArray(sx) ? sx : [sx]),
22
23
  ]}
23
24
  >
24
- {number && (
25
- <>
25
+ {number && validUrl && (
26
+ <Link
27
+ onClick={(e) => e.stopPropagation()}
28
+ href={number}
29
+ target='_blank'
30
+ underline='hover'
31
+ sx={{ display: 'inline-flex', alignItems: 'center' }}
32
+ >
26
33
  <IconSvg src={iconLocation} size='small' />
27
34
  <Trans id='Follow order' />
28
- </>
35
+ </Link>
29
36
  )}
37
+
38
+ {number && !validUrl && <Typography>{number}</Typography>}
30
39
  </Box>
31
40
  )
32
41
  }
@@ -1,3 +1,5 @@
1
+ import { PasswordElement } from '@graphcommerce/ecommerce-ui'
2
+ import { graphqlErrorByCategory } from '@graphcommerce/magento-graphql'
1
3
  import {
2
4
  Button,
3
5
  Form,
@@ -9,8 +11,9 @@ import {
9
11
  import { emailPattern, useFormGqlMutation } from '@graphcommerce/react-hook-form'
10
12
  import { i18n } from '@lingui/core'
11
13
  import { Trans } from '@lingui/react'
14
+ // eslint-disable-next-line @typescript-eslint/no-restricted-imports
12
15
  import { TextField } from '@mui/material'
13
- import { ApolloCustomerErrorAlert } from '../ApolloCustomerError/ApolloCustomerErrorAlert'
16
+ import { ApolloCustomerErrorSnackbar } from '../ApolloCustomerError'
14
17
  import {
15
18
  UpdateCustomerEmailDocument,
16
19
  UpdateCustomerEmailMutation,
@@ -26,13 +29,20 @@ export function UpdateCustomerEmailForm(props: UpdateCustomerEmailFormProps) {
26
29
 
27
30
  const form = useFormGqlMutation<
28
31
  UpdateCustomerEmailMutation,
29
- UpdateCustomerEmailMutationVariables & {
30
- currentEmail?: string
31
- confirmEmail?: string
32
- }
33
- >(UpdateCustomerEmailDocument)
32
+ UpdateCustomerEmailMutationVariables & { currentEmail?: string; confirmEmail?: string }
33
+ >(
34
+ UpdateCustomerEmailDocument,
35
+ {},
36
+ {
37
+ errorPolicy: 'all',
38
+ },
39
+ )
34
40
 
35
- const { handleSubmit, error, required, formState, watch, muiRegister, reset } = form
41
+ const { handleSubmit, error, required, formState, watch, muiRegister, reset, control } = form
42
+ const [remainingError, authenticationError] = graphqlErrorByCategory({
43
+ category: 'graphql-authentication',
44
+ error,
45
+ })
36
46
  const submit = handleSubmit(() => {
37
47
  reset()
38
48
  })
@@ -45,7 +55,7 @@ export function UpdateCustomerEmailForm(props: UpdateCustomerEmailFormProps) {
45
55
  key='current-email'
46
56
  variant='outlined'
47
57
  type='text'
48
- autoComplete='currentEmail'
58
+ autoComplete='email'
49
59
  autoFocus
50
60
  error={formState.isSubmitted && !!formState.errors.currentEmail}
51
61
  helperText={formState.isSubmitted && formState.errors.currentEmail?.message}
@@ -67,8 +77,7 @@ export function UpdateCustomerEmailForm(props: UpdateCustomerEmailFormProps) {
67
77
  key='email'
68
78
  variant='outlined'
69
79
  type='text'
70
- autoComplete='email'
71
- autoFocus
80
+ autoComplete='off'
72
81
  error={formState.isSubmitted && !!formState.errors.email}
73
82
  helperText={formState.isSubmitted && formState.errors.email?.message}
74
83
  label={<Trans id='New email' />}
@@ -82,8 +91,7 @@ export function UpdateCustomerEmailForm(props: UpdateCustomerEmailFormProps) {
82
91
  key='confirm-email'
83
92
  variant='outlined'
84
93
  type='text'
85
- autoComplete='confirmEmail'
86
- autoFocus
94
+ autoComplete='off'
87
95
  error={formState.isSubmitted && !!formState.errors.confirmEmail}
88
96
  helperText={formState.isSubmitted && formState.errors.confirmEmail?.message}
89
97
  label={<Trans id='Confirm new email' />}
@@ -96,21 +104,21 @@ export function UpdateCustomerEmailForm(props: UpdateCustomerEmailFormProps) {
96
104
  </FormRow>
97
105
 
98
106
  <FormRow>
99
- <TextField
107
+ <PasswordElement
108
+ control={control}
100
109
  variant='outlined'
101
- type='password'
102
- error={!!formState.errors.password}
110
+ name='password'
103
111
  label={<Trans id='Password' />}
104
- autoComplete='password'
112
+ autoComplete='current-password'
105
113
  required={required.password}
106
- {...muiRegister('password', {
107
- required: required.password,
108
- })}
109
- helperText={formState.errors.password?.message}
110
114
  disabled={formState.isSubmitting}
115
+ error={Boolean(authenticationError)}
116
+ helperText={authenticationError?.message}
111
117
  />
112
118
  </FormRow>
113
119
 
120
+ <ApolloCustomerErrorSnackbar error={remainingError} />
121
+
114
122
  <FormDivider />
115
123
  <FormActions>
116
124
  <Button
@@ -123,7 +131,6 @@ export function UpdateCustomerEmailForm(props: UpdateCustomerEmailFormProps) {
123
131
  <Trans id='Save changes' />
124
132
  </Button>
125
133
  </FormActions>
126
- <ApolloCustomerErrorAlert error={error} />
127
134
 
128
135
  <MessageSnackbar sticky open={formState.isSubmitSuccessful && !error}>
129
136
  <Trans id='Successfully updated email' />
@@ -1,5 +1,6 @@
1
1
  import { Controller, useFormAutoSubmit, useFormGqlMutation } from '@graphcommerce/react-hook-form'
2
2
  import { Trans } from '@lingui/react'
3
+ // eslint-disable-next-line @typescript-eslint/no-restricted-imports
3
4
  import { FormControl, FormControlLabel, FormHelperText, Switch } from '@mui/material'
4
5
  import React, { useEffect, useMemo } from 'react'
5
6
  import { AccountAddressFragment } from '../AccountAddress/AccountAddress.gql'
@@ -0,0 +1,47 @@
1
+ import { PasswordElement, PasswordElementProps } from '@graphcommerce/ecommerce-ui'
2
+ import { useQuery } from '@graphcommerce/graphql'
3
+ import { StoreConfigDocument } from '@graphcommerce/magento-store'
4
+ import { FieldValues } from '@graphcommerce/react-hook-form'
5
+ import { i18n } from '@lingui/core'
6
+
7
+ export type ValidatedPasswordElementProps<T extends FieldValues> = PasswordElementProps<T>
8
+
9
+ export function ValidatedPasswordElement<TFieldValues extends FieldValues>(
10
+ props: PasswordElementProps<TFieldValues>,
11
+ ): JSX.Element {
12
+ const { ...textFieldProps } = props
13
+
14
+ const storeConfig = useQuery(StoreConfigDocument).data?.storeConfig
15
+ const minPasswordLength = Number(storeConfig?.minimum_password_length) ?? 0
16
+ const passwordMinCharacterSets = Number(storeConfig?.required_character_classes_number) ?? 0
17
+
18
+ const validation: NonNullable<PasswordElementProps<TFieldValues>['validation']> = {}
19
+
20
+ validation.minLength = {
21
+ value: minPasswordLength,
22
+ message: i18n._(/* i18n */ 'Password must have at least {minPasswordLength} characters', {
23
+ minPasswordLength,
24
+ }),
25
+ }
26
+
27
+ validation.validate = (value: string) => {
28
+ const pass = value.trim()
29
+ let counter = 0
30
+
31
+ if (pass.match(/\d+/)) counter++
32
+ if (pass.match(/[a-z]+/)) counter++
33
+ if (pass.match(/[A-Z]+/)) counter++
34
+ if (pass.match(/[^a-zA-Z0-9]+/)) counter++
35
+
36
+ if (counter < passwordMinCharacterSets) {
37
+ return i18n._(
38
+ /* i18n */ 'Minimum of different classes of characters in password is {passwordMinCharacterSets}. Classes of characters: Lower Case, Upper Case, Digits, Special Characters.',
39
+ { passwordMinCharacterSets },
40
+ )
41
+ }
42
+
43
+ return true
44
+ }
45
+
46
+ return <PasswordElement {...textFieldProps} validation={validation} />
47
+ }
@@ -2,6 +2,7 @@ fragment CustomerInfo on Customer {
2
2
  default_billing
3
3
  default_shipping
4
4
  addresses {
5
+ default_billing
5
6
  ...CustomerAddress
6
7
  }
7
8
  email
@@ -0,0 +1,5 @@
1
+ query UseCustomerValidateToken {
2
+ customer {
3
+ email
4
+ }
5
+ }
@@ -0,0 +1,23 @@
1
+ import { useApolloClient } from '@graphcommerce/graphql'
2
+ import { ErrorCategory } from '@graphcommerce/magento-graphql'
3
+ import { useRouter } from 'next/router'
4
+ import { UseCustomerValidateTokenDocument } from './UseCustomerValidateToken.gql'
5
+ import { useCustomerQuery } from './useCustomerQuery'
6
+
7
+ /** Validates the current customer token. This hook is supposed to be called only once. */
8
+ export function useCustomerValidateToken() {
9
+ const client = useApolloClient()
10
+ const router = useRouter()
11
+
12
+ useCustomerQuery(UseCustomerValidateTokenDocument, {
13
+ initialFetchPolicy: 'network-only',
14
+ onError: async ({ graphQLErrors }) => {
15
+ const category: ErrorCategory = 'graphql-authorization'
16
+ // If there is no authorization error, do nothing.
17
+ if (!graphQLErrors.some((e) => e.extensions?.category === category)) return
18
+
19
+ await client.clearStore()
20
+ router.reload()
21
+ },
22
+ })
23
+ }
@@ -1,7 +1,5 @@
1
- import { NormalizedCacheObject } from '@graphcommerce/graphql'
2
- import { ApolloLink, ApolloCache, setContext } from '@graphcommerce/graphql/apollo'
1
+ import { setContext } from '@graphcommerce/graphql/apollo'
3
2
  import { CustomerTokenDocument } from '../hooks'
4
- import { onAuthorizationError } from './onAuthenticationError'
5
3
 
6
4
  export const addTokenHeader = setContext((_, context) => {
7
5
  if (!context.headers) context.headers = {}
@@ -17,7 +15,4 @@ export const addTokenHeader = setContext((_, context) => {
17
15
  }
18
16
  })
19
17
 
20
- export const customerTokenLink = ApolloLink.from([addTokenHeader, onAuthorizationError])
21
-
22
- /** Not really required anymore, you can use customerTokenLink directly */
23
- export const createCustomerTokenLink = (_: ApolloCache<NormalizedCacheObject>) => customerTokenLink
18
+ export const customerTokenLink = addTokenHeader