@salesforce/retail-react-app 6.0.0-preview.0 → 6.0.0-preview.1

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 (65) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/app/assets/svg/apple.svg +53 -0
  3. package/app/assets/svg/google.svg +68 -0
  4. package/app/components/_app-config/index.jsx +4 -0
  5. package/app/components/email-confirmation/index.jsx +65 -0
  6. package/app/components/email-confirmation/index.test.js +22 -0
  7. package/app/components/forms/login-fields.jsx +33 -6
  8. package/app/components/forms/login-fields.test.js +64 -0
  9. package/app/components/forms/profile-fields.jsx +7 -1
  10. package/app/components/icons/index.jsx +7 -3
  11. package/app/components/login/index.jsx +56 -55
  12. package/app/components/login/index.test.js +76 -0
  13. package/app/components/passwordless-login/index.jsx +109 -0
  14. package/app/components/passwordless-login/index.test.js +60 -0
  15. package/app/components/reset-password/index.jsx +83 -49
  16. package/app/components/reset-password/index.test.js +95 -0
  17. package/app/components/social-login/index.jsx +125 -0
  18. package/app/components/social-login/index.test.jsx +39 -0
  19. package/app/components/standard-login/index.jsx +83 -0
  20. package/app/components/standard-login/index.test.js +46 -0
  21. package/app/components/toggle-card/index.jsx +3 -1
  22. package/app/constants.js +36 -0
  23. package/app/hooks/use-auth-modal.js +124 -91
  24. package/app/hooks/use-auth-modal.test.js +113 -3
  25. package/app/hooks/use-dnt-notification.js +39 -37
  26. package/app/hooks/use-password-reset.js +57 -0
  27. package/app/hooks/use-password-reset.test.js +94 -0
  28. package/app/mocks/mock-data.js +39 -0
  29. package/app/pages/account/index.test.js +28 -1
  30. package/app/pages/account/profile.jsx +13 -4
  31. package/app/pages/account/profile.test.js +119 -0
  32. package/app/pages/checkout/index.jsx +10 -1
  33. package/app/pages/checkout/index.test.js +5 -2
  34. package/app/pages/checkout/partials/contact-info.jsx +73 -17
  35. package/app/pages/checkout/partials/contact-info.test.js +226 -14
  36. package/app/pages/checkout/partials/login-state.jsx +116 -0
  37. package/app/pages/checkout/partials/login-state.test.js +76 -0
  38. package/app/pages/login/index.jsx +143 -31
  39. package/app/pages/login/index.test.js +2 -0
  40. package/app/pages/login/passwordless-landing.test.js +154 -0
  41. package/app/pages/product-detail/index.jsx +11 -2
  42. package/app/pages/product-list/index.jsx +11 -1
  43. package/app/pages/reset-password/index.jsx +24 -57
  44. package/app/pages/reset-password/index.test.jsx +4 -68
  45. package/app/pages/reset-password/reset-password-landing.jsx +103 -0
  46. package/app/pages/social-login-redirect/index.jsx +143 -0
  47. package/app/pages/social-login-redirect/index.test.jsx +16 -0
  48. package/app/routes.jsx +23 -0
  49. package/app/ssr.js +92 -1
  50. package/app/static/translations/compiled/en-GB.json +150 -32
  51. package/app/static/translations/compiled/en-US.json +150 -32
  52. package/app/static/translations/compiled/en-XA.json +318 -56
  53. package/app/utils/jwt-utils.js +88 -0
  54. package/app/utils/jwt-utils.test.js +134 -0
  55. package/app/utils/marketing-cloud/marketing-cloud-email-link.js +136 -0
  56. package/app/utils/marketing-cloud/marketing-cloud-email-link.test.js +65 -0
  57. package/app/utils/utils.js +18 -0
  58. package/app/utils/utils.test.js +23 -0
  59. package/babel.config.js +18 -1
  60. package/config/default.js +15 -0
  61. package/config/mocks/default.js +10 -0
  62. package/jest-setup.js +11 -3
  63. package/package.json +10 -8
  64. package/translations/en-GB.json +63 -9
  65. package/translations/en-US.json +63 -9
package/CHANGELOG.md CHANGED
@@ -1,8 +1,10 @@
1
1
  ## v6.0.0
2
2
  - DNT Consent Banner: [#2203](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2203)
3
+ - Support Node 22 [#2218](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2218)
4
+ - Implemented opt-in Social & Passwordless Login features and fixed the Reset Password flow which now leverages SLAS APIs [#2079] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2079)
3
5
  - Allow store to be selectable in StoreLocator [#2187](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2187)
4
6
  - Replace transfer basket call with merge basket on checkout [#2138](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2138)
5
- - Support Node 22 [#2218](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2218)
7
+ - PDP / PLP: Add page meta data tags that have been defined in BM [#2232](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2232)
6
8
 
7
9
  ### Bug Fixes
8
10
  - [BUG] Fixed GET /shopper-context API calls being made without the usid [#2206](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2206)
@@ -0,0 +1,53 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg
3
+ width="24"
4
+ height="24"
5
+ viewBox="0 0 24 24"
6
+ fill="none"
7
+ version="1.1"
8
+ id="svg1"
9
+ sodipodi:docname="Apple.svg"
10
+ inkscape:version="1.3.2 (091e20e, 2023-11-25)"
11
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
13
+ xmlns="http://www.w3.org/2000/svg"
14
+ xmlns:svg="http://www.w3.org/2000/svg">
15
+ <sodipodi:namedview
16
+ id="namedview1"
17
+ pagecolor="#ffffff"
18
+ bordercolor="#000000"
19
+ borderopacity="0.25"
20
+ inkscape:showpageshadow="2"
21
+ inkscape:pageopacity="0.0"
22
+ inkscape:pagecheckerboard="0"
23
+ inkscape:deskcolor="#d1d1d1"
24
+ inkscape:zoom="9.8333333"
25
+ inkscape:cx="11.949153"
26
+ inkscape:cy="12"
27
+ inkscape:window-width="1312"
28
+ inkscape:window-height="449"
29
+ inkscape:window-x="0"
30
+ inkscape:window-y="25"
31
+ inkscape:window-maximized="0"
32
+ inkscape:current-layer="svg1" />
33
+ <g
34
+ clip-path="url(#clip0_37937_16105)"
35
+ id="g1">
36
+ <path
37
+ d="M19.762 8.818C19.646 8.908 17.598 10.062 17.598 12.628C17.598 15.596 20.204 16.646 20.282 16.672C20.27 16.736 19.868 18.11 18.908 19.51C18.052 20.742 17.158 21.972 15.798 21.972C14.438 21.972 14.088 21.182 12.518 21.182C10.988 21.182 10.444 21.998 9.2 21.998C7.956 21.998 7.088 20.858 6.09 19.458C4.934 17.814 4 15.26 4 12.836C4 8.948 6.528 6.886 9.016 6.886C10.338 6.886 11.44 7.754 12.27 7.754C13.06 7.754 14.292 6.834 15.796 6.834C16.366 6.834 18.414 6.886 19.762 8.818ZM15.082 5.188C15.704 4.45 16.144 3.426 16.144 2.402C16.144 2.26 16.132 2.116 16.106 2C15.094 2.038 13.89 2.674 13.164 3.516C12.594 4.164 12.062 5.188 12.062 6.226C12.062 6.382 12.088 6.538 12.1 6.588C12.164 6.6 12.268 6.614 12.372 6.614C13.28 6.614 14.422 6.006 15.082 5.188Z"
38
+ fill="#181818"
39
+ id="path1" />
40
+ </g>
41
+ <defs
42
+ id="defs1">
43
+ <clipPath
44
+ id="clip0_37937_16105">
45
+ <rect
46
+ width="16.28"
47
+ height="20"
48
+ fill="white"
49
+ transform="translate(4 2)"
50
+ id="rect1" />
51
+ </clipPath>
52
+ </defs>
53
+ </svg>
@@ -0,0 +1,68 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg
3
+ width="28"
4
+ height="28"
5
+ viewBox="0 0 28 28"
6
+ fill="none"
7
+ version="1.1"
8
+ id="svg5"
9
+ sodipodi:docname="web_light_sq_na.svg"
10
+ inkscape:version="1.3.2 (091e20e, 2023-11-25)"
11
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
13
+ xmlns="http://www.w3.org/2000/svg"
14
+ xmlns:svg="http://www.w3.org/2000/svg">
15
+ <sodipodi:namedview
16
+ id="namedview5"
17
+ pagecolor="#ffffff"
18
+ bordercolor="#000000"
19
+ borderopacity="0.25"
20
+ inkscape:showpageshadow="2"
21
+ inkscape:pageopacity="0.0"
22
+ inkscape:pagecheckerboard="0"
23
+ inkscape:deskcolor="#d1d1d1"
24
+ inkscape:zoom="5.9"
25
+ inkscape:cx="15.677966"
26
+ inkscape:cy="15.59322"
27
+ inkscape:window-width="1552"
28
+ inkscape:window-height="551"
29
+ inkscape:window-x="109"
30
+ inkscape:window-y="1375"
31
+ inkscape:window-maximized="0"
32
+ inkscape:current-layer="svg5" />
33
+ <g
34
+ clip-path="url(#clip0_710_6223)"
35
+ id="g4"
36
+ transform="translate(-5.7627118,-5.9322035)">
37
+ <path
38
+ d="m 29.6,20.2273 c 0,-0.7091 -0.0636,-1.3909 -0.1818,-2.0455 H 20 V 22.05 h 5.3818 c -0.2318,1.25 -0.9363,2.3091 -1.9954,3.0182 v 2.5091 h 3.2318 C 28.5091,25.8364 29.6,23.2727 29.6,20.2273 Z"
39
+ fill="#4285f4"
40
+ id="path1" />
41
+ <path
42
+ d="m 20,30 c 2.7,0 4.9636,-0.8955 6.6181,-2.4227 l -3.2318,-2.5091 c -0.8954,0.6 -2.0409,0.9545 -3.3863,0.9545 -2.6046,0 -4.8091,-1.7591 -5.5955,-4.1227 h -3.3409 v 2.5909 C 12.7091,27.7591 16.0909,30 20,30 Z"
43
+ fill="#34a853"
44
+ id="path2" />
45
+ <path
46
+ d="m 14.4045,21.9 c -0.2,-0.6 -0.3136,-1.2409 -0.3136,-1.9 0,-0.6591 0.1136,-1.3 0.3136,-1.9 V 15.5091 H 11.0636 C 10.3864,16.8591 10,18.3864 10,20 c 0,1.6136 0.3864,3.1409 1.0636,4.4909 z"
47
+ fill="#fbbc04"
48
+ id="path3" />
49
+ <path
50
+ d="m 20,13.9773 c 1.4681,0 2.7863,0.5045 3.8227,1.4954 l 2.8682,-2.8682 C 24.9591,10.9909 22.6954,10 20,10 c -3.9091,0 -7.2909,2.2409 -8.9364,5.5091 L 14.4045,18.1 C 15.1909,15.7364 17.3954,13.9773 20,13.9773 Z"
51
+ fill="#e94235"
52
+ id="path4" />
53
+ </g>
54
+ <defs
55
+ id="defs5">
56
+ <clipPath
57
+ id="clip0_710_6223">
58
+ <rect
59
+ width="20"
60
+ height="20"
61
+ fill="#ffffff"
62
+ transform="translate(10,10)"
63
+ id="rect5"
64
+ x="0"
65
+ y="0" />
66
+ </clipPath>
67
+ </defs>
68
+ </svg>
@@ -51,8 +51,11 @@ const AppConfig = ({children, locals = {}}) => {
51
51
  }
52
52
 
53
53
  const commerceApiConfig = locals.appConfig.commerceAPI
54
+
54
55
  const appOrigin = useAppOrigin()
55
56
 
57
+ const passwordlessCallback = locals.appConfig.login?.passwordless?.callbackURI
58
+
56
59
  return (
57
60
  <CommerceApiProvider
58
61
  shortCode={commerceApiConfig.parameters.shortCode}
@@ -62,6 +65,7 @@ const AppConfig = ({children, locals = {}}) => {
62
65
  locale={locals.locale?.id}
63
66
  currency={locals.locale?.preferredCurrency}
64
67
  redirectURI={`${appOrigin}/callback`}
68
+ passwordlessLoginCallbackURI={passwordlessCallback}
65
69
  proxy={`${appOrigin}${commerceApiConfig.proxyPath}`}
66
70
  headers={headers}
67
71
  defaultDnt={DEFAULT_DNT_STATE}
@@ -0,0 +1,65 @@
1
+ /*
2
+ * Copyright (c) 2024, salesforce.com, inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+
8
+ import React from 'react'
9
+ import PropTypes from 'prop-types'
10
+ import {FormattedMessage} from 'react-intl'
11
+ import {Button, Stack, Text} from '@salesforce/retail-react-app/app/components/shared/ui'
12
+ import {BrandLogo} from '@salesforce/retail-react-app/app/components/icons'
13
+
14
+ const PasswordlessEmailConfirmation = ({form, submitForm, email = ''}) => {
15
+ return (
16
+ <form
17
+ onSubmit={form.handleSubmit(submitForm)}
18
+ data-testid="sf-form-resend-passwordless-email"
19
+ >
20
+ <Stack spacing={6}>
21
+ <Stack justify="center" align="center" spacing={6} role="alert">
22
+ <BrandLogo width="60px" height="auto" aria-hidden={true} />
23
+ <Text align="center" fontSize="xl" fontWeight="semibold">
24
+ <FormattedMessage
25
+ defaultMessage="Check Your Email"
26
+ id="auth_modal.check_email.title.check_your_email"
27
+ />
28
+ </Text>
29
+ <Stack spacing={10}>
30
+ <Text align="center" fontSize="md">
31
+ <FormattedMessage
32
+ defaultMessage="We just sent a login link to <b>{email}</b>"
33
+ id="auth_modal.check_email.description.just_sent"
34
+ values={{
35
+ email: email,
36
+ b: (chunks) => <b>{chunks}</b>
37
+ }}
38
+ />
39
+ </Text>
40
+ <Text align="center" fontSize="sm">
41
+ <FormattedMessage
42
+ defaultMessage="The link may take a few minutes to arrive, check your spam folder if you're having trouble finding it"
43
+ id="auth_modal.check_email.description.check_spam_folder"
44
+ />
45
+ </Text>
46
+ </Stack>
47
+ </Stack>
48
+ <Button type="submit">
49
+ <FormattedMessage
50
+ defaultMessage="Resend Link"
51
+ id="auth_modal.check_email.button.resend_link"
52
+ />
53
+ </Button>
54
+ </Stack>
55
+ </form>
56
+ )
57
+ }
58
+
59
+ PasswordlessEmailConfirmation.propTypes = {
60
+ form: PropTypes.object,
61
+ submitForm: PropTypes.func,
62
+ email: PropTypes.string
63
+ }
64
+
65
+ export default PasswordlessEmailConfirmation
@@ -0,0 +1,22 @@
1
+ /*
2
+ * Copyright (c) 2024, Salesforce, Inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+ import React from 'react'
8
+ import {screen} from '@testing-library/react'
9
+ import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
10
+ import PasswordlessEmailConfirmation from '@salesforce/retail-react-app/app/components/email-confirmation/index'
11
+ import {useForm} from 'react-hook-form'
12
+
13
+ const WrapperComponent = ({...props}) => {
14
+ const form = useForm()
15
+ return <PasswordlessEmailConfirmation form={form} {...props} />
16
+ }
17
+
18
+ test('renders PasswordlessEmailConfirmation component with passed email', () => {
19
+ const email = 'test@salesforce.com'
20
+ renderWithProviders(<WrapperComponent email={email} />)
21
+ expect(screen.getByText(email)).toBeInTheDocument()
22
+ })
@@ -6,26 +6,53 @@
6
6
  */
7
7
  import React from 'react'
8
8
  import PropTypes from 'prop-types'
9
- import {Stack} from '@salesforce/retail-react-app/app/components/shared/ui'
10
- import useLoginFields from '@salesforce/retail-react-app/app/components/forms/useLoginFields'
9
+ import {FormattedMessage} from 'react-intl'
10
+ import {Stack, Box, Button} from '@salesforce/retail-react-app/app/components/shared/ui'
11
11
  import Field from '@salesforce/retail-react-app/app/components/field'
12
+ import useLoginFields from '@salesforce/retail-react-app/app/components/forms/useLoginFields'
12
13
 
13
- const LoginFields = ({form, prefix = ''}) => {
14
+ const LoginFields = ({
15
+ form,
16
+ handleForgotPasswordClick,
17
+ prefix = '',
18
+ hideEmail = false,
19
+ hidePassword = false
20
+ }) => {
14
21
  const fields = useLoginFields({form, prefix})
15
22
  return (
16
23
  <Stack spacing={5}>
17
- <Field {...fields.email} />
18
- <Field {...fields.password} />
24
+ {!hideEmail && <Field {...fields.email} />}
25
+ {!hidePassword && (
26
+ <Stack>
27
+ <Field {...fields.password} />
28
+ {handleForgotPasswordClick && (
29
+ <Box>
30
+ <Button variant="link" size="sm" onClick={handleForgotPasswordClick}>
31
+ <FormattedMessage
32
+ defaultMessage="Forgot password?"
33
+ id="login_form.link.forgot_password"
34
+ />
35
+ </Button>
36
+ </Box>
37
+ )}
38
+ </Stack>
39
+ )}
19
40
  </Stack>
20
41
  )
21
42
  }
22
43
 
23
44
  LoginFields.propTypes = {
45
+ handleForgotPasswordClick: PropTypes.func,
46
+
24
47
  /** Object returned from `useForm` */
25
48
  form: PropTypes.object.isRequired,
26
49
 
27
50
  /** Optional prefix for field names */
28
- prefix: PropTypes.string
51
+ prefix: PropTypes.string,
52
+
53
+ /** Optional configurations */
54
+ hideEmail: PropTypes.bool,
55
+ hidePassword: PropTypes.bool
29
56
  }
30
57
 
31
58
  export default LoginFields
@@ -0,0 +1,64 @@
1
+ /*
2
+ * Copyright (c) 2024, salesforce.com, inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+ import React from 'react'
8
+ import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
9
+ import {useForm} from 'react-hook-form'
10
+ import LoginFields from '@salesforce/retail-react-app/app/components/forms/login-fields'
11
+ import {screen} from '@testing-library/react'
12
+
13
+ const WrapperComponent = ({...props}) => {
14
+ const form = useForm()
15
+ return <LoginFields form={form} handleForgotPasswordClick={() => {}} {...props} />
16
+ }
17
+
18
+ describe('LoginFields component', () => {
19
+ test('renders both email and password fields by default', () => {
20
+ renderWithProviders(<WrapperComponent />)
21
+
22
+ const emailInput = screen.getByLabelText('Email')
23
+ expect(emailInput).toBeInTheDocument()
24
+ expect(emailInput).toHaveAttribute('type', 'email')
25
+
26
+ const passwordInput = screen.getByLabelText('Password')
27
+ expect(passwordInput).toBeInTheDocument()
28
+ expect(passwordInput).toHaveAttribute('type', 'password')
29
+ expect(screen.getByRole('button', {name: 'Forgot password?'})).toBeInTheDocument()
30
+ })
31
+
32
+ test('renders properly when hideEmail is true', () => {
33
+ renderWithProviders(<WrapperComponent hideEmail={true} />)
34
+
35
+ expect(screen.queryByText('Email')).not.toBeInTheDocument()
36
+ expect(screen.queryByRole('textbox', {name: 'Email'})).not.toBeInTheDocument()
37
+
38
+ const passwordInput = screen.getByLabelText('Password')
39
+ expect(passwordInput).toBeInTheDocument()
40
+ expect(passwordInput).toHaveAttribute('type', 'password')
41
+ expect(screen.getByRole('button', {name: 'Forgot password?'})).toBeInTheDocument()
42
+ })
43
+
44
+ test('renders properly when hidePassword is true', () => {
45
+ renderWithProviders(<WrapperComponent hidePassword={true} />)
46
+
47
+ const emailInput = screen.getByLabelText('Email')
48
+ expect(emailInput).toBeInTheDocument()
49
+ expect(emailInput).toHaveAttribute('type', 'email')
50
+
51
+ expect(screen.queryByText('Password')).not.toBeInTheDocument()
52
+ expect(screen.queryByRole('textbox', {name: 'password'})).not.toBeInTheDocument()
53
+ expect(screen.queryByRole('button', {name: 'Forgot password?'})).not.toBeInTheDocument()
54
+ })
55
+
56
+ test('hides "Forgot Password?" button when handleForgotPasswordClick is undefined', () => {
57
+ renderWithProviders(<WrapperComponent handleForgotPasswordClick={undefined} />)
58
+
59
+ const passwordInput = screen.getByLabelText('Password')
60
+ expect(passwordInput).toBeInTheDocument()
61
+ expect(passwordInput).toHaveAttribute('type', 'password')
62
+ expect(screen.queryByRole('button', {name: 'Forgot password?'})).not.toBeInTheDocument()
63
+ })
64
+ })
@@ -6,15 +6,21 @@
6
6
  */
7
7
  import React from 'react'
8
8
  import PropTypes from 'prop-types'
9
+ import {defineMessage, useIntl} from 'react-intl'
9
10
  import {SimpleGrid, Stack} from '@salesforce/retail-react-app/app/components/shared/ui'
10
11
  import useProfileFields from '@salesforce/retail-react-app/app/components/forms/useProfileFields'
11
12
  import Field from '@salesforce/retail-react-app/app/components/field'
12
13
 
13
14
  const ProfileFields = ({form, prefix = ''}) => {
14
15
  const fields = useProfileFields({form, prefix})
16
+ const intl = useIntl()
17
+ const formTitleAriaLabel = defineMessage({
18
+ defaultMessage: 'Profile Form',
19
+ id: 'profile_fields.label.profile_form'
20
+ })
15
21
 
16
22
  return (
17
- <Stack spacing={5}>
23
+ <Stack spacing={5} aria-label={intl.formatMessage(formTitleAriaLabel)}>
18
24
  <SimpleGrid columns={[1, 1, 1, 2]} spacing={5}>
19
25
  <Field {...fields.firstName} />
20
26
  <Field {...fields.lastName} />
@@ -14,8 +14,9 @@ import {Icon, useTheme} from '@salesforce/retail-react-app/app/components/shared
14
14
  // during SSR.
15
15
  // NOTE: Another solution would be to use `require-context.macro` package to accomplish
16
16
  // importing icon svg's.
17
- import '@salesforce/retail-react-app/app/assets/svg/alert.svg'
18
17
  import '@salesforce/retail-react-app/app/assets/svg/account.svg'
18
+ import '@salesforce/retail-react-app/app/assets/svg/alert.svg'
19
+ import '@salesforce/retail-react-app/app/assets/svg/apple.svg'
19
20
  import '@salesforce/retail-react-app/app/assets/svg/basket.svg'
20
21
  import '@salesforce/retail-react-app/app/assets/svg/check.svg'
21
22
  import '@salesforce/retail-react-app/app/assets/svg/check-circle.svg'
@@ -37,6 +38,7 @@ import '@salesforce/retail-react-app/app/assets/svg/flag-it.svg'
37
38
  import '@salesforce/retail-react-app/app/assets/svg/flag-cn.svg'
38
39
  import '@salesforce/retail-react-app/app/assets/svg/flag-jp.svg'
39
40
  import '@salesforce/retail-react-app/app/assets/svg/github-logo.svg'
41
+ import '@salesforce/retail-react-app/app/assets/svg/google.svg'
40
42
  import '@salesforce/retail-react-app/app/assets/svg/hamburger.svg'
41
43
  import '@salesforce/retail-react-app/app/assets/svg/info.svg'
42
44
  import '@salesforce/retail-react-app/app/assets/svg/social-facebook.svg'
@@ -137,9 +139,10 @@ export const icon = (name, passProps, localizationAttributes) => {
137
139
  // Export Chakra icon components that use our SVG sprite symbol internally
138
140
  // For non-square SVGs, we can use the symbol data from the import to set the
139
141
  // proper viewBox attribute on the Icon wrapper.
140
- export const AmexIcon = icon('cc-amex', {viewBox: AmexSymbol.viewBox})
141
- export const AlertIcon = icon('alert')
142
142
  export const AccountIcon = icon('account')
143
+ export const AlertIcon = icon('alert')
144
+ export const AmexIcon = icon('cc-amex', {viewBox: AmexSymbol.viewBox})
145
+ export const AppleIcon = icon('apple')
143
146
  export const BrandLogo = icon('brand-logo', {viewBox: BrandLogoSymbol.viewBox})
144
147
  export const BasketIcon = icon('basket')
145
148
  export const CheckIcon = icon('check')
@@ -163,6 +166,7 @@ export const FlagITIcon = icon('flag-it')
163
166
  export const FlagCNIcon = icon('flag-cn')
164
167
  export const FlagJPIcon = icon('flag-jp')
165
168
  export const GithubLogo = icon('github-logo')
169
+ export const GoogleIcon = icon('google')
166
170
  export const HamburgerIcon = icon('hamburger')
167
171
  export const HeartIcon = icon('heart')
168
172
  export const HeartSolidIcon = icon('heart-solid')
@@ -8,18 +8,23 @@
8
8
  import React, {Fragment} from 'react'
9
9
  import PropTypes from 'prop-types'
10
10
  import {FormattedMessage} from 'react-intl'
11
- import {
12
- Alert,
13
- Box,
14
- Button,
15
- Stack,
16
- Text
17
- } from '@salesforce/retail-react-app/app/components/shared/ui'
11
+ import {Alert, Button, Stack, Text} from '@salesforce/retail-react-app/app/components/shared/ui'
18
12
  import {AlertIcon, BrandLogo} from '@salesforce/retail-react-app/app/components/icons'
19
- import LoginFields from '@salesforce/retail-react-app/app/components/forms/login-fields'
13
+ import StandardLogin from '@salesforce/retail-react-app/app/components/standard-login'
14
+ import PasswordlessLogin from '@salesforce/retail-react-app/app/components/passwordless-login'
20
15
  import {noop} from '@salesforce/retail-react-app/app/utils/utils'
21
16
 
22
- const LoginForm = ({submitForm, clickForgotPassword = noop, clickCreateAccount = noop, form}) => {
17
+ const LoginForm = ({
18
+ submitForm,
19
+ handleForgotPasswordClick,
20
+ handlePasswordlessLoginClick,
21
+ clickCreateAccount = noop,
22
+ form,
23
+ isPasswordlessEnabled = false,
24
+ isSocialEnabled = false,
25
+ idps = [],
26
+ setLoginType
27
+ }) => {
23
28
  return (
24
29
  <Fragment>
25
30
  <Stack justify="center" align="center" spacing={8} marginBottom={8}>
@@ -36,55 +41,46 @@ const LoginForm = ({submitForm, clickForgotPassword = noop, clickCreateAccount =
36
41
  onSubmit={form.handleSubmit(submitForm)}
37
42
  data-testid="sf-auth-modal-form"
38
43
  >
39
- <Stack spacing={8} paddingLeft={4} paddingRight={4}>
40
- {form.formState.errors?.global && (
41
- <Alert status="error">
42
- <AlertIcon color="red.500" boxSize={4} />
43
- <Text fontSize="sm" ml={3}>
44
- {form.formState.errors.global.message}
45
- </Text>
46
- </Alert>
44
+ {form.formState.errors?.global && (
45
+ <Alert status="error" marginBottom={8}>
46
+ <AlertIcon color="red.500" boxSize={4} />
47
+ <Text fontSize="sm" ml={3}>
48
+ {form.formState.errors.global.message}
49
+ </Text>
50
+ </Alert>
51
+ )}
52
+ <Stack spacing={6}>
53
+ {isPasswordlessEnabled ? (
54
+ <PasswordlessLogin
55
+ form={form}
56
+ handleForgotPasswordClick={handleForgotPasswordClick}
57
+ handlePasswordlessLoginClick={handlePasswordlessLoginClick}
58
+ isSocialEnabled={isSocialEnabled}
59
+ idps={idps}
60
+ setLoginType={setLoginType}
61
+ />
62
+ ) : (
63
+ <StandardLogin
64
+ form={form}
65
+ handleForgotPasswordClick={handleForgotPasswordClick}
66
+ isSocialEnabled={isSocialEnabled}
67
+ idps={idps}
68
+ />
47
69
  )}
48
- <Stack>
49
- <LoginFields form={form} />
50
70
 
51
- <Box>
52
- <Button variant="link" size="sm" onClick={clickForgotPassword}>
53
- <FormattedMessage
54
- defaultMessage="Forgot password?"
55
- id="login_form.link.forgot_password"
56
- />
57
- </Button>
58
- </Box>
59
- </Stack>
60
- <Stack spacing={6}>
61
- <Button
62
- type="submit"
63
- onClick={() => {
64
- form.clearErrors('global')
65
- }}
66
- isLoading={form.formState.isSubmitting}
67
- >
71
+ <Stack direction="row" spacing={1} justify="center">
72
+ <Text fontSize="sm">
73
+ <FormattedMessage
74
+ defaultMessage="Don't have an account?"
75
+ id="login_form.message.dont_have_account"
76
+ />
77
+ </Text>
78
+ <Button variant="link" size="sm" onClick={clickCreateAccount}>
68
79
  <FormattedMessage
69
- defaultMessage="Sign In"
70
- id="login_form.button.sign_in"
80
+ defaultMessage="Create account"
81
+ id="login_form.action.create_account"
71
82
  />
72
83
  </Button>
73
-
74
- <Stack direction="row" spacing={1} justify="center">
75
- <Text fontSize="sm">
76
- <FormattedMessage
77
- defaultMessage="Don't have an account?"
78
- id="login_form.message.dont_have_account"
79
- />
80
- </Text>
81
- <Button variant="link" size="sm" onClick={clickCreateAccount}>
82
- <FormattedMessage
83
- defaultMessage="Create account"
84
- id="login_form.action.create_account"
85
- />
86
- </Button>
87
- </Stack>
88
84
  </Stack>
89
85
  </Stack>
90
86
  </form>
@@ -94,9 +90,14 @@ const LoginForm = ({submitForm, clickForgotPassword = noop, clickCreateAccount =
94
90
 
95
91
  LoginForm.propTypes = {
96
92
  submitForm: PropTypes.func,
97
- clickForgotPassword: PropTypes.func,
93
+ handleForgotPasswordClick: PropTypes.func,
98
94
  clickCreateAccount: PropTypes.func,
99
- form: PropTypes.object
95
+ handlePasswordlessLoginClick: PropTypes.func,
96
+ form: PropTypes.object,
97
+ isPasswordlessEnabled: PropTypes.bool,
98
+ isSocialEnabled: PropTypes.bool,
99
+ idps: PropTypes.arrayOf(PropTypes.string),
100
+ setLoginType: PropTypes.func
100
101
  }
101
102
 
102
103
  export default LoginForm
@@ -0,0 +1,76 @@
1
+ /*
2
+ * Copyright (c) 2024, salesforce.com, inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+ import React from 'react'
8
+ import {screen} from '@testing-library/react'
9
+ import LoginForm from '@salesforce/retail-react-app/app/components/login/index'
10
+ import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
11
+ import {useForm} from 'react-hook-form'
12
+
13
+ const WrapperComponent = ({...props}) => {
14
+ const form = useForm()
15
+ return <LoginForm form={form} {...props} />
16
+ }
17
+
18
+ describe('LoginForm', () => {
19
+ describe('isPasswordlessEnabled is enabled', () => {
20
+ test('renders passwordless login form', () => {
21
+ renderWithProviders(<WrapperComponent isPasswordlessEnabled={true} />)
22
+
23
+ expect(screen.getByText(/Welcome Back/)).toBeInTheDocument()
24
+ expect(screen.getByLabelText('Email')).toBeInTheDocument()
25
+ expect(screen.queryByLabelText('Password')).not.toBeInTheDocument()
26
+ expect(screen.getByRole('button', {name: 'Continue Securely'})).toBeInTheDocument()
27
+ expect(screen.getByText(/Or Login With/)).toBeInTheDocument()
28
+ expect(screen.getByRole('button', {name: 'Password'})).toBeInTheDocument()
29
+ expect(screen.getByText(/Don't have an account/)).toBeInTheDocument()
30
+ expect(screen.getByRole('button', {name: 'Create account'})).toBeInTheDocument()
31
+ })
32
+
33
+ test('renders form errors when "Continue Securely" button is clicked', async () => {
34
+ const mockPasswordlessLoginClick = jest.fn()
35
+ const {user} = renderWithProviders(
36
+ <WrapperComponent
37
+ isPasswordlessEnabled={true}
38
+ handlePasswordlessLoginClick={mockPasswordlessLoginClick}
39
+ />
40
+ )
41
+
42
+ await user.click(screen.getByRole('button', {name: 'Continue Securely'}))
43
+ expect(screen.getByText(/Please enter your email address./)).toBeInTheDocument()
44
+ })
45
+
46
+ test('renders form errors when "Password" button is clicked', async () => {
47
+ const mockSetLoginType = jest.fn()
48
+ const {user} = renderWithProviders(
49
+ <WrapperComponent isPasswordlessEnabled={true} setLoginType={mockSetLoginType} />
50
+ )
51
+
52
+ await user.click(screen.getByRole('button', {name: 'Password'}))
53
+ expect(screen.getByText(/Please enter your email address./)).toBeInTheDocument()
54
+ })
55
+ })
56
+
57
+ describe('passwordless is disabled', () => {
58
+ test('renders standard login form', () => {
59
+ renderWithProviders(<WrapperComponent />)
60
+
61
+ expect(screen.getByText(/Welcome Back/)).toBeInTheDocument()
62
+ expect(screen.getByLabelText('Email')).toBeInTheDocument()
63
+ expect(screen.getByLabelText('Password')).toBeInTheDocument()
64
+ expect(screen.getByRole('button', {name: 'Sign In'})).toBeInTheDocument()
65
+ expect(screen.getByText(/Don't have an account/)).toBeInTheDocument()
66
+ expect(screen.getByRole('button', {name: 'Create account'})).toBeInTheDocument()
67
+ })
68
+
69
+ test('renders form errors when "Sign In" button is clicked', async () => {
70
+ const {user} = renderWithProviders(<WrapperComponent />)
71
+
72
+ await user.click(screen.getByRole('button', {name: 'Sign In'}))
73
+ expect(screen.getByText(/Please enter your email address./)).toBeInTheDocument()
74
+ })
75
+ })
76
+ })