From 1f24933870a65836162534c6bf00d26e1d775158 Mon Sep 17 00:00:00 2001 From: Caleb Pollman Date: Fri, 18 Nov 2022 17:19:21 -0800 Subject: [PATCH] feat(rna): React Native Authenticator (#3017) Co-authored-by: wlee221 Co-authored-by: Ioana Brooks <68251134+ioanabrooks@users.noreply.github.com> Co-authored-by: Joe Buono Co-authored-by: Danny Banks --- .github/changeset-presets/bump-versions.md | 2 + .github/workflows/publish-rna.yml | 17 + examples/react-native/.env.sample | 1 + examples/react-native/.gitignore | 2 + examples/react-native/README.md | 46 + .../android/app/src/main/AndroidManifest.xml | 6 + examples/react-native/babel.config.js | 9 + examples/react-native/index.js | 7 +- examples/react-native/ios/Podfile.lock | 28 +- .../ios/ReactNative.xcodeproj/project.pbxproj | 2 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../ios/ReactNative/AppDelegate.mm | 16 + .../react-native/ios/ReactNative/Info.plist | 99 +- examples/react-native/metro.config.js | 17 +- examples/react-native/package.json | 9 +- examples/react-native/src/App/App.tsx | 6 +- .../features/Authenticator/Demo/Example.tsx | 35 + .../features/Authenticator/Styles/Example.tsx | 50 + examples/react-native/src/hooks/index.ts | 1 + .../src/hooks/useDeepLinkingDebug.ts | 27 + examples/react-native/src/ui/index.ts | 10 +- examples/react-native/storybook/index.tsx | 6 +- .../stories/Authenticator.stories.tsx | 183 + .../storybook/stories/Button.stories.tsx | 44 +- .../storybook/stories/Divider.stories.tsx | 31 + .../stories/ErrorMessage.stories.tsx | 54 + .../FederatedProviderButton.stories.tsx | 49 + .../storybook/stories/Icon.stories.tsx | 17 +- .../storybook/stories/Label.stories.tsx | 37 +- .../stories/PasswordField.stories.tsx | 33 + .../stories/PhoneNumberField.stories.tsx | 48 + .../storybook/stories/Radio.stories.tsx | 54 +- .../storybook/stories/RadioGroup.stories.tsx | 97 + .../storybook/stories/Tabs.stories.tsx | 86 + .../storybook/stories/TextField.stories.tsx | 44 + .../react-native/storybook/storyLoader.js | 18 + examples/react-native/types/env.d.ts | 3 + .../authenticator/i18n/i18n.steps.ts | 68 +- packages/react-core/jest.config.js | 10 +- packages/react-core/package.json | 1 + .../hooks/__mock__/components.ts | 106 + .../hooks/__tests__/utils.spec.tsx | 57 + .../src/Authenticator/hooks/constants.ts | 30 + .../src/Authenticator/hooks/index.ts | 4 + .../src/Authenticator/hooks/types.ts | 205 + .../__mock__/useAuthenticator.ts | 64 + .../useAuthenticator.spec.tsx.snap | 4 +- .../__tests__/useAuthenticator.spec.tsx | 2 +- .../useAuthenticator/__tests__/utils.spec.tsx | 58 +- .../hooks/useAuthenticator/constants.ts | 15 - .../hooks/useAuthenticator/index.ts | 2 +- .../hooks/useAuthenticator/types.ts | 6 +- .../useAuthenticator/useAuthenticator.tsx | 18 +- .../hooks/useAuthenticator/utils.ts | 56 +- .../useAuthenticatorInitMachine.spec.tsx | 64 + .../useAuthenticatorInitMachine/index.ts | 1 + .../useAuthenticatorInitMachine.tsx | 25 + .../useAuthenticatorRoute.spec.ts.snap | 181 + .../__tests__/useAuthenticatorRoute.spec.ts | 38 + .../__tests__/utils.spec.ts | 215 + .../hooks/useAuthenticatorRoute/constants.ts | 101 + .../hooks/useAuthenticatorRoute/index.ts | 2 + .../hooks/useAuthenticatorRoute/types.ts | 113 + .../useAuthenticatorRoute.ts | 126 + .../hooks/useAuthenticatorRoute/utils.ts | 208 + .../src/Authenticator/hooks/utils.ts | 38 + .../react-core/src/Authenticator/index.ts | 25 +- .../__snapshots__/index.spec.ts.snap | 6 + .../RenderNothing/RenderNothing.tsx | 2 +- .../__tests__/RenderNothing.spec.tsx | 6 +- .../__tests__/useHasValueUpdated.spec.ts | 51 + .../hooks/__tests__/usePreviousValue.spec.ts | 22 + packages/react-core/src/hooks/index.ts | 2 + .../src/hooks/useHasValueUpdated.ts | 14 + .../react-core/src/hooks/usePreviousValue.ts | 15 + packages/react-core/src/index.ts | 19 + packages/react-native/.eslintrc.js | 8 +- packages/react-native/jest.config.js | 1 + packages/react-native/jest.setup.js | 3 + packages/react-native/package.json | 7 +- .../src/Authenticator/Authenticator.tsx | 97 + .../ConfirmResetPassword.tsx | 69 + .../__tests__/ConfirmResetPassword.spec.tsx | 88 + .../ConfirmResetPassword.spec.tsx.snap | 984 +++ .../Defaults/ConfirmResetPassword/index.ts | 1 + .../Defaults/ConfirmResetPassword/styles.ts | 15 + .../Defaults/ConfirmSignIn/ConfirmSignIn.tsx | 71 + .../__tests__/ConfirmSignIn.spec.tsx | 77 + .../__snapshots__/ConfirmSignIn.spec.tsx.snap | 454 ++ .../Defaults/ConfirmSignIn/index.ts | 1 + .../Defaults/ConfirmSignIn/styles.ts | 15 + .../Defaults/ConfirmSignUp/ConfirmSignUp.tsx | 74 + .../__tests__/ConfirmSignUp.spec.tsx | 63 + .../__snapshots__/ConfirmSignUp.spec.tsx.snap | 442 ++ .../Defaults/ConfirmSignUp/index.ts | 1 + .../Defaults/ConfirmSignUp/style.ts | 15 + .../ConfirmVerifyUser/ConfirmVerifyUser.tsx | 69 + .../__tests__/ConfirmVerifyUser.spec.tsx | 70 + .../ConfirmVerifyUser.spec.tsx.snap | 433 ++ .../Defaults/ConfirmVerifyUser/index.ts | 1 + .../Defaults/ConfirmVerifyUser/styles.ts | 15 + .../ForceNewPassword/ForceNewPassword.tsx | 65 + .../__tests__/ForceNewPassword.spec.tsx | 62 + .../ForceNewPassword.spec.tsx.snap | 300 + .../Defaults/ForceNewPassword/index.ts | 1 + .../Defaults/ForceNewPassword/styles.ts | 15 + .../Defaults/ResetPassword/ResetPassword.tsx | 68 + .../__tests__/ResetPassword.spec.tsx | 77 + .../__snapshots__/ResetPassword.spec.tsx.snap | 454 ++ .../Defaults/ResetPassword/index.ts | 1 + .../Defaults/ResetPassword/styles.ts | 15 + .../Defaults/SetupTOTP/SetupTOTP.tsx | 117 + .../SetupTOTP/__tests__/SetupTOTP.spec.tsx | 117 + .../__snapshots__/SetupTOTP.spec.tsx.snap | 922 +++ .../Authenticator/Defaults/SetupTOTP/index.ts | 1 + .../Defaults/SetupTOTP/styles.ts | 33 + .../Authenticator/Defaults/SignIn/SignIn.tsx | 72 + .../Defaults/SignIn/__tests__/SignIn.spec.tsx | 66 + .../__snapshots__/SignIn.spec.tsx.snap | 1137 ++++ .../Authenticator/Defaults/SignIn/index.ts | 1 + .../Authenticator/Defaults/SignUp/SignUp.tsx | 76 + .../Defaults/SignUp/__tests__/SignUp.spec.tsx | 101 + .../__snapshots__/SignUp.spec.tsx.snap | 3434 +++++++++++ .../Authenticator/Defaults/SignUp/index.ts | 1 + .../Authenticator/Defaults/SignUp/style.ts | 22 + .../Defaults/VerifyUser/VerifyUser.tsx | 68 + .../VerifyUser/__tests__/VerifyUser.spec.tsx | 106 + .../__snapshots__/VerifyUser.spec.tsx.snap | 578 ++ .../Defaults/VerifyUser/index.ts | 1 + .../src/Authenticator/Defaults/index.ts | 11 + .../src/Authenticator/Defaults/types.ts | 146 + .../__tests__/Authenticator.spec.tsx | 101 + .../__snapshots__/Authenticator.spec.tsx.snap | 38 + .../withAuthenticator.spec.tsx.snap | 30 + .../__tests__/withAuthenticator.spec.tsx | 73 + .../DefaultContainer/DefaultContainer.tsx | 16 + .../DefaultContainer/InnerContainer.tsx | 16 + .../__tests__/DefaultContainer.spec.tsx | 19 + .../DefaultContainer.spec.tsx.snap | 49 + .../common/DefaultContainer/index.ts | 2 + .../common/DefaultContainer/styles.ts | 18 + .../common/DefaultContainer/types.ts | 7 + .../common/DefaultContent/DefaultContent.tsx | 90 + .../common/DefaultContent/index.ts | 1 + .../common/DefaultContent/styles.ts | 33 + .../common/DefaultContent/types.ts | 68 + .../common/DefaultFooter/DefaultFooter.tsx | 11 + .../__tests__/DefaultFooter.spec.tsx | 26 + .../__snapshots__/DefaultFooter.spec.tsx.snap | 43 + .../common/DefaultFooter/index.ts | 2 + .../common/DefaultFooter/types.ts | 3 + .../DefaultRadioFormFields.tsx | 45 + .../DefaultTextFormFields.tsx | 55 + .../common/DefaultFormFields/FieldErrors.tsx | 23 + .../__tests__/DefaultFormFields.spec.tsx | 15 + .../__tests__/FieldErrors.spec.tsx | 21 + .../DefaultFormFields.spec.tsx.snap | 3 + .../__snapshots__/FieldErrors.spec.tsx.snap | 14 + .../common/DefaultFormFields/index.ts | 3 + .../common/DefaultFormFields/types.ts | 26 + .../common/DefaultHeader/DefaultHeader.tsx | 16 + .../__tests__/DefaultHeader.spec.tsx | 23 + .../__snapshots__/DefaultHeader.spec.tsx.snap | 24 + .../common/DefaultHeader/index.ts | 2 + .../common/DefaultHeader/types.ts | 3 + .../FederatedProviderButton.tsx | 31 + .../FederatedProviderButton.spec.tsx | 36 + .../FederatedProviderButton.spec.tsx.snap | 93 + .../common/FederatedProviderButton/index.ts | 2 + .../common/FederatedProviderButton/styles.ts | 21 + .../common/FederatedProviderButton/types.ts | 18 + .../FederatedProviderButtons.tsx | 45 + .../FederatedProviderButtons.spec.tsx | 55 + .../FederatedProviderButtons.spec.tsx.snap | 167 + .../common/FederatedProviderButtons/index.ts | 1 + .../common/FederatedProviderButtons/styles.ts | 7 + .../common/FederatedProviderButtons/types.ts | 16 + .../src/Authenticator/common/index.ts | 7 + .../src/Authenticator/hooks/index.ts | 2 + .../src/Authenticator/hooks/types.ts | 39 + .../__tests__/useFieldValues.spec.ts | 248 + .../useFieldValues/__tests__/utils.spec.ts | 212 + .../hooks/useFieldValues/constants.ts | 12 + .../hooks/useFieldValues/index.ts | 2 + .../hooks/useFieldValues/types.ts | 35 + .../hooks/useFieldValues/useFieldValues.ts | 101 + .../hooks/useFieldValues/utils.ts | 187 + .../react-native/src/Authenticator/index.ts | 6 + .../react-native/src/Authenticator/types.ts | 22 + .../src/Authenticator/withAuthenticator.tsx | 18 + .../__snapshots__/BannerMessage.spec.tsx.snap | 34 +- .../__snapshots__/ModalMessage.spec.tsx.snap | 53 +- .../@react-native-clipboard/clipboard.ts | 1 + .../__snapshots__/index.spec.ts.snap | 8 + .../src/assets/icons/amazonLogo.png | Bin 0 -> 898 bytes .../src/assets/icons/amazonLogo@2x.png | Bin 0 -> 2553 bytes .../src/assets/icons/amazonLogo@3x.png | Bin 0 -> 4586 bytes .../src/assets/icons/appleLogo.png | Bin 0 -> 569 bytes .../src/assets/icons/appleLogo@2x.png | Bin 0 -> 1476 bytes .../src/assets/icons/appleLogo@3x.png | Bin 0 -> 2696 bytes .../src/assets/icons/checkboxFilled.png | Bin 399 -> 275 bytes .../src/assets/icons/checkboxFilled@2x.png | Bin 0 -> 514 bytes .../src/assets/icons/checkboxFilled@3x.png | Bin 0 -> 733 bytes .../src/assets/icons/checkboxOutline.png | Bin 303 -> 151 bytes .../src/assets/icons/checkboxOutline@2x.png | Bin 0 -> 257 bytes .../src/assets/icons/checkboxOutline@3x.png | Bin 0 -> 364 bytes .../react-native/src/assets/icons/close.png | Bin 3118 -> 208 bytes .../src/assets/icons/close@2x.png | Bin 0 -> 328 bytes .../src/assets/icons/close@3x.png | Bin 0 -> 442 bytes .../react-native/src/assets/icons/copy.png | Bin 0 -> 200 bytes .../react-native/src/assets/icons/copy@2x.png | Bin 0 -> 350 bytes .../react-native/src/assets/icons/copy@3x.png | Bin 0 -> 512 bytes .../react-native/src/assets/icons/error.png | Bin 0 -> 337 bytes .../src/assets/icons/error@2x.png | Bin 0 -> 670 bytes .../src/assets/icons/error@3x.png | Bin 0 -> 997 bytes .../src/assets/icons/facebookLogo.png | Bin 0 -> 354 bytes .../src/assets/icons/facebookLogo@2x.png | Bin 0 -> 766 bytes .../src/assets/icons/facebookLogo@3x.png | Bin 0 -> 1365 bytes .../src/assets/icons/googleLogo.png | Bin 0 -> 1014 bytes .../src/assets/icons/googleLogo@2x.png | Bin 0 -> 2697 bytes .../src/assets/icons/googleLogo@3x.png | Bin 0 -> 4788 bytes .../react-native/src/assets/icons/index.ts | 8 + .../src/assets/icons/visibilityOff.png | Bin 0 -> 757 bytes .../src/assets/icons/visibilityOff@2x.png | Bin 0 -> 1500 bytes .../src/assets/icons/visibilityOff@3x.png | Bin 0 -> 2229 bytes .../src/assets/icons/visibilityOn.png | Bin 0 -> 488 bytes .../src/assets/icons/visibilityOn@2x.png | Bin 0 -> 1027 bytes .../src/assets/icons/visibilityOn@3x.png | Bin 0 -> 1483 bytes packages/react-native/src/index.ts | 9 + .../src/primitives/Button/Button.tsx | 57 +- .../Button/__tests__/Button.spec.tsx | 82 +- .../__snapshots__/Button.spec.tsx.snap | 106 +- .../src/primitives/Button/styles.ts | 64 +- .../src/primitives/Button/types.ts | 16 +- .../src/primitives/Checkbox/Checkbox.tsx | 17 +- .../Checkbox/__tests__/Checkbox.spec.tsx | 135 +- .../__snapshots__/Checkbox.spec.tsx.snap | 354 +- .../src/primitives/Checkbox/index.ts | 2 +- .../src/primitives/Checkbox/styles.ts | 37 +- .../src/primitives/Checkbox/types.ts | 17 +- .../src/primitives/Divider/Divider.tsx | 34 + .../Divider/__tests__/Divider.spec.tsx | 50 + .../__snapshots__/Divider.spec.tsx.snap | 163 + .../src/primitives/Divider/index.ts | 2 + .../src/primitives/Divider/styles.ts | 29 + .../src/primitives/Divider/types.ts | 21 + .../primitives/ErrorMessage/ErrorMessage.tsx | 44 + .../__tests__/ErrorMessage.spec.tsx | 60 + .../__snapshots__/ErrorMessage.spec.tsx.snap | 113 + .../src/primitives/ErrorMessage/index.ts | 2 + .../src/primitives/ErrorMessage/styles.ts | 32 + .../src/primitives/ErrorMessage/types.ts | 18 + .../src/primitives/Heading/Heading.tsx | 9 +- .../Heading/__tests__/Heading.spec.tsx | 65 +- .../__snapshots__/Heading.spec.tsx.snap | 67 +- .../src/primitives/Heading/styles.ts | 63 +- .../src/primitives/Heading/types.ts | 2 +- .../react-native/src/primitives/Icon/Icon.tsx | 20 +- .../primitives/Icon/__tests__/Icon.spec.tsx | 85 +- .../__snapshots__/Icon.spec.tsx.snap | 87 +- .../src/primitives/Icon/constants.ts | 9 + .../react-native/src/primitives/Icon/index.ts | 3 +- .../src/primitives/Icon/styles.ts | 45 +- .../react-native/src/primitives/Icon/types.ts | 7 +- .../src/primitives/IconButton/IconButton.tsx | 42 +- .../IconButton/__tests__/IconButton.spec.tsx | 66 +- .../__snapshots__/IconButton.spec.tsx.snap | 129 +- .../src/primitives/IconButton/styles.ts | 25 + .../src/primitives/IconButton/types.ts | 5 +- .../src/primitives/Label/Label.tsx | 15 +- .../primitives/Label/__tests__/Label.spec.tsx | 69 +- .../__snapshots__/Label.spec.tsx.snap | 62 +- .../src/primitives/Label/index.ts | 2 +- .../src/primitives/Label/styles.ts | 46 +- .../src/primitives/Label/types.ts | 27 +- .../PasswordField/PasswordField.tsx | 49 + .../__tests__/PasswordField.spec.tsx | 110 + .../__snapshots__/PasswordField.spec.tsx.snap | 603 ++ .../src/primitives/PasswordField/index.ts | 2 + .../src/primitives/PasswordField/styles.ts | 15 + .../src/primitives/PasswordField/types.ts | 27 + .../PhoneNumberField/PhoneNumberField.tsx | 63 + .../__tests__/PhoneNumberField.spec.tsx | 99 + .../PhoneNumberField.spec.tsx.snap | 5430 +++++++++++++++++ .../src/primitives/PhoneNumberField/index.ts | 2 + .../src/primitives/PhoneNumberField/styles.ts | 40 + .../src/primitives/PhoneNumberField/types.ts | 47 + .../src/primitives/Radio/Radio.tsx | 49 +- .../primitives/Radio/__tests__/Radio.spec.tsx | 124 +- .../__snapshots__/Radio.spec.tsx.snap | 172 +- .../primitives/Radio/getRadioDimensions.ts | 27 +- .../src/primitives/Radio/styles.ts | 108 +- .../src/primitives/Radio/types.ts | 23 +- .../src/primitives/RadioGroup/RadioGroup.tsx | 108 + .../RadioGroup/__tests__/RadioGroup.spec.tsx | 183 + .../__snapshots__/RadioGroup.spec.tsx.snap | 1717 ++++++ .../src/primitives/RadioGroup/index.ts | 2 + .../src/primitives/RadioGroup/styles.ts | 21 + .../src/primitives/RadioGroup/types.ts | 27 + .../react-native/src/primitives/Tabs/Tab.tsx | 49 + .../react-native/src/primitives/Tabs/Tabs.tsx | 43 + .../primitives/Tabs/__tests__/Tab.spec.tsx | 68 + .../primitives/Tabs/__tests__/Tabs.spec.tsx | 81 + .../__tests__/__snapshots__/Tab.spec.tsx.snap | 298 + .../__snapshots__/Tabs.spec.tsx.snap | 175 + .../react-native/src/primitives/Tabs/index.ts | 3 + .../src/primitives/Tabs/styles.ts | 62 + .../react-native/src/primitives/Tabs/types.ts | 49 + .../src/primitives/TextField/TextField.tsx | 65 + .../TextField/__tests__/TextField.spec.tsx | 154 + .../__snapshots__/TextField.spec.tsx.snap | 488 ++ .../src/primitives/TextField/index.ts | 2 + .../src/primitives/TextField/styles.ts | 48 + .../src/primitives/TextField/types.ts | 66 + packages/react-native/src/primitives/index.ts | 7 + .../react-native/src/theme/ThemeContext.tsx | 12 + .../react-native/src/theme/ThemeProvider.tsx | 21 + .../theme/__tests__/ThemeProvider.spec.tsx | 23 + .../__snapshots__/useTheme.spec.tsx.snap | 226 + .../src/theme/__tests__/createTheme.spec.ts | 195 + .../src/theme/__tests__/useTheme.spec.tsx | 51 + .../react-native/src/theme/createTheme.ts | 133 + packages/react-native/src/theme/index.ts | 4 + packages/react-native/src/theme/types.ts | 95 + .../src/theme/types/style-dictionary.d.ts | 21 + packages/react-native/src/theme/useTheme.ts | 9 + .../Authenticator/Authenticator.tsx | 17 +- packages/react/src/hooks/useAmplify.ts | 4 +- .../react/src/hooks/useBreakpointValue.ts | 2 + .../shared/__tests__/styleUtils.test.tsx | 22 +- .../shared/__tests__/utils.test.tsx | 16 +- .../shared/responsive/__tests__/utils.test.ts | 26 +- .../src/primitives/shared/responsive/utils.ts | 13 +- .../react/src/primitives/shared/styleUtils.ts | 8 +- packages/react/src/primitives/shared/utils.ts | 11 +- packages/ui/.eslintrc.js | 2 +- packages/ui/package.json | 9 +- packages/ui/scripts/generateCSS.js | 17 - packages/ui/scripts/generateCSS.ts | 28 + packages/ui/sd.config.ts | 44 - .../ui/src/helpers/authenticator/textUtil.ts | 10 +- .../authenticator/defaultTexts.ts | 2 + .../src/i18n/dictionaries/authenticator/en.ts | 1 + .../src/theme/__tests__/createTheme.test.ts | 209 +- .../src/theme/__tests__/defaultTheme.test.ts | 1 - .../ui/src/theme/__tests__/overrides.test.ts | 1 - packages/ui/src/theme/createTheme.ts | 72 +- .../ui/src/theme/defaultDarkModeOverride.ts | 146 +- packages/ui/src/theme/defaultTheme.ts | 4 +- packages/ui/src/theme/index.ts | 8 +- packages/ui/src/theme/tokens/borderWidths.ts | 44 +- packages/ui/src/theme/tokens/colors.ts | 241 +- .../ui/src/theme/tokens/components/alert.ts | 64 +- .../theme/tokens/components/authenticator.ts | 104 +- .../theme/tokens/components/autocomplete.ts | 93 +- .../ui/src/theme/tokens/components/badge.ts | 69 +- .../ui/src/theme/tokens/components/button.ts | 167 +- .../ui/src/theme/tokens/components/card.ts | 44 +- .../src/theme/tokens/components/checkbox.ts | 161 +- .../theme/tokens/components/checkboxField.ts | 21 +- .../src/theme/tokens/components/collection.ts | 59 +- .../ui/src/theme/tokens/components/copy.ts | 36 +- .../theme/tokens/components/dialCodeSelect.ts | 11 +- .../ui/src/theme/tokens/components/divider.ts | 52 +- .../src/theme/tokens/components/expander.ts | 145 +- .../ui/src/theme/tokens/components/field.ts | 34 +- .../theme/tokens/components/fieldControl.ts | 173 +- .../src/theme/tokens/components/fieldGroup.ts | 20 +- .../theme/tokens/components/fieldMessages.ts | 31 +- .../ui/src/theme/tokens/components/flex.ts | 22 +- .../ui/src/theme/tokens/components/heading.ts | 33 +- .../theme/tokens/components/highlightMatch.ts | 14 +- .../ui/src/theme/tokens/components/icon.ts | 12 +- .../ui/src/theme/tokens/components/image.ts | 20 +- .../theme/tokens/components/inAppMessaging.ts | 56 +- .../ui/src/theme/tokens/components/index.ts | 112 +- .../ui/src/theme/tokens/components/link.ts | 38 +- .../ui/src/theme/tokens/components/loader.ts | 83 +- .../ui/src/theme/tokens/components/menu.ts | 61 +- .../src/theme/tokens/components/pagination.ts | 100 +- .../theme/tokens/components/passwordField.ts | 31 +- .../tokens/components/phoneNumberField.ts | 17 +- .../theme/tokens/components/placeholder.ts | 32 +- .../ui/src/theme/tokens/components/radio.ts | 156 +- .../src/theme/tokens/components/radioGroup.ts | 34 +- .../ui/src/theme/tokens/components/rating.ts | 29 +- .../theme/tokens/components/searchField.ts | 38 +- .../ui/src/theme/tokens/components/select.ts | 70 +- .../theme/tokens/components/selectField.ts | 28 +- .../theme/tokens/components/sliderField.ts | 127 +- .../theme/tokens/components/stepperField.ts | 46 +- .../theme/tokens/components/switchField.ts | 113 +- .../ui/src/theme/tokens/components/table.ts | 142 +- .../ui/src/theme/tokens/components/tabs.ts | 90 +- .../ui/src/theme/tokens/components/text.ts | 24 +- .../theme/tokens/components/textAreaField.ts | 23 +- .../src/theme/tokens/components/textField.ts | 23 +- .../theme/tokens/components/toggleButton.ts | 201 +- .../tokens/components/toggleButtonGroup.ts | 21 +- packages/ui/src/theme/tokens/fontSizes.ts | 41 +- packages/ui/src/theme/tokens/fontWeights.ts | 39 +- packages/ui/src/theme/tokens/fonts.ts | 32 +- packages/ui/src/theme/tokens/index.ts | 153 +- packages/ui/src/theme/tokens/lineHeights.ts | 19 +- packages/ui/src/theme/tokens/opacities.ts | 79 +- .../ui/src/theme/tokens/outlineOffsets.ts | 19 +- packages/ui/src/theme/tokens/outlineWidths.ts | 19 +- packages/ui/src/theme/tokens/radii.ts | 30 +- packages/ui/src/theme/tokens/shadows.ts | 21 +- packages/ui/src/theme/tokens/space.ts | 68 +- packages/ui/src/theme/tokens/time.ts | 21 +- packages/ui/src/theme/tokens/transforms.ts | 31 +- .../ui/src/theme/tokens/types/designToken.ts | 304 +- packages/ui/src/theme/tokens/types/scales.ts | 15 - packages/ui/src/theme/types.ts | 18 +- packages/ui/src/theme/utils.ts | 106 +- packages/ui/src/types/authenticator/index.ts | 1 + packages/ui/src/types/authenticator/user.ts | 8 + packages/ui/src/types/authenticator/utils.ts | 6 + yarn.lock | 55 +- 420 files changed, 32605 insertions(+), 3158 deletions(-) create mode 100644 .github/workflows/publish-rna.yml create mode 100644 examples/react-native/.env.sample create mode 100644 examples/react-native/ios/ReactNative.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 examples/react-native/src/features/Authenticator/Demo/Example.tsx create mode 100644 examples/react-native/src/features/Authenticator/Styles/Example.tsx create mode 100644 examples/react-native/src/hooks/index.ts create mode 100644 examples/react-native/src/hooks/useDeepLinkingDebug.ts create mode 100644 examples/react-native/storybook/stories/Authenticator.stories.tsx create mode 100644 examples/react-native/storybook/stories/Divider.stories.tsx create mode 100644 examples/react-native/storybook/stories/ErrorMessage.stories.tsx create mode 100644 examples/react-native/storybook/stories/FederatedProviderButton.stories.tsx create mode 100644 examples/react-native/storybook/stories/PasswordField.stories.tsx create mode 100644 examples/react-native/storybook/stories/PhoneNumberField.stories.tsx create mode 100644 examples/react-native/storybook/stories/RadioGroup.stories.tsx create mode 100644 examples/react-native/storybook/stories/Tabs.stories.tsx create mode 100644 examples/react-native/storybook/stories/TextField.stories.tsx create mode 100644 examples/react-native/types/env.d.ts create mode 100644 packages/react-core/src/Authenticator/hooks/__mock__/components.ts create mode 100644 packages/react-core/src/Authenticator/hooks/__tests__/utils.spec.tsx create mode 100644 packages/react-core/src/Authenticator/hooks/constants.ts create mode 100644 packages/react-core/src/Authenticator/hooks/types.ts create mode 100644 packages/react-core/src/Authenticator/hooks/useAuthenticator/__mock__/useAuthenticator.ts create mode 100644 packages/react-core/src/Authenticator/hooks/useAuthenticatorInitMachine/__tests__/useAuthenticatorInitMachine.spec.tsx create mode 100644 packages/react-core/src/Authenticator/hooks/useAuthenticatorInitMachine/index.ts create mode 100644 packages/react-core/src/Authenticator/hooks/useAuthenticatorInitMachine/useAuthenticatorInitMachine.tsx create mode 100644 packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/__tests__/__snapshots__/useAuthenticatorRoute.spec.ts.snap create mode 100644 packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/__tests__/useAuthenticatorRoute.spec.ts create mode 100644 packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/__tests__/utils.spec.ts create mode 100644 packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/constants.ts create mode 100644 packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/index.ts create mode 100644 packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/types.ts create mode 100644 packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/useAuthenticatorRoute.ts create mode 100644 packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/utils.ts create mode 100644 packages/react-core/src/Authenticator/hooks/utils.ts create mode 100644 packages/react-core/src/hooks/__tests__/useHasValueUpdated.spec.ts create mode 100644 packages/react-core/src/hooks/__tests__/usePreviousValue.spec.ts create mode 100644 packages/react-core/src/hooks/index.ts create mode 100644 packages/react-core/src/hooks/useHasValueUpdated.ts create mode 100644 packages/react-core/src/hooks/usePreviousValue.ts create mode 100644 packages/react-native/jest.setup.js create mode 100644 packages/react-native/src/Authenticator/Authenticator.tsx create mode 100644 packages/react-native/src/Authenticator/Defaults/ConfirmResetPassword/ConfirmResetPassword.tsx create mode 100644 packages/react-native/src/Authenticator/Defaults/ConfirmResetPassword/__tests__/ConfirmResetPassword.spec.tsx create mode 100644 packages/react-native/src/Authenticator/Defaults/ConfirmResetPassword/__tests__/__snapshots__/ConfirmResetPassword.spec.tsx.snap create mode 100644 packages/react-native/src/Authenticator/Defaults/ConfirmResetPassword/index.ts create mode 100644 packages/react-native/src/Authenticator/Defaults/ConfirmResetPassword/styles.ts create mode 100644 packages/react-native/src/Authenticator/Defaults/ConfirmSignIn/ConfirmSignIn.tsx create mode 100644 packages/react-native/src/Authenticator/Defaults/ConfirmSignIn/__tests__/ConfirmSignIn.spec.tsx create mode 100644 packages/react-native/src/Authenticator/Defaults/ConfirmSignIn/__tests__/__snapshots__/ConfirmSignIn.spec.tsx.snap create mode 100644 packages/react-native/src/Authenticator/Defaults/ConfirmSignIn/index.ts create mode 100644 packages/react-native/src/Authenticator/Defaults/ConfirmSignIn/styles.ts create mode 100644 packages/react-native/src/Authenticator/Defaults/ConfirmSignUp/ConfirmSignUp.tsx create mode 100644 packages/react-native/src/Authenticator/Defaults/ConfirmSignUp/__tests__/ConfirmSignUp.spec.tsx create mode 100644 packages/react-native/src/Authenticator/Defaults/ConfirmSignUp/__tests__/__snapshots__/ConfirmSignUp.spec.tsx.snap create mode 100644 packages/react-native/src/Authenticator/Defaults/ConfirmSignUp/index.ts create mode 100644 packages/react-native/src/Authenticator/Defaults/ConfirmSignUp/style.ts create mode 100644 packages/react-native/src/Authenticator/Defaults/ConfirmVerifyUser/ConfirmVerifyUser.tsx create mode 100644 packages/react-native/src/Authenticator/Defaults/ConfirmVerifyUser/__tests__/ConfirmVerifyUser.spec.tsx create mode 100644 packages/react-native/src/Authenticator/Defaults/ConfirmVerifyUser/__tests__/__snapshots__/ConfirmVerifyUser.spec.tsx.snap create mode 100644 packages/react-native/src/Authenticator/Defaults/ConfirmVerifyUser/index.ts create mode 100644 packages/react-native/src/Authenticator/Defaults/ConfirmVerifyUser/styles.ts create mode 100644 packages/react-native/src/Authenticator/Defaults/ForceNewPassword/ForceNewPassword.tsx create mode 100644 packages/react-native/src/Authenticator/Defaults/ForceNewPassword/__tests__/ForceNewPassword.spec.tsx create mode 100644 packages/react-native/src/Authenticator/Defaults/ForceNewPassword/__tests__/__snapshots__/ForceNewPassword.spec.tsx.snap create mode 100644 packages/react-native/src/Authenticator/Defaults/ForceNewPassword/index.ts create mode 100644 packages/react-native/src/Authenticator/Defaults/ForceNewPassword/styles.ts create mode 100644 packages/react-native/src/Authenticator/Defaults/ResetPassword/ResetPassword.tsx create mode 100644 packages/react-native/src/Authenticator/Defaults/ResetPassword/__tests__/ResetPassword.spec.tsx create mode 100644 packages/react-native/src/Authenticator/Defaults/ResetPassword/__tests__/__snapshots__/ResetPassword.spec.tsx.snap create mode 100644 packages/react-native/src/Authenticator/Defaults/ResetPassword/index.ts create mode 100644 packages/react-native/src/Authenticator/Defaults/ResetPassword/styles.ts create mode 100644 packages/react-native/src/Authenticator/Defaults/SetupTOTP/SetupTOTP.tsx create mode 100644 packages/react-native/src/Authenticator/Defaults/SetupTOTP/__tests__/SetupTOTP.spec.tsx create mode 100644 packages/react-native/src/Authenticator/Defaults/SetupTOTP/__tests__/__snapshots__/SetupTOTP.spec.tsx.snap create mode 100644 packages/react-native/src/Authenticator/Defaults/SetupTOTP/index.ts create mode 100644 packages/react-native/src/Authenticator/Defaults/SetupTOTP/styles.ts create mode 100644 packages/react-native/src/Authenticator/Defaults/SignIn/SignIn.tsx create mode 100644 packages/react-native/src/Authenticator/Defaults/SignIn/__tests__/SignIn.spec.tsx create mode 100644 packages/react-native/src/Authenticator/Defaults/SignIn/__tests__/__snapshots__/SignIn.spec.tsx.snap create mode 100644 packages/react-native/src/Authenticator/Defaults/SignIn/index.ts create mode 100644 packages/react-native/src/Authenticator/Defaults/SignUp/SignUp.tsx create mode 100644 packages/react-native/src/Authenticator/Defaults/SignUp/__tests__/SignUp.spec.tsx create mode 100644 packages/react-native/src/Authenticator/Defaults/SignUp/__tests__/__snapshots__/SignUp.spec.tsx.snap create mode 100644 packages/react-native/src/Authenticator/Defaults/SignUp/index.ts create mode 100644 packages/react-native/src/Authenticator/Defaults/SignUp/style.ts create mode 100644 packages/react-native/src/Authenticator/Defaults/VerifyUser/VerifyUser.tsx create mode 100644 packages/react-native/src/Authenticator/Defaults/VerifyUser/__tests__/VerifyUser.spec.tsx create mode 100644 packages/react-native/src/Authenticator/Defaults/VerifyUser/__tests__/__snapshots__/VerifyUser.spec.tsx.snap create mode 100644 packages/react-native/src/Authenticator/Defaults/VerifyUser/index.ts create mode 100644 packages/react-native/src/Authenticator/Defaults/index.ts create mode 100644 packages/react-native/src/Authenticator/Defaults/types.ts create mode 100644 packages/react-native/src/Authenticator/__tests__/Authenticator.spec.tsx create mode 100644 packages/react-native/src/Authenticator/__tests__/__snapshots__/Authenticator.spec.tsx.snap create mode 100644 packages/react-native/src/Authenticator/__tests__/__snapshots__/withAuthenticator.spec.tsx.snap create mode 100644 packages/react-native/src/Authenticator/__tests__/withAuthenticator.spec.tsx create mode 100644 packages/react-native/src/Authenticator/common/DefaultContainer/DefaultContainer.tsx create mode 100644 packages/react-native/src/Authenticator/common/DefaultContainer/InnerContainer.tsx create mode 100644 packages/react-native/src/Authenticator/common/DefaultContainer/__tests__/DefaultContainer.spec.tsx create mode 100644 packages/react-native/src/Authenticator/common/DefaultContainer/__tests__/__snapshots__/DefaultContainer.spec.tsx.snap create mode 100644 packages/react-native/src/Authenticator/common/DefaultContainer/index.ts create mode 100644 packages/react-native/src/Authenticator/common/DefaultContainer/styles.ts create mode 100644 packages/react-native/src/Authenticator/common/DefaultContainer/types.ts create mode 100644 packages/react-native/src/Authenticator/common/DefaultContent/DefaultContent.tsx create mode 100644 packages/react-native/src/Authenticator/common/DefaultContent/index.ts create mode 100644 packages/react-native/src/Authenticator/common/DefaultContent/styles.ts create mode 100644 packages/react-native/src/Authenticator/common/DefaultContent/types.ts create mode 100644 packages/react-native/src/Authenticator/common/DefaultFooter/DefaultFooter.tsx create mode 100644 packages/react-native/src/Authenticator/common/DefaultFooter/__tests__/DefaultFooter.spec.tsx create mode 100644 packages/react-native/src/Authenticator/common/DefaultFooter/__tests__/__snapshots__/DefaultFooter.spec.tsx.snap create mode 100644 packages/react-native/src/Authenticator/common/DefaultFooter/index.ts create mode 100644 packages/react-native/src/Authenticator/common/DefaultFooter/types.ts create mode 100644 packages/react-native/src/Authenticator/common/DefaultFormFields/DefaultRadioFormFields.tsx create mode 100644 packages/react-native/src/Authenticator/common/DefaultFormFields/DefaultTextFormFields.tsx create mode 100644 packages/react-native/src/Authenticator/common/DefaultFormFields/FieldErrors.tsx create mode 100644 packages/react-native/src/Authenticator/common/DefaultFormFields/__tests__/DefaultFormFields.spec.tsx create mode 100644 packages/react-native/src/Authenticator/common/DefaultFormFields/__tests__/FieldErrors.spec.tsx create mode 100644 packages/react-native/src/Authenticator/common/DefaultFormFields/__tests__/__snapshots__/DefaultFormFields.spec.tsx.snap create mode 100644 packages/react-native/src/Authenticator/common/DefaultFormFields/__tests__/__snapshots__/FieldErrors.spec.tsx.snap create mode 100644 packages/react-native/src/Authenticator/common/DefaultFormFields/index.ts create mode 100644 packages/react-native/src/Authenticator/common/DefaultFormFields/types.ts create mode 100644 packages/react-native/src/Authenticator/common/DefaultHeader/DefaultHeader.tsx create mode 100644 packages/react-native/src/Authenticator/common/DefaultHeader/__tests__/DefaultHeader.spec.tsx create mode 100644 packages/react-native/src/Authenticator/common/DefaultHeader/__tests__/__snapshots__/DefaultHeader.spec.tsx.snap create mode 100644 packages/react-native/src/Authenticator/common/DefaultHeader/index.ts create mode 100644 packages/react-native/src/Authenticator/common/DefaultHeader/types.ts create mode 100644 packages/react-native/src/Authenticator/common/FederatedProviderButton/FederatedProviderButton.tsx create mode 100644 packages/react-native/src/Authenticator/common/FederatedProviderButton/__tests__/FederatedProviderButton.spec.tsx create mode 100644 packages/react-native/src/Authenticator/common/FederatedProviderButton/__tests__/__snapshots__/FederatedProviderButton.spec.tsx.snap create mode 100644 packages/react-native/src/Authenticator/common/FederatedProviderButton/index.ts create mode 100644 packages/react-native/src/Authenticator/common/FederatedProviderButton/styles.ts create mode 100644 packages/react-native/src/Authenticator/common/FederatedProviderButton/types.ts create mode 100644 packages/react-native/src/Authenticator/common/FederatedProviderButtons/FederatedProviderButtons.tsx create mode 100644 packages/react-native/src/Authenticator/common/FederatedProviderButtons/__tests__/FederatedProviderButtons.spec.tsx create mode 100644 packages/react-native/src/Authenticator/common/FederatedProviderButtons/__tests__/__snapshots__/FederatedProviderButtons.spec.tsx.snap create mode 100644 packages/react-native/src/Authenticator/common/FederatedProviderButtons/index.ts create mode 100644 packages/react-native/src/Authenticator/common/FederatedProviderButtons/styles.ts create mode 100644 packages/react-native/src/Authenticator/common/FederatedProviderButtons/types.ts create mode 100644 packages/react-native/src/Authenticator/common/index.ts create mode 100644 packages/react-native/src/Authenticator/hooks/index.ts create mode 100644 packages/react-native/src/Authenticator/hooks/types.ts create mode 100644 packages/react-native/src/Authenticator/hooks/useFieldValues/__tests__/useFieldValues.spec.ts create mode 100644 packages/react-native/src/Authenticator/hooks/useFieldValues/__tests__/utils.spec.ts create mode 100644 packages/react-native/src/Authenticator/hooks/useFieldValues/constants.ts create mode 100644 packages/react-native/src/Authenticator/hooks/useFieldValues/index.ts create mode 100644 packages/react-native/src/Authenticator/hooks/useFieldValues/types.ts create mode 100644 packages/react-native/src/Authenticator/hooks/useFieldValues/useFieldValues.ts create mode 100644 packages/react-native/src/Authenticator/hooks/useFieldValues/utils.ts create mode 100644 packages/react-native/src/Authenticator/index.ts create mode 100644 packages/react-native/src/Authenticator/types.ts create mode 100644 packages/react-native/src/Authenticator/withAuthenticator.tsx create mode 100644 packages/react-native/src/__mocks__/@react-native-clipboard/clipboard.ts create mode 100644 packages/react-native/src/assets/icons/amazonLogo.png create mode 100644 packages/react-native/src/assets/icons/amazonLogo@2x.png create mode 100644 packages/react-native/src/assets/icons/amazonLogo@3x.png create mode 100644 packages/react-native/src/assets/icons/appleLogo.png create mode 100644 packages/react-native/src/assets/icons/appleLogo@2x.png create mode 100644 packages/react-native/src/assets/icons/appleLogo@3x.png create mode 100644 packages/react-native/src/assets/icons/checkboxFilled@2x.png create mode 100644 packages/react-native/src/assets/icons/checkboxFilled@3x.png create mode 100644 packages/react-native/src/assets/icons/checkboxOutline@2x.png create mode 100644 packages/react-native/src/assets/icons/checkboxOutline@3x.png create mode 100644 packages/react-native/src/assets/icons/close@2x.png create mode 100644 packages/react-native/src/assets/icons/close@3x.png create mode 100644 packages/react-native/src/assets/icons/copy.png create mode 100644 packages/react-native/src/assets/icons/copy@2x.png create mode 100644 packages/react-native/src/assets/icons/copy@3x.png create mode 100644 packages/react-native/src/assets/icons/error.png create mode 100644 packages/react-native/src/assets/icons/error@2x.png create mode 100644 packages/react-native/src/assets/icons/error@3x.png create mode 100644 packages/react-native/src/assets/icons/facebookLogo.png create mode 100644 packages/react-native/src/assets/icons/facebookLogo@2x.png create mode 100644 packages/react-native/src/assets/icons/facebookLogo@3x.png create mode 100644 packages/react-native/src/assets/icons/googleLogo.png create mode 100644 packages/react-native/src/assets/icons/googleLogo@2x.png create mode 100644 packages/react-native/src/assets/icons/googleLogo@3x.png create mode 100644 packages/react-native/src/assets/icons/visibilityOff.png create mode 100644 packages/react-native/src/assets/icons/visibilityOff@2x.png create mode 100644 packages/react-native/src/assets/icons/visibilityOff@3x.png create mode 100644 packages/react-native/src/assets/icons/visibilityOn.png create mode 100644 packages/react-native/src/assets/icons/visibilityOn@2x.png create mode 100644 packages/react-native/src/assets/icons/visibilityOn@3x.png create mode 100644 packages/react-native/src/primitives/Divider/Divider.tsx create mode 100644 packages/react-native/src/primitives/Divider/__tests__/Divider.spec.tsx create mode 100644 packages/react-native/src/primitives/Divider/__tests__/__snapshots__/Divider.spec.tsx.snap create mode 100644 packages/react-native/src/primitives/Divider/index.ts create mode 100644 packages/react-native/src/primitives/Divider/styles.ts create mode 100644 packages/react-native/src/primitives/Divider/types.ts create mode 100644 packages/react-native/src/primitives/ErrorMessage/ErrorMessage.tsx create mode 100644 packages/react-native/src/primitives/ErrorMessage/__tests__/ErrorMessage.spec.tsx create mode 100644 packages/react-native/src/primitives/ErrorMessage/__tests__/__snapshots__/ErrorMessage.spec.tsx.snap create mode 100644 packages/react-native/src/primitives/ErrorMessage/index.ts create mode 100644 packages/react-native/src/primitives/ErrorMessage/styles.ts create mode 100644 packages/react-native/src/primitives/ErrorMessage/types.ts create mode 100644 packages/react-native/src/primitives/Icon/constants.ts create mode 100644 packages/react-native/src/primitives/PasswordField/PasswordField.tsx create mode 100644 packages/react-native/src/primitives/PasswordField/__tests__/PasswordField.spec.tsx create mode 100644 packages/react-native/src/primitives/PasswordField/__tests__/__snapshots__/PasswordField.spec.tsx.snap create mode 100644 packages/react-native/src/primitives/PasswordField/index.ts create mode 100644 packages/react-native/src/primitives/PasswordField/styles.ts create mode 100644 packages/react-native/src/primitives/PasswordField/types.ts create mode 100644 packages/react-native/src/primitives/PhoneNumberField/PhoneNumberField.tsx create mode 100644 packages/react-native/src/primitives/PhoneNumberField/__tests__/PhoneNumberField.spec.tsx create mode 100644 packages/react-native/src/primitives/PhoneNumberField/__tests__/__snapshots__/PhoneNumberField.spec.tsx.snap create mode 100644 packages/react-native/src/primitives/PhoneNumberField/index.ts create mode 100644 packages/react-native/src/primitives/PhoneNumberField/styles.ts create mode 100644 packages/react-native/src/primitives/PhoneNumberField/types.ts create mode 100644 packages/react-native/src/primitives/RadioGroup/RadioGroup.tsx create mode 100644 packages/react-native/src/primitives/RadioGroup/__tests__/RadioGroup.spec.tsx create mode 100644 packages/react-native/src/primitives/RadioGroup/__tests__/__snapshots__/RadioGroup.spec.tsx.snap create mode 100644 packages/react-native/src/primitives/RadioGroup/index.ts create mode 100644 packages/react-native/src/primitives/RadioGroup/styles.ts create mode 100644 packages/react-native/src/primitives/RadioGroup/types.ts create mode 100644 packages/react-native/src/primitives/Tabs/Tab.tsx create mode 100644 packages/react-native/src/primitives/Tabs/Tabs.tsx create mode 100644 packages/react-native/src/primitives/Tabs/__tests__/Tab.spec.tsx create mode 100644 packages/react-native/src/primitives/Tabs/__tests__/Tabs.spec.tsx create mode 100644 packages/react-native/src/primitives/Tabs/__tests__/__snapshots__/Tab.spec.tsx.snap create mode 100644 packages/react-native/src/primitives/Tabs/__tests__/__snapshots__/Tabs.spec.tsx.snap create mode 100644 packages/react-native/src/primitives/Tabs/index.ts create mode 100644 packages/react-native/src/primitives/Tabs/styles.ts create mode 100644 packages/react-native/src/primitives/Tabs/types.ts create mode 100644 packages/react-native/src/primitives/TextField/TextField.tsx create mode 100644 packages/react-native/src/primitives/TextField/__tests__/TextField.spec.tsx create mode 100644 packages/react-native/src/primitives/TextField/__tests__/__snapshots__/TextField.spec.tsx.snap create mode 100644 packages/react-native/src/primitives/TextField/index.ts create mode 100644 packages/react-native/src/primitives/TextField/styles.ts create mode 100644 packages/react-native/src/primitives/TextField/types.ts create mode 100644 packages/react-native/src/theme/ThemeContext.tsx create mode 100644 packages/react-native/src/theme/ThemeProvider.tsx create mode 100644 packages/react-native/src/theme/__tests__/ThemeProvider.spec.tsx create mode 100644 packages/react-native/src/theme/__tests__/__snapshots__/useTheme.spec.tsx.snap create mode 100644 packages/react-native/src/theme/__tests__/createTheme.spec.ts create mode 100644 packages/react-native/src/theme/__tests__/useTheme.spec.tsx create mode 100644 packages/react-native/src/theme/createTheme.ts create mode 100644 packages/react-native/src/theme/index.ts create mode 100644 packages/react-native/src/theme/types.ts create mode 100644 packages/react-native/src/theme/types/style-dictionary.d.ts create mode 100644 packages/react-native/src/theme/useTheme.ts delete mode 100755 packages/ui/scripts/generateCSS.js create mode 100755 packages/ui/scripts/generateCSS.ts delete mode 100644 packages/ui/sd.config.ts delete mode 100644 packages/ui/src/theme/tokens/types/scales.ts create mode 100644 packages/ui/src/types/authenticator/utils.ts diff --git a/.github/changeset-presets/bump-versions.md b/.github/changeset-presets/bump-versions.md index fdec16c1f0b..0fdd15263a4 100644 --- a/.github/changeset-presets/bump-versions.md +++ b/.github/changeset-presets/bump-versions.md @@ -3,6 +3,8 @@ '@aws-amplify/ui-angular': patch '@aws-amplify/ui-react': patch '@aws-amplify/ui-vue': patch +'@aws-amplify/ui-react-core': patch +'@aws-amplify/ui-react-native': patch --- Version bump for all public packages diff --git a/.github/workflows/publish-rna.yml b/.github/workflows/publish-rna.yml new file mode 100644 index 00000000000..d21a40a8b8e --- /dev/null +++ b/.github/workflows/publish-rna.yml @@ -0,0 +1,17 @@ +# Description: This workflow runs unit + e2e tests +# +# Triggered by: merge to `rna/release` branch + +name: Publish / RNA + +on: + push: + branches: [rna/release] + +jobs: + publish: + uses: ./.github/workflows/reusable-tagged-publish.yml + with: + dist-tag: rna + secrets: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/examples/react-native/.env.sample b/examples/react-native/.env.sample new file mode 100644 index 00000000000..5bae3646d47 --- /dev/null +++ b/examples/react-native/.env.sample @@ -0,0 +1 @@ +GREETING='Hello World!' diff --git a/examples/react-native/.gitignore b/examples/react-native/.gitignore index 81570d99185..da12d60371a 100644 --- a/examples/react-native/.gitignore +++ b/examples/react-native/.gitignore @@ -59,3 +59,5 @@ buck-out/ # Ruby / CocoaPods /ios/Pods/ /vendor/bundle/ + +aws-exports.js diff --git a/examples/react-native/README.md b/examples/react-native/README.md index 88dc92d58ee..19c04e8e48a 100644 --- a/examples/react-native/README.md +++ b/examples/react-native/README.md @@ -129,6 +129,52 @@ To include an Amplify UI package as a dependency add it to the `dependencies` fi > Only internal packages within the _packages_ directory are resolved in _metro.config.js_ +## Adding Dependencies with Native Modules or direct React usage required by `@aws-amplify/ui-react-native` + +Metro needs to be informed of the location of dependencies with native modules and dependencies that use React directly. Any new dependency added to `@aws-amplify/ui-react-native` with native modules or a dependency on React will need to be added to the `config.resolver.extraNodeModules` field of the _metro.config.js_ with the path to resolve, example: + +``` +config.resolver.extraNodeModules = { + 'react-native': path.resolve(__dirname, 'node_modules/react-native'), +} +``` + +## Using env variables + +Add a local _.env_ file, then copy/paste the contents of _.env.sample_ inside, updating the values as needed: + +```sh +# .env +GREETING='Hello World!' +``` + +### Adding `env` variables + +Add your variable name to _.env_ (and _.env.sample_ if committing) and to _./types/env.d.ts_ to appease typescript: + +```sh +# .env +GREETING='Hello World!' +MY_ENV_VARIABLE=FOO +``` + +```ts +// ./types/env.d.ts +declare module '@env' { + export const GREETING: string; + export const MY_ENV_VARIABLE: string; +} +``` + +To use your newly added env variable: + +```ts +// *.tsx +import { MY_ENV_VARIABLE } from '@env'; +``` + +If the example app is not picking up changes to the values in _.env_ close Metro and reset the cache (see troubleshooting section). + ## Troubleshooting ### Cleaning the Metro Cache diff --git a/examples/react-native/android/app/src/main/AndroidManifest.xml b/examples/react-native/android/app/src/main/AndroidManifest.xml index a81aadef0a4..7645a6e2a4d 100644 --- a/examples/react-native/android/app/src/main/AndroidManifest.xml +++ b/examples/react-native/android/app/src/main/AndroidManifest.xml @@ -21,6 +21,12 @@ + + + + + + diff --git a/examples/react-native/babel.config.js b/examples/react-native/babel.config.js index f842b77fcfb..120bc451569 100644 --- a/examples/react-native/babel.config.js +++ b/examples/react-native/babel.config.js @@ -1,3 +1,12 @@ module.exports = { presets: ['module:metro-react-native-babel-preset'], + plugins: [ + [ + 'module:react-native-dotenv', + { + moduleName: '@env', + path: '.env', + }, + ], + ], }; diff --git a/examples/react-native/index.js b/examples/react-native/index.js index fb76ded9e4c..ffc9f206e71 100644 --- a/examples/react-native/index.js +++ b/examples/react-native/index.js @@ -1,10 +1,15 @@ +// These polyfills are required by Amplify JS, +// but are commonly found in most React Native apps +import 'react-native-get-random-values'; +import 'react-native-url-polyfill/auto'; + import { AppRegistry } from 'react-native'; import { App } from './src/App'; import { name as appName } from './app.json'; import { setupStorybook } from './storybook'; //TODO: replace with env var -const initStorybook = true; +const initStorybook = false; const { StorybookRoot } = setupStorybook(initStorybook); diff --git a/examples/react-native/ios/Podfile.lock b/examples/react-native/ios/Podfile.lock index afe26e1c9fd..2bf91587dcf 100644 --- a/examples/react-native/ios/Podfile.lock +++ b/examples/react-native/ios/Podfile.lock @@ -282,9 +282,11 @@ PODS: - React-jsinspector (0.68.1) - React-logger (0.68.1): - glog + - react-native-get-random-values (1.8.0): + - React-Core - react-native-netinfo (8.3.1): - React-Core - - react-native-safe-area-context (4.3.3): + - react-native-safe-area-context (4.4.1): - RCT-Folly - RCTRequired - RCTTypeSafety @@ -355,8 +357,14 @@ PODS: - React-jsi (= 0.68.1) - React-logger (= 0.68.1) - React-perflogger (= 0.68.1) + - RNAWSCognito (5.2.11): + - React-Core - RNCAsyncStorage (1.17.10): - React-Core + - RNCClipboard (1.11.1): + - React-Core + - RNCPicker (2.4.8): + - React-Core - SocketRocket (0.6.0) - Yoga (1.14.0) - YogaKit (1.18.1): @@ -405,6 +413,7 @@ DEPENDENCIES: - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) + - react-native-get-random-values (from `../../../node_modules/react-native-get-random-values`) - "react-native-netinfo (from `../../../node_modules/@react-native-community/netinfo`)" - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) @@ -419,7 +428,10 @@ DEPENDENCIES: - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - RNAWSCognito (from `../node_modules/amazon-cognito-identity-js`) - "RNCAsyncStorage (from `../../../node_modules/@react-native-async-storage/async-storage`)" + - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" + - "RNCPicker (from `../node_modules/@react-native-picker/picker`)" - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: @@ -477,6 +489,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/jsinspector" React-logger: :path: "../node_modules/react-native/ReactCommon/logger" + react-native-get-random-values: + :path: "../../../node_modules/react-native-get-random-values" react-native-netinfo: :path: "../../../node_modules/@react-native-community/netinfo" react-native-safe-area-context: @@ -505,8 +519,14 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/runtimeexecutor" ReactCommon: :path: "../node_modules/react-native/ReactCommon" + RNAWSCognito: + :path: "../node_modules/amazon-cognito-identity-js" RNCAsyncStorage: :path: "../../../node_modules/@react-native-async-storage/async-storage" + RNCClipboard: + :path: "../node_modules/@react-native-clipboard/clipboard" + RNCPicker: + :path: "../node_modules/@react-native-picker/picker" Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" @@ -542,8 +562,9 @@ SPEC CHECKSUMS: React-jsiexecutor: 4a4bae5671b064a2248a690cf75957669489d08c React-jsinspector: 218a2503198ff28a085f8e16622a8d8f507c8019 React-logger: f79dd3cc0f9b44f5611c6c7862badd891a862cf8 + react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a react-native-netinfo: 1a6035d3b9780221d407c277ebfb5722ace00658 - react-native-safe-area-context: b456e1c40ec86f5593d58b275bd0e9603169daca + react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a React-perflogger: 30ab8d6db10e175626069e742eead3ebe8f24fd5 React-RCTActionSheet: 4b45da334a175b24dabe75f856b98fed3dfd6201 React-RCTAnimation: d6237386cb04500889877845b3e9e9291146bc2e @@ -556,7 +577,10 @@ SPEC CHECKSUMS: React-RCTVibration: 9e344c840176b0af9c84d5019eb4fed8b3c105a1 React-runtimeexecutor: 7285b499d0339104b2813a1f58ad1ada4adbd6c0 ReactCommon: bf2888a826ceedf54b99ad1b6182d1bc4a8a3984 + RNAWSCognito: 9554c635fc9bfdc86d5d40084c792c318c9706bf RNCAsyncStorage: 0c357f3156fcb16c8589ede67cc036330b6698ca + RNCClipboard: 2834e1c4af68697089cdd455ee4a4cdd198fa7dd + RNCPicker: 0bf8ef8f7800524f32d2bb2a8bcadd53eda0ecd1 SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608 Yoga: 17cd9a50243093b547c1e539c749928dd68152da YogaKit: f782866e155069a2cca2517aafea43200b01fd5a diff --git a/examples/react-native/ios/ReactNative.xcodeproj/project.pbxproj b/examples/react-native/ios/ReactNative.xcodeproj/project.pbxproj index 4f052d60929..da69e3c3ae8 100644 --- a/examples/react-native/ios/ReactNative.xcodeproj/project.pbxproj +++ b/examples/react-native/ios/ReactNative.xcodeproj/project.pbxproj @@ -576,6 +576,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = "$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, @@ -640,6 +641,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = "$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, diff --git a/examples/react-native/ios/ReactNative.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/react-native/ios/ReactNative.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000000..18d981003d6 --- /dev/null +++ b/examples/react-native/ios/ReactNative.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/react-native/ios/ReactNative/AppDelegate.mm b/examples/react-native/ios/ReactNative/AppDelegate.mm index c13e671b8c4..b0944172217 100644 --- a/examples/react-native/ios/ReactNative/AppDelegate.mm +++ b/examples/react-native/ios/ReactNative/AppDelegate.mm @@ -3,6 +3,7 @@ #import #import #import +#import #import @@ -66,6 +67,21 @@ - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge #endif } +- (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + options:(NSDictionary *)options +{ + return [RCTLinkingManager application:application openURL:url options:options]; +} + +- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity + restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler +{ + return [RCTLinkingManager application:application + continueUserActivity:userActivity + restorationHandler:restorationHandler]; +} + #if RCT_NEW_ARCH_ENABLED #pragma mark - RCTCxxBridgeDelegate diff --git a/examples/react-native/ios/ReactNative/Info.plist b/examples/react-native/ios/ReactNative/Info.plist index dbd7000fa96..374bd9619d8 100644 --- a/examples/react-native/ios/ReactNative/Info.plist +++ b/examples/react-native/ios/ReactNative/Info.plist @@ -1,55 +1,64 @@ - - CFBundleDevelopmentRegion - en - CFBundleDisplayName - ReactNative - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - NSAppTransportSecurity - NSExceptionDomains + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ReactNative + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSAppTransportSecurity - localhost + NSExceptionDomains - NSExceptionAllowsInsecureHTTPLoads - + localhost + + NSExceptionAllowsInsecureHTTPLoads + + + NSLocationWhenInUseUsageDescription + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CFBundleURLTypes + + + CFBundleURLSchemes + + myapp + + + - NSLocationWhenInUseUsageDescription - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - diff --git a/examples/react-native/metro.config.js b/examples/react-native/metro.config.js index c5592790062..681cf3bb3b8 100644 --- a/examples/react-native/metro.config.js +++ b/examples/react-native/metro.config.js @@ -93,11 +93,24 @@ config.resolver.blockList = [ ...usedInternalPackagePaths, ]; -// point to the example app react-native dep +// point to the example app deps for React related and deps requiring native modules config.resolver.extraNodeModules = { - '@xstate/react': path.resolve(__dirname, 'node_modules/@xstate/react'), + // core React and React Native dependencies react: path.resolve(__dirname, 'node_modules/react'), 'react-native': path.resolve(__dirname, 'node_modules/react-native'), + + // xstate uses React under the hood + '@xstate/react': path.resolve(__dirname, 'node_modules/@xstate/react'), + + // depedendencies with native modules + '@react-native-clipboard/clipboard': path.resolve( + __dirname, + 'node_modules/@react-native-clipboard/clipboard' + ), + '@react-native-picker/picker': path.resolve( + __dirname, + 'node_modules/@react-native-picker/picker' + ), 'react-native-safe-area-context': path.resolve( __dirname, 'node_modules/react-native-safe-area-context' diff --git a/examples/react-native/package.json b/examples/react-native/package.json index f36bc69cf2f..5e7f3ecef09 100644 --- a/examples/react-native/package.json +++ b/examples/react-native/package.json @@ -17,11 +17,16 @@ "@aws-amplify/ui-react-core": "*", "@aws-amplify/ui-react-native": "*", "@react-native-async-storage/async-storage": "^1.17.5", + "@react-native-clipboard/clipboard": "^1.11.1", "@react-native-community/netinfo": "^8.3.0", + "@react-native-picker/picker": "^2.4.8", "@xstate/react": "3.0.0", + "amazon-cognito-identity-js": "^5.2.11", "react": "17.0.2", "react-native": "0.68.1", - "react-native-safe-area-context": "^4.2.5" + "react-native-get-random-values": "^1.8.0", + "react-native-safe-area-context": "^4.2.5", + "react-native-url-polyfill": "^1.3.0" }, "devDependencies": { "@babel/core": "^7.17.10", @@ -36,6 +41,7 @@ "@storybook/react-native-server": "^5.3.23", "@types/jest": "^26.0.23", "@types/react-native": "^0.67.3", + "@types/react-native-dotenv": "^0.2.0", "@types/react-test-renderer": "^17.0.1", "@typescript-eslint/eslint-plugin": "^5.17.0", "@typescript-eslint/parser": "^5.17.0", @@ -45,6 +51,7 @@ "jest": "^26.6.3", "metro-react-native-babel-preset": "^0.70.2", "react-dom": "17.0.2", + "react-native-dotenv": "^3.4.0", "react-test-renderer": "^17.0.2", "typescript": "^4.6.3" }, diff --git a/examples/react-native/src/App/App.tsx b/examples/react-native/src/App/App.tsx index 0e903c0556f..23904d0dc7d 100644 --- a/examples/react-native/src/App/App.tsx +++ b/examples/react-native/src/App/App.tsx @@ -1,9 +1,11 @@ import React from 'react'; -import { Demo as InAppDemo } from '../features/InAppMessaging'; +// import Example from '../features/Authenticator/Demo/Example'; +import Example from '../features/Authenticator/Styles/Example'; +// import { Demo as InAppDemo } from '../features/InAppMessaging'; const App = () => { - return ; + return ; }; export default App; diff --git a/examples/react-native/src/features/Authenticator/Demo/Example.tsx b/examples/react-native/src/features/Authenticator/Demo/Example.tsx new file mode 100644 index 00000000000..15bddc02813 --- /dev/null +++ b/examples/react-native/src/features/Authenticator/Demo/Example.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { StyleSheet, View } from 'react-native'; + +import { Authenticator, useAuthenticator } from '@aws-amplify/ui-react-native'; +import { Amplify } from 'aws-amplify'; + +import { Button } from '../../../ui'; + +// replace with actual amplify config from environments +// import config from '../../../aws-exports'; + +Amplify.configure({}); + +function SignOutButton() { + const { signOut } = useAuthenticator(); + return ; +} + +function App() { + return ( + + + + + + + + ); +} + +const style = StyleSheet.create({ + container: { flex: 1, alignItems: 'center', justifyContent: 'center' }, +}); + +export default App; diff --git a/examples/react-native/src/features/Authenticator/Styles/Example.tsx b/examples/react-native/src/features/Authenticator/Styles/Example.tsx new file mode 100644 index 00000000000..490f8a73e10 --- /dev/null +++ b/examples/react-native/src/features/Authenticator/Styles/Example.tsx @@ -0,0 +1,50 @@ +import React, { ReactNode } from 'react'; +import { StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native'; + +import { Authenticator, useAuthenticator } from '@aws-amplify/ui-react-native'; +import { Amplify } from 'aws-amplify'; + +import { Button } from '../../../ui'; + +Amplify.configure({}); + +const MyHeader = ({ + children, + style, +}: { + children?: ReactNode; + style?: StyleProp; +}) => ( + + {children} + +); + +function SignOutButton() { + const { signOut } = useAuthenticator(); + return ; +} + +function App() { + return ( + + ( + + ), + }} + > + + + + + + ); +} + +const style = StyleSheet.create({ + container: { flex: 1, alignItems: 'center', justifyContent: 'center' }, +}); + +export default App; diff --git a/examples/react-native/src/hooks/index.ts b/examples/react-native/src/hooks/index.ts new file mode 100644 index 00000000000..66bdfba2dab --- /dev/null +++ b/examples/react-native/src/hooks/index.ts @@ -0,0 +1 @@ +export { default as useDeepLinkingDebug } from './useDeepLinkingDebug'; diff --git a/examples/react-native/src/hooks/useDeepLinkingDebug.ts b/examples/react-native/src/hooks/useDeepLinkingDebug.ts new file mode 100644 index 00000000000..17ee3287898 --- /dev/null +++ b/examples/react-native/src/hooks/useDeepLinkingDebug.ts @@ -0,0 +1,27 @@ +import { useEffect } from 'react'; +import { Linking } from 'react-native'; + +export const deepLinkHandler = ( + url: string | null | { url: string }, + shouldLog = true +) => { + if (!url) { + return; + } + + if (shouldLog) { + console.log('Detected url:', url); + } +}; + +export default async function useDeepLinking(shouldLog = true): Promise { + useEffect(() => { + Linking.addEventListener('url', deepLinkHandler); + }, []); + + try { + deepLinkHandler(await Linking.getInitialURL(), shouldLog); + } catch (e) { + console.log(`Cold boot deep link error: ${e}`); + } +} diff --git a/examples/react-native/src/ui/index.ts b/examples/react-native/src/ui/index.ts index 7b4255c8f5c..ce02cd6ef9c 100644 --- a/examples/react-native/src/ui/index.ts +++ b/examples/react-native/src/ui/index.ts @@ -1,13 +1,5 @@ /** * Re-export internal React Native UI primitives and types for use in example apps and storybook. - * This file should be removed when primitves are exposed for external usage + * This file should be removed when primitives are exposed for external usage */ export * from '@aws-amplify/ui-react-native/dist/primitives'; - -/** - * The below components should be migrated to ui-react-native/src/primitives once completed, - * e.g. have full type documentation, controlled vs uncontrolled handling, stories, touch feedback, - * accessibility props, unit tests, etc - */ - -export * from './RadioGroup'; diff --git a/examples/react-native/storybook/index.tsx b/examples/react-native/storybook/index.tsx index 0303b99619e..900eeae5e0c 100644 --- a/examples/react-native/storybook/index.tsx +++ b/examples/react-native/storybook/index.tsx @@ -8,7 +8,7 @@ import { import { withKnobs } from '@storybook/addon-knobs'; import noop from 'lodash/noop'; import { loadStories } from './storyLoader'; -import { Screen } from './ui/Screen'; +import { DefaultContainer } from '@aws-amplify/ui-react-native/src/Authenticator/common'; const STORYBOOK_REQUIRE_CYCLE_PREFIX = 'Require cycle: node_modules/core-js/internals/microtask.js'; @@ -26,9 +26,9 @@ export function setupStorybook(initStorybook: boolean) { // add decorators addDecorator(withKnobs); addDecorator((Story: any) => ( - + - + )); } diff --git a/examples/react-native/storybook/stories/Authenticator.stories.tsx b/examples/react-native/storybook/stories/Authenticator.stories.tsx new file mode 100644 index 00000000000..dc7007df43a --- /dev/null +++ b/examples/react-native/storybook/stories/Authenticator.stories.tsx @@ -0,0 +1,183 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react-native'; +import { AuthChallengeName, CodeDeliveryDetails } from '@aws-amplify/ui'; +import { GetTotpSecretCode } from '@aws-amplify/ui-react-core/src/Authenticator/hooks'; +import { InnerContainer } from '@aws-amplify/ui-react-native/src/Authenticator/common'; +import { Authenticator } from '@aws-amplify/ui-react-native'; +import noop from 'lodash/noop'; + +const componentFields = { + code: { + name: 'code', + label: 'Code', + placeholder: 'Code', + type: 'default' as const, + }, + confirmPassword: { + name: 'confirmPassword', + label: 'Confirm Password', + placeholder: 'Confirm Password', + type: 'password' as const, + }, + newPassword: { + name: 'newPassword', + label: 'New Password', + placeholder: 'New Password', + type: 'password' as const, + }, + password: { + name: 'password', + label: 'Password', + placeholder: 'Password', + type: 'password' as const, + }, + phone: { + name: 'phone', + label: 'Phone', + placeholder: 'Phone', + type: 'phone' as const, + }, + username: { + name: 'username', + label: 'Username', + placeholder: 'Username', + type: 'default' as const, + }, +}; + +type Fields = keyof typeof componentFields; + +const getComponentFields = (fields: Fields[]) => { + return { + fields: fields.map((field) => componentFields[field]), + }; +}; + +const getComponentSlots = (subcomponent: any) => { + return { + Footer: subcomponent.Footer, + FormFields: subcomponent.FormFields, + Header: subcomponent.Header, + }; +}; + +const sharedProps = { + error: null as unknown as string, + handleBlur: noop, + handleChange: noop, + handleSubmit: (values: any) => { + console.log('Values', values); + }, + isPending: false, +}; + +const confirmResetPasswordProps = { + ...sharedProps, + ...getComponentFields(['code', 'newPassword', 'confirmPassword']), + ...getComponentSlots(Authenticator.ConfirmResetPassword), + resendCode: noop, +}; + +const confirmSignInProps = { + ...sharedProps, + ...getComponentFields(['code']), + ...getComponentSlots(Authenticator.ConfirmSignIn), + challengeName: 'SMS_MFA' as AuthChallengeName, + toSignIn: noop, +}; + +const confirmSignUpProps = { + ...sharedProps, + ...getComponentFields(['code']), + ...getComponentSlots(Authenticator.ConfirmSignUp), + codeDeliveryDetails: { + AttributeName: 'email', + DeliveryMedium: 'EMAIL', + Destination: 'a***@e***.com', + } as CodeDeliveryDetails, + resendCode: noop, +}; + +const confirmVerifyUserProps = { + ...sharedProps, + ...getComponentFields(['username']), + ...getComponentSlots(Authenticator.ConfirmResetPassword), + skipVerification: noop, +}; + +const forceNewPasswordProps = { + ...sharedProps, + ...getComponentFields(['newPassword', 'confirmPassword']), + ...getComponentSlots(Authenticator.ForceNewPassword), + toSignIn: noop, +}; + +const resetPasswordProps = { + ...sharedProps, + ...getComponentFields(['username']), + ...getComponentSlots(Authenticator.ResetPassword), + toSignIn: noop, +}; + +const setupTOTPProps = { + ...sharedProps, + ...getComponentFields(['code']), + ...getComponentSlots(Authenticator.SetupTOTP), + getTotpSecretCode: noop as GetTotpSecretCode, + toSignIn: noop, +}; + +const signInProps = { + ...sharedProps, + ...getComponentFields(['username', 'password']), + ...getComponentSlots(Authenticator.SignIn), + toFederatedSignIn: noop, + toResetPassword: noop, + toSignUp: noop, +}; + +const signUpProps = { + ...sharedProps, + ...getComponentFields(['username', 'password', 'confirmPassword', 'phone']), + ...getComponentSlots(Authenticator.SignUp), + toFederatedSignIn: noop, + toSignIn: noop, +}; + +const verifyUserProps = { + ...sharedProps, + ...getComponentSlots(Authenticator.VerifyUser), + fields: [ + { name: 'email', type: 'radio' as const, value: 'jeff@example.com' }, + ], + skipVerification: noop, +}; + +storiesOf('Authenticator', module) + .addDecorator((Story: any) => ( + + + + )) + .add('ConfirmResetPassword', () => ( + + )) + .add('ConfirmSignIn', () => ( + + )) + .add('ConfirmSignUp', () => ( + + )) + .add('ConfirmVerifyUser', () => ( + + )) + .add('ForceNewPassword', () => ( + + )) + .add('ResetPassword', () => ( + + )) + .add('SetupTOTP', () => ) + .add('SignIn', () => ) + .add('SignUp', () => ) + .add('VerifyUser', () => ); diff --git a/examples/react-native/storybook/stories/Button.stories.tsx b/examples/react-native/storybook/stories/Button.stories.tsx index 199a0066454..375ab12e555 100644 --- a/examples/react-native/storybook/stories/Button.stories.tsx +++ b/examples/react-native/storybook/stories/Button.stories.tsx @@ -1,19 +1,55 @@ import React from 'react'; -import { Text } from 'react-native'; +import { StyleSheet, Text } from 'react-native'; import { action } from '@storybook/addon-actions'; -import { text } from '@storybook/addon-knobs'; import { storiesOf } from '@storybook/react-native'; import { Button } from '@aws-amplify/ui-react-native/dist/primitives'; storiesOf('Button', module) + .add('default', () => ) .add('with text', () => ( )) + .add('variants', () => ( + <> + + + + + )) .add('with emoji', () => ( )) - .add('disabled', () => ); + .add('disabled', () => ( + <> + + + + + )) + .add('styles', () => ( + + )); + +const styles = StyleSheet.create({ + container: { + backgroundColor: 'blue', + borderRadius: 5, + padding: 10, + }, + whiteText: { + color: 'white', + fontWeight: '900', + }, +}); diff --git a/examples/react-native/storybook/stories/Divider.stories.tsx b/examples/react-native/storybook/stories/Divider.stories.tsx new file mode 100644 index 00000000000..3b575385dbd --- /dev/null +++ b/examples/react-native/storybook/stories/Divider.stories.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { storiesOf } from '@storybook/react-native'; + +import { Divider } from '@aws-amplify/ui-react-native/dist/primitives'; + +storiesOf('Divider', module) + .add('default', () => Default Label) + .add('no label', () => ) + .add('styles', () => ( + + Styled label + + )); + +const styles = StyleSheet.create({ + container: { + width: '75%', + }, + label: { + color: 'teal', + }, + line: { + backgroundColor: 'teal', + height: 1, + }, +}); diff --git a/examples/react-native/storybook/stories/ErrorMessage.stories.tsx b/examples/react-native/storybook/stories/ErrorMessage.stories.tsx new file mode 100644 index 00000000000..4155e4494d7 --- /dev/null +++ b/examples/react-native/storybook/stories/ErrorMessage.stories.tsx @@ -0,0 +1,54 @@ +import React, { useState } from 'react'; +import { StyleSheet, Text, View } from 'react-native'; +import { storiesOf } from '@storybook/react-native'; +import { ErrorMessage } from '@aws-amplify/ui-react-native/dist/primitives'; + +const OnDismissDemo = () => { + const [dismissed, setDismissed] = useState(false); + + return ( + + {dismissed ? ( + ErrorMessage dismissed + ) : ( + setDismissed(!dismissed)}> + Press the close icon (x) to dismiss + + )} + + ); +}; + +storiesOf('ErrorMessage', module) + .add('default', () => Default ErrorMessage) + .add('onDismiss', () => ) + .add('styles', () => ( + + White text, orange background + + )) + .add('long text', () => ( + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim + veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea + commodo consequat. Duis aute irure dolor in reprehenderit in voluptate + velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat + cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + )); + +const styles = StyleSheet.create({ + alert: { + backgroundColor: '#ff7f50', + }, + dismissDemo: { + padding: 20, + }, + whiteText: { + color: 'white', + fontSize: 20, + fontWeight: 'bold', + }, +}); diff --git a/examples/react-native/storybook/stories/FederatedProviderButton.stories.tsx b/examples/react-native/storybook/stories/FederatedProviderButton.stories.tsx new file mode 100644 index 00000000000..0181b0bc5b3 --- /dev/null +++ b/examples/react-native/storybook/stories/FederatedProviderButton.stories.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { ImageSourcePropType, StyleSheet, View } from 'react-native'; +import { SocialProvider } from '@aws-amplify/ui'; +import { storiesOf } from '@storybook/react-native'; +import { FederatedProviderButton } from '@aws-amplify/ui-react-native/dist/Authenticator/common'; +import { icons } from '@aws-amplify/ui-react-native/dist/assets'; +import { capitalize } from '@aws-amplify/ui-react-native/src/utils'; + +const providers: SocialProvider[] = ['amazon', 'apple', 'facebook', 'google']; + +type Logos = { + [key in SocialProvider]: ImageSourcePropType; +}; + +const logos: Logos = { + amazon: icons.amazonLogo, + apple: icons.appleLogo, + facebook: icons.facebookLogo, + google: icons.googleLogo, +}; + +storiesOf('FederatedProviderButton', module) + .add('default', () => ( + + Sign In with Amazon + + )) + .add('mock', () => ( + + {providers.map((provider, index) => ( + + Sign In with {capitalize(provider)} + + ))} + + )); + +const styles = StyleSheet.create({ + container: { + width: '90%', + }, + button: { + marginVertical: 8, + }, +}); diff --git a/examples/react-native/storybook/stories/Icon.stories.tsx b/examples/react-native/storybook/stories/Icon.stories.tsx index 8f334b3535d..f3c338c1ee1 100644 --- a/examples/react-native/storybook/stories/Icon.stories.tsx +++ b/examples/react-native/storybook/stories/Icon.stories.tsx @@ -1,8 +1,9 @@ import React, { useEffect } from 'react'; import { storiesOf } from '@storybook/react-native'; +import { Animated, Easing, StyleSheet } from 'react-native'; + import { Icon } from '@aws-amplify/ui-react-native/dist/primitives'; import { icons } from '@aws-amplify/ui-react-native/dist/assets'; -import { Animated, Easing } from 'react-native'; const source = icons.close; @@ -39,8 +40,14 @@ const StatefulAnimatedIcon = () => { }; storiesOf('Icon', module) - .add('default', () => ) - .add('animated', () => ) - .add('with style', () => ( - + .add('Default', () => ) + .add('Animated', () => ) + .add('Styled', () => ( + )); + +const styles = StyleSheet.create({ + custom: { + backgroundColor: 'lightgray', + }, +}); diff --git a/examples/react-native/storybook/stories/Label.stories.tsx b/examples/react-native/storybook/stories/Label.stories.tsx index f2758341259..b5d0fde11a4 100644 --- a/examples/react-native/storybook/stories/Label.stories.tsx +++ b/examples/react-native/storybook/stories/Label.stories.tsx @@ -1,11 +1,42 @@ import React from 'react'; import { StyleSheet } from 'react-native'; import { storiesOf } from '@storybook/react-native'; -import { Label } from '@aws-amplify/ui-react-native/dist/primitives'; +import { object, select } from '@storybook/addon-knobs'; + +import { Label } from '@aws-amplify/ui-react-native/dist/primitives/Label'; +import { LabelVariation } from '@aws-amplify/ui-react-native/dist/primitives/Label/types'; + +const variations: LabelVariation[] = [ + 'primary', + 'secondary', + 'tertiary', + 'error', + 'warning', + 'info', + 'success', +]; storiesOf('Label', module) - .add('default Label', () => ) - .add('style', () => ); + .add('Default', () => ) + .add('Styled', () => ) + .add('Variations', () => ( + <> + {variations.map((variation) => ( + + ))} + + )) + .add('Playground', () => ( + + )); const styles = StyleSheet.create({ redText: { diff --git a/examples/react-native/storybook/stories/PasswordField.stories.tsx b/examples/react-native/storybook/stories/PasswordField.stories.tsx new file mode 100644 index 00000000000..cba00086ed7 --- /dev/null +++ b/examples/react-native/storybook/stories/PasswordField.stories.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { storiesOf } from '@storybook/react-native'; + +import { PasswordField } from '@aws-amplify/ui-react-native/dist/primitives'; + +const styles = StyleSheet.create({ + container: { + width: '50%', + }, + input: { + color: 'red', + }, + icon: { + tintColor: 'red', + }, +}); + +storiesOf('PasswordField', module) + .add('default', () => ) + .add('without password visibility icon', () => ( + + )) + .add('disabled', () => ) + .add('style', () => ( + + This should be red + + )); diff --git a/examples/react-native/storybook/stories/PhoneNumberField.stories.tsx b/examples/react-native/storybook/stories/PhoneNumberField.stories.tsx new file mode 100644 index 00000000000..3b362aa07bf --- /dev/null +++ b/examples/react-native/storybook/stories/PhoneNumberField.stories.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { storiesOf } from '@storybook/react-native'; + +import { + PhoneNumberField, + PhoneNumberFieldProps, +} from '@aws-amplify/ui-react-native/dist/primitives'; + +const styles = StyleSheet.create({ + container: { + width: '75%', + }, + inputStyle: { + width: '50%', + }, + picker: { + color: 'red', + fontSize: 16, + width: '50%', + }, + pickerItem: { + color: 'red', + fontSize: 16, + }, +}); + +const codes = ['+1', '+7', '+20', '+27', '+30']; +const props: PhoneNumberFieldProps = { + style: styles.container, + defaultDialCode: undefined, + dialCodes: codes, +}; + +storiesOf('PhoneNumberField', module) + .add('default', () => ) + .add('disabled', () => ) + .add('styled', () => ( + + Dial code should be red + + )); diff --git a/examples/react-native/storybook/stories/Radio.stories.tsx b/examples/react-native/storybook/stories/Radio.stories.tsx index b7669c0dccd..8f0ebe8e87a 100644 --- a/examples/react-native/storybook/stories/Radio.stories.tsx +++ b/examples/react-native/storybook/stories/Radio.stories.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { StyleSheet } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import { storiesOf } from '@storybook/react-native'; import { Radio } from '@aws-amplify/ui-react-native/dist/primitives'; @@ -29,25 +29,42 @@ storiesOf('Radio', module) )) .add('disabled', () => ( <> - - - - + + + + )) .add('size', () => ( - <> - - - - - - - - - - - + + + + + + + + + + + + + + )) .add('styles', () => ( { + const [value, setValue] = useState(props.value); + const onChange = (nextValue: string) => { + setValue(nextValue); + }; + + return ( + + + + + + ); +}; + +const UncontrolledRadioGroup = ({ ...props }: any) => { + const [selectedValue, setSelectedValue] = useState('Empty :('); + + return ( + + + + + + ); +}; + +const CustomRadioGroup = ({ ...props }: any) => { + const [value, setValue] = useState('green'); + const onChange = (nextValue: string) => { + setValue(nextValue); + }; + + return ( + + + + + + + ); +}; + +storiesOf('RadioGroup', module) + .add('default', () => ) + .add('controlled', () => ( + + )) + .add('uncontrolled', () => ) + .add('direction', () => ( + <> + + + + )) + .add('disabled', () => ( + + )) + .add('labelStyle', () => ( + + )) + .add('size', () => ( + + )) + .add('Radio overrides', () => ); + +const styles = StyleSheet.create({ + redText: { + color: 'red', + }, + radioDotStyle: { + backgroundColor: 'green', + }, +}); diff --git a/examples/react-native/storybook/stories/Tabs.stories.tsx b/examples/react-native/storybook/stories/Tabs.stories.tsx new file mode 100644 index 00000000000..37f89d62dae --- /dev/null +++ b/examples/react-native/storybook/stories/Tabs.stories.tsx @@ -0,0 +1,86 @@ +import React, { useState } from 'react'; +import { StyleSheet } from 'react-native'; +import { storiesOf } from '@storybook/react-native'; +import { Tab, Tabs } from '@aws-amplify/ui-react-native/dist/primitives'; + +const ControlledTabs = ({ ...props }: any) => { + const [selectedIndex, setSelectedIndex] = useState( + props.selectedIndex + ); + const handleOnChange = (nextValue: number) => { + setSelectedIndex(nextValue); + }; + + return ( + + {props.children} + + ); +}; + +storiesOf('Tabs', module) + .add('default', () => ( + + Sign In + Create Account + + )) + .add('disabled', () => ( + + Tab 1 + Tab 2 (disabled) + + )) + .add('multiple', () => ( + + Tab 1 + Tab 2 + Tab 3 + + )) + .add('indicatorPosition', () => ( + + Tab 1 + Tab 2 + Tab 3 + + )) + .add('styles', () => ( + + + Tab 1 + + + Tab 2 + + + )); + +const styles = StyleSheet.create({ + container: { width: '90%' }, + styledContainer: { + borderColor: 'gray', + borderWidth: StyleSheet.hairlineWidth, + }, + tabStyle1: { + backgroundColor: 'green', + borderTopColor: 'yellow', + }, + tabStyle2: { + backgroundColor: 'lavender', + borderTopColor: 'rebeccapurple', + }, + tabTextStyle1: { + color: 'yellow', + fontWeight: '900', + }, + tabTextStyle2: { + color: 'gray', + fontWeight: '500', + }, +}); diff --git a/examples/react-native/storybook/stories/TextField.stories.tsx b/examples/react-native/storybook/stories/TextField.stories.tsx new file mode 100644 index 00000000000..bd4c785897c --- /dev/null +++ b/examples/react-native/storybook/stories/TextField.stories.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { storiesOf } from '@storybook/react-native'; + +import { TextField } from '@aws-amplify/ui-react-native/dist/primitives'; + +const styles = StyleSheet.create({ + container: { + width: '50%', + }, + errorMessage: { + color: 'red', + }, +}); + +storiesOf('TextField', module) + .add('default', () => ) + + .add('placeholder', () => ( + + )) + .add('with label', () => ) + .add('password', () => ( + + )) + .add('phone', () => ( + + )) + .add('with error', () => ( + + )) + .add('disabled', () => ( + + )); diff --git a/examples/react-native/storybook/storyLoader.js b/examples/react-native/storybook/storyLoader.js index d637b14d62e..7e6c7721b4f 100644 --- a/examples/react-native/storybook/storyLoader.js +++ b/examples/react-native/storybook/storyLoader.js @@ -1,19 +1,37 @@ function loadStories() { + require('./stories/Authenticator.stories'); require('./stories/Button.stories'); require('./stories/Checkbox.stories'); + require('./stories/Divider.stories'); + require('./stories/ErrorMessage.stories'); + require('./stories/FederatedProviderButton.stories'); require('./stories/Heading.stories'); require('./stories/Icon.stories'); require('./stories/Label.stories'); + require('./stories/PasswordField.stories'); + require('./stories/PhoneNumberField.stories'); require('./stories/Radio.stories'); + require('./stories/RadioGroup.stories'); + require('./stories/Tabs.stories'); + require('./stories/TextField.stories'); } const stories = [ + './stories/Authenticator.stories', './stories/Button.stories', './stories/Checkbox.stories', + './stories/Divider.stories', + './stories/ErrorMessage.stories', + './stories/FederatedProviderButton.stories', './stories/Heading.stories', './stories/Icon.stories', './stories/Label.stories', + './stories/PasswordField.stories', + './stories/PhoneNumberField.stories', './stories/Radio.stories', + './stories/RadioGroup.stories', + './stories/Tabs.stories', + './stories/TextField.stories', ]; module.exports = { diff --git a/examples/react-native/types/env.d.ts b/examples/react-native/types/env.d.ts new file mode 100644 index 00000000000..272f05d90b7 --- /dev/null +++ b/examples/react-native/types/env.d.ts @@ -0,0 +1,3 @@ +declare module '@env' { + export const GREETING: string; +} diff --git a/packages/e2e/cypress/integration/ui/components/authenticator/i18n/i18n.steps.ts b/packages/e2e/cypress/integration/ui/components/authenticator/i18n/i18n.steps.ts index ae5f5f6f743..ebb531b4faf 100644 --- a/packages/e2e/cypress/integration/ui/components/authenticator/i18n/i18n.steps.ts +++ b/packages/e2e/cypress/integration/ui/components/authenticator/i18n/i18n.steps.ts @@ -1,7 +1,73 @@ -import { translations } from '@aws-amplify/ui'; +// import { translations } from '@aws-amplify/ui'; import { And, Then, When } from '@badeball/cypress-cucumber-preprocessor'; import { escapeRegExp } from 'lodash'; +// temporary workaround due to https://github.com/aws/aws-sdk-js-v3/issues/3828 +const jaDict = { + 'Account recovery requires verified contact information': + 'アカウントの復旧には確認済みの連絡先が必要です', + 'An account with the given email already exists.': + '入力されたメールアドレスのアカウントが既に存在します', + 'Back to Sign In': 'サインインに戻る', + 'Change Password': 'パスワードを変える ', + Code: 'コード', + Confirm: '確定', + 'Confirm a Code': 'コードを確認', + 'Confirm Password': 'パスワードの確認', + 'Confirm Sign In': 'サインインする', + 'Confirm Sign Up': '登録する', + 'Confirmation Code': '確認コード', + 'Create a new account': '新しいアカウントを作る', + 'Create account': 'アカウントを作る ', + 'Create Account': 'アカウントを作る', + Email: 'メールアドレス', + 'Enter your password': 'パスワードを入力 ', + 'Enter your username': 'ユーザー名を入力 ', + 'Forgot Password': 'パスワードを忘れた ', + 'Forgot your password?': 'パスワードを忘れましたか? ', + 'Have an account? ': 'アカウントを持っていますか?', + 'Incorrect username or password': 'ユーザー名かパスワードが異なります ', + 'Invalid password format': 'パスワードの形式が無効です ', + 'Invalid phone number format': + '不正な電話番号の形式です。\n+12345678900 の形式で入力してください', + 'Lost your code? ': 'コードを失くしましたか?', + 'New Password': '新しいパスワード', + 'No account? ': 'アカウントが無いとき ', + or: '又は', + Password: 'パスワード ', + 'Password attempts exceeded': 'サインインの試行回数が上限に達しました', + 'Phone Number': '電話番号', + 'Resend Code': 'コードを再送信', + 'Reset password': 'パスワードをリセット ', + 'Reset your password': 'パスワードをリセットする', + 'Send Code': 'コードを送信', + 'Sign in': 'サインイン', + 'Sign In': 'サインイン ', + 'Sign in to your account': 'アカウントにサインイン ', + 'Sign In with Amazon': 'Amazonでサインイン', + 'Sign In with Facebook': 'Facebookでサインイン', + 'Sign In with Google': 'Googleでサインイン', + 'Sign Out': 'サインアウト ', + 'Sign Up': '登録 ', + Skip: 'スキップ', + Submit: '送信', + 'User already exists': '既にユーザーが存在しています ', + 'User does not exist': 'ユーザーが存在しません ', + Username: 'ユーザー名 ', + 'Username cannot be empty': 'ユーザー名は入力必須です', + Verify: '確認', + 'Verify Contact': '連絡先を確認', + 'We Emailed You': 'コードを送信しました', + 'Your code is on the way. To log in, enter the code we emailed to': + 'ログインするには、メールに記載されたコードを入力してください。送信先:', + 'Your code is on the way. To log in, enter the code we texted to': + 'ログインするには、テキストメッセージに記載されたコードを入力してください。送信先:', + 'It may take a minute to arrive.': + 'コードを受信するまで数分かかる場合があります。', +}; + +const translations = { ja: jaDict }; + When( 'I click the {string} tab in {string}', (label: string, language: string) => { diff --git a/packages/react-core/jest.config.js b/packages/react-core/jest.config.js index f47ab9809cf..d18a98c8448 100644 --- a/packages/react-core/jest.config.js +++ b/packages/react-core/jest.config.js @@ -2,10 +2,14 @@ module.exports = { collectCoverageFrom: [ '/src/**/*.(ts|tsx)', - // do not collect coverage from constants files + // do not collect coverage from: + // - constants files '!/src/**/*(c|C)onstants.ts', - // do not collect coverage from primary exports file - '!/src/index.ts', + // - __mock__ directories + '!/src/**/__mock__/*', + // - exports files + '!/src/index.ts', // primary + '!/src/Authenticator/index.ts', // Authenticator ], coverageThreshold: { global: { diff --git a/packages/react-core/package.json b/packages/react-core/package.json index 6c2e0db7ef2..5beb1e62de1 100644 --- a/packages/react-core/package.json +++ b/packages/react-core/package.json @@ -21,6 +21,7 @@ "clean": "rimraf dist node_modules", "prebuild": "rimraf dist", "build": "rollup --config", + "build:esm": "tsc --project tsconfig.esm.json", "dev": "yarn build:esm --watch", "lint": "tsc --noEmit && eslint src", "test": "jest --coverage --verbose", diff --git a/packages/react-core/src/Authenticator/hooks/__mock__/components.ts b/packages/react-core/src/Authenticator/hooks/__mock__/components.ts new file mode 100644 index 00000000000..0a0193aae1b --- /dev/null +++ b/packages/react-core/src/Authenticator/hooks/__mock__/components.ts @@ -0,0 +1,106 @@ +import { Defaults, Overrides } from '../types'; + +type DefaultComponents

= Defaults

; + +const Footer = () => null; +const FormFields = () => null; +const Header = () => null; + +const ConfirmResetPassword: DefaultComponents<{}>['ConfirmResetPassword'] = + () => { + return null; + }; +ConfirmResetPassword.Footer = Footer; +ConfirmResetPassword.FormFields = FormFields; +ConfirmResetPassword.Header = Header; + +const ConfirmSignIn: DefaultComponents<{}>['ConfirmSignIn'] = () => { + return null; +}; +ConfirmSignIn.Footer = Footer; +ConfirmSignIn.FormFields = FormFields; +ConfirmSignIn.Header = Header; + +const ConfirmSignUp: DefaultComponents<{}>['ConfirmSignUp'] = () => { + return null; +}; +ConfirmSignUp.Footer = Footer; +ConfirmSignUp.FormFields = FormFields; +ConfirmSignUp.Header = Header; + +const ConfirmVerifyUser: DefaultComponents<{}>['ConfirmVerifyUser'] = () => { + return null; +}; +ConfirmVerifyUser.Footer = Footer; +ConfirmVerifyUser.FormFields = FormFields; +ConfirmVerifyUser.Header = Header; + +const ForceNewPassword: DefaultComponents<{}>['ForceNewPassword'] = () => { + return null; +}; +ForceNewPassword.Footer = Footer; +ForceNewPassword.FormFields = FormFields; +ForceNewPassword.Header = Header; + +const ResetPassword: DefaultComponents<{}>['ResetPassword'] = () => { + return null; +}; +ResetPassword.Footer = Footer; +ResetPassword.FormFields = FormFields; +ResetPassword.Header = Header; + +const SetupTOTP: DefaultComponents<{}>['SetupTOTP'] = () => { + return null; +}; +SetupTOTP.Footer = Footer; +SetupTOTP.FormFields = FormFields; +SetupTOTP.Header = Header; + +const SignIn: DefaultComponents<{}>['SignIn'] = () => { + return null; +}; +SignIn.Footer = Footer; +SignIn.FormFields = FormFields; +SignIn.Header = Header; + +const SignUp: DefaultComponents<{}>['SignUp'] = () => { + return null; +}; +SignUp.Footer = Footer; +SignUp.FormFields = FormFields; +SignUp.Header = Header; + +const VerifyUser: DefaultComponents<{}>['VerifyUser'] = () => { + return null; +}; +VerifyUser.Footer = Footer; +VerifyUser.FormFields = FormFields; +VerifyUser.Header = Header; + +export const DEFAULTS: DefaultComponents<{}> = { + ConfirmResetPassword, + ConfirmSignIn, + ConfirmSignUp, + ConfirmVerifyUser, + ForceNewPassword, + ResetPassword, + SetupTOTP, + SignIn, + SignUp, + VerifyUser, +}; + +const OverrideConfirmResetPassword: Overrides['ConfirmResetPassword'] = () => + null; + +export const OVERRIDES: Overrides = { + ConfirmResetPassword: OverrideConfirmResetPassword, +}; + +const InvalidSignIn = 'Not a component' as unknown as Overrides['SignIn']; + +export const INVALID_SIGN_IN_OVERRIDES: Overrides = { + SignIn: InvalidSignIn, +}; + +export const INVALID_OVERRIDES = 'INVALID_OVERRIDES' as Overrides; diff --git a/packages/react-core/src/Authenticator/hooks/__tests__/utils.spec.tsx b/packages/react-core/src/Authenticator/hooks/__tests__/utils.spec.tsx new file mode 100644 index 00000000000..7ad3a9b6082 --- /dev/null +++ b/packages/react-core/src/Authenticator/hooks/__tests__/utils.spec.tsx @@ -0,0 +1,57 @@ +import { COMPONENT_ROUTE_KEYS } from '../constants'; +import { AuthenticatorRouteComponentKey } from '../types'; +import { isComponentRouteKey, resolveAuthenticatorComponents } from '../utils'; + +import { + DEFAULTS, + INVALID_OVERRIDES, + INVALID_SIGN_IN_OVERRIDES, + OVERRIDES, +} from '../__mock__/components'; + +describe('isComponentRouteKey', () => { + it.each(COMPONENT_ROUTE_KEYS)('returns true for a %s value', (route) => { + const output = isComponentRouteKey(route); + expect(output).toBe(true); + }); + + it('returns false for a non-component route key value', () => { + const output = isComponentRouteKey( + 'route' as AuthenticatorRouteComponentKey + ); + + expect(output).toBe(false); + }); +}); + +describe('resolveAuthenticatorComponents', () => { + it('returns defaults when no overrides are defined', () => { + const output = resolveAuthenticatorComponents(DEFAULTS); + expect(output).toBe(DEFAULTS); + }); + + it('returns the expected components when an override is provided', () => { + const output = resolveAuthenticatorComponents(DEFAULTS, OVERRIDES); + + expect(output.ConfirmResetPassword).not.toBe(DEFAULTS.ConfirmResetPassword); + expect(output.ConfirmResetPassword).toBe(OVERRIDES.ConfirmResetPassword); + expect(output.ConfirmSignIn).toBe(DEFAULTS.ConfirmSignIn); + expect(output.VerifyUser).toBe(DEFAULTS.VerifyUser); + }); + + it('returns the expected components when an override is invalid', () => { + const output = resolveAuthenticatorComponents( + DEFAULTS, + INVALID_SIGN_IN_OVERRIDES + ); + + expect(output.SignIn).not.toBe(INVALID_SIGN_IN_OVERRIDES.SignIn); + expect(output.SignIn).toBe(DEFAULTS.SignIn); + }); + + it('returns the expected components when the overrides param is invalid', () => { + const output = resolveAuthenticatorComponents(DEFAULTS, INVALID_OVERRIDES); + + expect(output).toStrictEqual(DEFAULTS); + }); +}); diff --git a/packages/react-core/src/Authenticator/hooks/constants.ts b/packages/react-core/src/Authenticator/hooks/constants.ts new file mode 100644 index 00000000000..b67f7ce7bfc --- /dev/null +++ b/packages/react-core/src/Authenticator/hooks/constants.ts @@ -0,0 +1,30 @@ +import { + AuthenticatorRouteComponentKey, + AuthenticatorRouteComponentName, +} from './types'; + +export const COMPONENT_ROUTE_KEYS: AuthenticatorRouteComponentKey[] = [ + 'confirmResetPassword', + 'confirmSignIn', + 'confirmSignUp', + 'confirmVerifyUser', + 'forceNewPassword', + 'resetPassword', + 'setupTOTP', + 'signIn', + 'signUp', + 'verifyUser', +]; + +export const COMPONENT_ROUTE_NAMES: AuthenticatorRouteComponentName[] = [ + 'ConfirmResetPassword', + 'ConfirmSignIn', + 'ConfirmSignUp', + 'ConfirmVerifyUser', + 'ForceNewPassword', + 'ResetPassword', + 'SetupTOTP', + 'SignIn', + 'SignUp', + 'VerifyUser', +]; diff --git a/packages/react-core/src/Authenticator/hooks/index.ts b/packages/react-core/src/Authenticator/hooks/index.ts index e212ee5af22..9ff45c3b832 100644 --- a/packages/react-core/src/Authenticator/hooks/index.ts +++ b/packages/react-core/src/Authenticator/hooks/index.ts @@ -1 +1,5 @@ export * from './useAuthenticator'; +export * from './useAuthenticatorRoute'; +export * from './useAuthenticatorInitMachine'; +export * from './utils'; +export * from './types'; diff --git a/packages/react-core/src/Authenticator/hooks/types.ts b/packages/react-core/src/Authenticator/hooks/types.ts new file mode 100644 index 00000000000..5efc408683b --- /dev/null +++ b/packages/react-core/src/Authenticator/hooks/types.ts @@ -0,0 +1,205 @@ +import React from 'react'; + +import { + AuthChallengeName, + AuthenticatorServiceFacade, + LegacyFormFieldOptions, +} from '@aws-amplify/ui'; + +export type AuthenticatorRouteComponentKey = + | 'confirmResetPassword' + | 'confirmSignIn' + | 'confirmSignUp' + | 'confirmVerifyUser' + | 'forceNewPassword' + | 'resetPassword' + | 'setupTOTP' + | 'signIn' + | 'signUp' + | 'verifyUser'; + +export type AuthenticatorLegacyField = LegacyFormFieldOptions; +export type AuthenticatorLegacyFields = AuthenticatorLegacyField[]; + +/** + * These are the "facades" that we provide, which contains contexts respective + * to current authenticator state. + */ +export type AuthenticatorMachineContext = AuthenticatorServiceFacade; +export type AuthenticatorMachineContextKey = keyof AuthenticatorMachineContext; + +export type AuthenticatorRouteComponentName = + Capitalize; + +export type GetTotpSecretCode = () => Promise; + +interface HeaderProps { + children?: React.ReactNode; +} + +interface FooterProps { + children?: React.ReactNode; +} + +type FormFieldsProps = { + isPending: AuthenticatorMachineContext['isPending']; + validationErrors?: AuthenticatorMachineContext['validationErrors']; +}; + +export type FooterComponent = React.ComponentType< + FooterProps & Props +>; + +export type FormFieldsComponent = React.ComponentType< + FormFieldsProps & { fields: FieldType[] } & Props +>; + +export type HeaderComponent = React.ComponentType< + HeaderProps & Props +>; + +export interface ComponentSlots { + Footer: FooterComponent; + Header: HeaderComponent; + + // `FormFieldsComponent` requires `FieldType` + FormFields: FormFieldsComponent; +} + +/** + * Common component prop types used for both RWA and RNA implementations + */ +export type CommonRouteProps = { + error?: AuthenticatorMachineContext['error']; + isPending: AuthenticatorMachineContext['isPending']; + handleBlur: AuthenticatorMachineContext['updateBlur']; + handleChange: AuthenticatorMachineContext['updateForm']; + handleSubmit: AuthenticatorMachineContext['submitForm']; +}; + +/** + * Base Route component props + */ +export type ConfirmResetPasswordBaseProps = { + resendCode: AuthenticatorMachineContext['resendCode']; + validationErrors?: AuthenticatorMachineContext['validationErrors']; +} & CommonRouteProps & + ComponentSlots; + +export type ConfirmSignInBaseProps = { + challengeName: AuthChallengeName; + toSignIn: AuthenticatorMachineContext['toSignIn']; +} & CommonRouteProps & + ComponentSlots; + +export type ConfirmSignUpBaseProps = { + codeDeliveryDetails: AuthenticatorMachineContext['codeDeliveryDetails']; + resendCode: AuthenticatorMachineContext['resendCode']; +} & CommonRouteProps & + ComponentSlots; + +export type ConfirmVerifyUserProps = { + skipVerification: AuthenticatorMachineContext['skipVerification']; +} & CommonRouteProps & + ComponentSlots; + +export type ForceResetPasswordBaseProps = { + toSignIn: AuthenticatorMachineContext['toSignIn']; + validationErrors?: AuthenticatorMachineContext['validationErrors']; +} & CommonRouteProps & + ComponentSlots; + +export type ResetPasswordBaseProps = { + toSignIn: AuthenticatorMachineContext['toSignIn']; +} & CommonRouteProps & + ComponentSlots; + +export type SetupTOTPBaseProps = { + getTotpSecretCode: GetTotpSecretCode; + toSignIn: AuthenticatorMachineContext['toSignIn']; +} & CommonRouteProps & + ComponentSlots; + +export type SignInBaseProps = { + hideSignUp?: boolean; + socialProviders?: AuthenticatorMachineContext['socialProviders']; + toFederatedSignIn: AuthenticatorMachineContext['toFederatedSignIn']; + toResetPassword: AuthenticatorMachineContext['toResetPassword']; + toSignUp: AuthenticatorMachineContext['toSignUp']; +} & CommonRouteProps & + ComponentSlots; + +export type SignUpBaseProps = { + hideSignIn?: boolean; + socialProviders?: AuthenticatorMachineContext['socialProviders']; + toFederatedSignIn: AuthenticatorMachineContext['toFederatedSignIn']; + toSignIn: AuthenticatorMachineContext['toSignIn']; + validationErrors?: AuthenticatorMachineContext['validationErrors']; +} & CommonRouteProps & + ComponentSlots; + +export type VerifyUserProps = { + skipVerification: AuthenticatorMachineContext['skipVerification']; +} & CommonRouteProps & + ComponentSlots; + +export interface DefaultProps { + ConfirmSignIn: ConfirmSignInBaseProps; + ConfirmSignUp: ConfirmSignUpBaseProps; + ConfirmResetPassword: ConfirmResetPasswordBaseProps; + ConfirmVerifyUser: ConfirmVerifyUserProps; + ForceNewPassword: ForceResetPasswordBaseProps; + ResetPassword: ResetPasswordBaseProps; + SetupTOTP: SetupTOTPBaseProps; + SignIn: SignInBaseProps; + SignUp: SignUpBaseProps; + VerifyUser: VerifyUserProps; +} + +/** + * common types extended for default component types/implementations and override component types + */ +type BaseComponent< + // Route specifc props + ComponentRouteProps = {}, + // Route specific `FieldType` + FieldType = {}, + // additional props assigned in the UI layer + Props = {} +> = React.ComponentType< + ComponentSlots & + ComponentRouteProps & { fields: FieldType[] } & Props +>; + +/** + * Authenticator Route Component Default types + */ +export type Defaults = { + [Key in AuthenticatorRouteComponentName]: BaseComponent< + DefaultProps[Key], + FieldType, + PlatformProps + > & + // add component slots for Defaults + ComponentSlots; +}; + +export type Overrides = { + [Key in AuthenticatorRouteComponentName]?: BaseComponent< + DefaultProps[Key], + FieldType, + PlatformProps + >; +}; + +/** + * Default Route Component union type + */ +export type DefaultComponentType = + Defaults[keyof Defaults]; + +/** + * Default Route Component union type + */ +export type DefaultPropsType = + DefaultProps[keyof DefaultProps]; diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticator/__mock__/useAuthenticator.ts b/packages/react-core/src/Authenticator/hooks/useAuthenticator/__mock__/useAuthenticator.ts new file mode 100644 index 00000000000..6a06b23f7f2 --- /dev/null +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticator/__mock__/useAuthenticator.ts @@ -0,0 +1,64 @@ +import { + AuthenticatorLegacyFields, + AuthenticatorMachineContext, +} from '../../types'; +import { UseAuthenticator } from '../types'; + +const authStatus = 'unauthenticated'; +const challengeName = 'CUSTOM_CHALLENGE'; +const codeDeliveryDetails = + {} as AuthenticatorMachineContext['codeDeliveryDetails']; +const error = 'error'; +const fields = [] as AuthenticatorLegacyFields; +const getTotpSecretCode = jest.fn(); +const hasValidationErrors = false; +const initializeMachine = jest.fn(); +const isPending = false; +const resendCode = jest.fn(); +const route = 'idle'; +const skipVerification = jest.fn(); +const signOut = jest.fn(); +const socialProviders = [] as AuthenticatorMachineContext['socialProviders']; +const submitForm = jest.fn(); +const toFederatedSignIn = jest.fn(); +const toResetPassword = jest.fn(); +const toSignIn = jest.fn(); +const toSignUp = jest.fn(); +const unverifiedContactMethods = {}; +const updateBlur = jest.fn(); +const updateForm = jest.fn(); +const validationErrors = {}; + +const user = { + challengeName, +} as AuthenticatorMachineContext['user']; + +export const mockMachineContext: AuthenticatorMachineContext = { + authStatus, + codeDeliveryDetails, + error, + hasValidationErrors, + initializeMachine, + isPending, + resendCode, + route, + signOut, + submitForm, + updateForm, + toSignIn, + toSignUp, + updateBlur, + user, + skipVerification, + socialProviders, + toFederatedSignIn, + toResetPassword, + unverifiedContactMethods, + validationErrors, +}; + +export const mockUseAuthenticatorOutput: UseAuthenticator = { + ...mockMachineContext, + fields, + getTotpSecretCode, +} as unknown as UseAuthenticator; diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticator/__tests__/__snapshots__/useAuthenticator.spec.tsx.snap b/packages/react-core/src/Authenticator/hooks/useAuthenticator/__tests__/__snapshots__/useAuthenticator.spec.tsx.snap index 38794516cda..b2d5edc5f29 100644 --- a/packages/react-core/src/Authenticator/hooks/useAuthenticator/__tests__/__snapshots__/useAuthenticator.spec.tsx.snap +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticator/__tests__/__snapshots__/useAuthenticator.spec.tsx.snap @@ -20,7 +20,9 @@ Object { "toResetPassword": [Function], "toSignIn": [Function], "toSignUp": [Function], - "unverifiedContactMethods": Array [], + "unverifiedContactMethods": Object { + "email": "test#example.com", + }, "updateBlur": [Function], "updateForm": [Function], "user": Object {}, diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticator/__tests__/useAuthenticator.spec.tsx b/packages/react-core/src/Authenticator/hooks/useAuthenticator/__tests__/useAuthenticator.spec.tsx index b61e40691de..a80f344d65a 100644 --- a/packages/react-core/src/Authenticator/hooks/useAuthenticator/__tests__/useAuthenticator.spec.tsx +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticator/__tests__/useAuthenticator.spec.tsx @@ -16,7 +16,7 @@ const mockServiceFacade: AuthenticatorServiceFacade = { isPending: false, route: 'idle', socialProviders: [], - unverifiedContactMethods: [] as UseAuthenticator['unverifiedContactMethods'], + unverifiedContactMethods: { email: 'test#example.com' }, user: {} as UseAuthenticator['user'], validationErrors: undefined as unknown as UseAuthenticator['validationErrors'], diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticator/__tests__/utils.spec.tsx b/packages/react-core/src/Authenticator/hooks/useAuthenticator/__tests__/utils.spec.tsx index 4085c08af89..ff539e3a6cd 100644 --- a/packages/react-core/src/Authenticator/hooks/useAuthenticator/__tests__/utils.spec.tsx +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticator/__tests__/utils.spec.tsx @@ -7,15 +7,12 @@ import { import * as UIModule from '@aws-amplify/ui'; -import { COMPONENT_ROUTE_KEYS } from '../constants'; -import { AuthenticatorRouteComponentKey } from '../types'; import { areSelectorDepsEqual, defaultComparator, getComparator, - getLegacyFields, + getMachineFields, getTotpSecretCodeCallback, - isComponentRouteKey, } from '../utils'; const setupTOTPSpy = jest.spyOn(Auth, 'setupTOTP').mockImplementation(); @@ -86,21 +83,6 @@ describe('defaultComparator', () => { }); }); -describe('isComponentRouteKey', () => { - it.each(COMPONENT_ROUTE_KEYS)('returns true for a %s value', (route) => { - const output = isComponentRouteKey(route); - expect(output).toBe(true); - }); - - it('returns false for a non-component route key value', () => { - const output = isComponentRouteKey( - 'route' as AuthenticatorRouteComponentKey - ); - - expect(output).toBe(false); - }); -}); - describe('getTotpSecretCodeCallback', () => { const user = {} as AmplifyUser; it('returns a getTotpSecretCode function', () => { @@ -118,17 +100,49 @@ describe('getTotpSecretCodeCallback', () => { }); }); -describe('getLegacyFields', () => { +describe('getMachineFields', () => { const state = {} as unknown as AuthMachineState; it('calls getSortedFormFields when route is a valid component route', () => { - getLegacyFields('signIn', state); + getMachineFields('signIn', state, {}); expect(getSortedFormFieldsSpy).toHaveBeenCalledWith('signIn', state); }); it('returns an empty array for a non-component route', () => { - const output = getLegacyFields('idle', state); + const output = getMachineFields('idle', state, {}); expect(output).toHaveLength(0); }); + + it('returns expected values for verifyUser route', () => { + const output = getMachineFields('verifyUser', state, { + email: 'test@example.com', + }); + + expect(output).toHaveLength(1); + expect(output).toStrictEqual([ + { + label: 'test@example.com', + name: 'email', + value: 'test@example.com', + type: 'radio', + }, + ]); + }); + + it('returns expected values for verifyUser route when contact method is empty', () => { + const output = getMachineFields('verifyUser', state, {}); + + expect(output).toHaveLength(0); + expect(output).toStrictEqual([]); + }); + + it('returns expected values for verifyUser route when contact method value is invalid', () => { + const output = getMachineFields('verifyUser', state, { + phone_number: null as unknown as string, + }); + + expect(output).toHaveLength(1); + expect(output).toStrictEqual([{}]); + }); }); diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticator/constants.ts b/packages/react-core/src/Authenticator/hooks/useAuthenticator/constants.ts index 157c47c823b..3fe34c2410b 100644 --- a/packages/react-core/src/Authenticator/hooks/useAuthenticator/constants.ts +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticator/constants.ts @@ -1,17 +1,2 @@ -import { AuthenticatorRouteComponentKey } from './types'; - export const USE_AUTHENTICATOR_ERROR = '`useAuthenticator` must be used inside an `Authenticator.Provider`.'; - -export const COMPONENT_ROUTE_KEYS: AuthenticatorRouteComponentKey[] = [ - 'signIn', - 'signUp', - 'forceNewPassword', - 'confirmResetPassword', - 'confirmSignIn', - 'confirmSignUp', - 'confirmVerifyUser', - 'resetPassword', - 'setupTOTP', - 'verifyUser', -]; diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticator/index.ts b/packages/react-core/src/Authenticator/hooks/useAuthenticator/index.ts index 90d928676d9..41e60a6c21e 100644 --- a/packages/react-core/src/Authenticator/hooks/useAuthenticator/index.ts +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticator/index.ts @@ -1,2 +1,2 @@ export { default as useAuthenticator } from './useAuthenticator'; -export { UseAuthenticator } from './types'; +export { UseAuthenticator, UseAuthenticatorSelector } from './types'; diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticator/types.ts b/packages/react-core/src/Authenticator/hooks/useAuthenticator/types.ts index 688af6bffe4..a042e710092 100644 --- a/packages/react-core/src/Authenticator/hooks/useAuthenticator/types.ts +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticator/types.ts @@ -30,7 +30,7 @@ export type AuthenticatorLegacyFields = LegacyFormFieldOptions[]; * Selector accepts current facade values and returns an array of * desired value(s) that should trigger re-render. */ -export type Selector = ( +export type UseAuthenticatorSelector = ( context: AuthenticatorMachineContext ) => AuthenticatorMachineContext[AuthenticatorMachineContextKey][]; @@ -42,6 +42,6 @@ export interface UseAuthenticator extends AuthenticatorServiceFacade { } export type Comparator = ( - currentFacade: AuthenticatorServiceFacade, - nextFacade: AuthenticatorServiceFacade + currentMachineContext: AuthenticatorMachineContext, + nextMachineContext: AuthenticatorMachineContext ) => boolean; diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticator/useAuthenticator.tsx b/packages/react-core/src/Authenticator/hooks/useAuthenticator/useAuthenticator.tsx index c7f34799d0a..3e70776b26e 100644 --- a/packages/react-core/src/Authenticator/hooks/useAuthenticator/useAuthenticator.tsx +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticator/useAuthenticator.tsx @@ -5,11 +5,11 @@ import { AuthMachineState, getServiceFacade } from '@aws-amplify/ui'; import { AuthenticatorContext } from '../../context'; import { USE_AUTHENTICATOR_ERROR } from './constants'; -import { Selector, UseAuthenticator } from './types'; +import { UseAuthenticatorSelector, UseAuthenticator } from './types'; import { defaultComparator, getComparator, - getLegacyFields, + getMachineFields, getTotpSecretCodeCallback, } from './utils'; @@ -17,7 +17,7 @@ import { * [📖 Docs](https://ui.docs.amplify.aws/react/connected-components/authenticator/headless#useauthenticator-hook) */ export default function useAuthenticator( - selector?: Selector + selector?: UseAuthenticatorSelector ): UseAuthenticator { const context = React.useContext(AuthenticatorContext); @@ -37,7 +37,7 @@ export default function useAuthenticator( const facade = useSelector(service, xstateSelector, comparator); - const { route, user, ...rest } = facade; + const { route, unverifiedContactMethods, user, ...rest } = facade; // do not memoize output. `service.getSnapshot` reference remains stable preventing // `fields` from updating with current form state on value changes @@ -45,14 +45,20 @@ export default function useAuthenticator( // legacy `formFields` values required until form state is removed from state machine const fields = useMemo( - () => getLegacyFields(route, serviceSnapshot as AuthMachineState), - [route, serviceSnapshot] + () => + getMachineFields( + route, + serviceSnapshot as AuthMachineState, + unverifiedContactMethods + ), + [route, serviceSnapshot, unverifiedContactMethods] ); return { ...rest, getTotpSecretCode: getTotpSecretCodeCallback(user), route, + unverifiedContactMethods, user, /** @deprecated For internal use only */ fields, diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticator/utils.ts b/packages/react-core/src/Authenticator/hooks/useAuthenticator/utils.ts index b4219ee7ae0..3052baea2fe 100644 --- a/packages/react-core/src/Authenticator/hooks/useAuthenticator/utils.ts +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticator/utils.ts @@ -5,17 +5,15 @@ import { AuthMachineState, FormFieldsArray, getSortedFormFields, + UnverifiedContactMethods, } from '@aws-amplify/ui'; +import isString from 'lodash/isString'; import { areEmptyArrays, areEmptyObjects } from '../../../utils'; +import { AuthenticatorLegacyField, AuthenticatorLegacyFields } from '../types'; +import { isComponentRouteKey } from '../utils'; -import { COMPONENT_ROUTE_KEYS } from './constants'; -import { - AuthenticatorRouteComponentKey, - AuthenticatorLegacyFields, - Comparator, - Selector, -} from './types'; +import { Comparator, UseAuthenticatorSelector } from './types'; export const defaultComparator = (): false => false; @@ -45,7 +43,7 @@ export function areSelectorDepsEqual( } export const getComparator = - (selector: Selector): Comparator => + (selector: UseAuthenticatorSelector): Comparator => (currentFacade, nextFacade) => { const currentSelectorDeps = selector(currentFacade); const nextSelectorDeps = selector(nextFacade); @@ -59,24 +57,40 @@ export const getTotpSecretCodeCallback = (user: AmplifyUser) => return await Auth.setupTOTP(user); }; -export const isComponentRouteKey = ( - route: AuthenticatorRoute -): route is AuthenticatorRouteComponentKey => - COMPONENT_ROUTE_KEYS.some((componentRoute) => componentRoute === route); - const flattenFormFields = ( fields: FormFieldsArray ): AuthenticatorLegacyFields => fields.flatMap(([name, options]) => ({ name, ...options })); +const convertContactMethodsToFields = ( + unverifiedContactMethods: UnverifiedContactMethods +): AuthenticatorLegacyFields => { + return ( + unverifiedContactMethods && + Object.entries(unverifiedContactMethods).map(([name, value]) => { + const valueIsString = isString(value); + if (!valueIsString || !name) { + return {} as AuthenticatorLegacyField; + } + return { name, label: value, type: 'radio', value }; + }) + ); +}; + /** - * Retrieves legacy form field values from state machine for routes that have fields + * Retrieves default and custom (RWA only, to be updated) form field values from state machine + * for subcomponent routes that render fields */ -export const getLegacyFields = ( +export const getMachineFields = ( route: AuthenticatorRoute, - state: AuthMachineState -): AuthenticatorLegacyFields => - // verifyUser is a component route, but does not have form fields - isComponentRouteKey(route) && route !== 'verifyUser' - ? flattenFormFields(getSortedFormFields(route, state)) - : []; + state: AuthMachineState, + unverifiedContactMethods: UnverifiedContactMethods +): AuthenticatorLegacyFields => { + if (isComponentRouteKey(route)) { + return route === 'verifyUser' + ? convertContactMethodsToFields(unverifiedContactMethods) + : flattenFormFields(getSortedFormFields(route, state)); + } + + return []; +}; diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticatorInitMachine/__tests__/useAuthenticatorInitMachine.spec.tsx b/packages/react-core/src/Authenticator/hooks/useAuthenticatorInitMachine/__tests__/useAuthenticatorInitMachine.spec.tsx new file mode 100644 index 00000000000..50968c2737a --- /dev/null +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticatorInitMachine/__tests__/useAuthenticatorInitMachine.spec.tsx @@ -0,0 +1,64 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import { useAuthenticator, UseAuthenticator } from '../../useAuthenticator'; +import { mockUseAuthenticatorOutput } from '../../useAuthenticator/__mock__/useAuthenticator'; + +import { routeSelector } from '../useAuthenticatorInitMachine'; +import { useAuthenticatorInitMachine } from '..'; + +jest.mock('../../useAuthenticator'); + +const initializeMachine = jest.fn(); + +describe('useAuthenticatorInitMachine', () => { + beforeEach(() => { + initializeMachine.mockClear(); + }); + + it('calls initializeMachine only once, even after subsequent rerenders', () => { + const route = 'setup'; + const initialData = {}; + const modifiedData = { mutated: 'dataObject' }; + + (useAuthenticator as jest.Mock).mockReturnValue({ + initializeMachine, + route, + } as unknown as UseAuthenticator); + + const { rerender } = renderHook( + ({ data }) => useAuthenticatorInitMachine(data), + { initialProps: { data: initialData } } + ); + + expect(initializeMachine).toHaveBeenCalledTimes(1); + + // change the input props of the hook to get the useEffect to run again + rerender({ data: modifiedData }); + + expect(initializeMachine).toHaveBeenCalledTimes(1); + }); + + it('does not call initializeMachine when the route !== "setup"', () => { + const route = 'idle'; + const data = {}; + + (useAuthenticator as jest.Mock).mockReturnValue({ + initializeMachine, + route, + } as unknown as UseAuthenticator); + + renderHook(() => useAuthenticatorInitMachine(data)); + + expect(initializeMachine).toHaveBeenCalledTimes(0); + }); +}); + +describe('routeSelector', () => { + it('only selects the value of route', () => { + const route = 'idle' as UseAuthenticator['route']; + const machineContext = { ...mockUseAuthenticatorOutput, route }; + + const output = routeSelector(machineContext); + expect(output).toStrictEqual([route]); + }); +}); diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticatorInitMachine/index.ts b/packages/react-core/src/Authenticator/hooks/useAuthenticatorInitMachine/index.ts new file mode 100644 index 00000000000..2619a881f1e --- /dev/null +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticatorInitMachine/index.ts @@ -0,0 +1 @@ +export { default as useAuthenticatorInitMachine } from './useAuthenticatorInitMachine'; diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticatorInitMachine/useAuthenticatorInitMachine.tsx b/packages/react-core/src/Authenticator/hooks/useAuthenticatorInitMachine/useAuthenticatorInitMachine.tsx new file mode 100644 index 00000000000..e511003ed8d --- /dev/null +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticatorInitMachine/useAuthenticatorInitMachine.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { AuthenticatorMachineOptions } from '@aws-amplify/ui'; + +import { + useAuthenticator, + UseAuthenticatorSelector, +} from '../useAuthenticator'; + +// only select `route` from machine context +export const routeSelector: UseAuthenticatorSelector = ({ route }) => [route]; + +export default function useAuthenticatorInitMachine( + data: AuthenticatorMachineOptions +): void { + const { route, initializeMachine } = useAuthenticator(routeSelector); + + const hasInitialized = React.useRef(false); + React.useEffect(() => { + if (!hasInitialized.current && route === 'setup') { + initializeMachine(data); + + hasInitialized.current = true; + } + }, [initializeMachine, route, data]); +} diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/__tests__/__snapshots__/useAuthenticatorRoute.spec.ts.snap b/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/__tests__/__snapshots__/useAuthenticatorRoute.spec.ts.snap new file mode 100644 index 00000000000..127e063192c --- /dev/null +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/__tests__/__snapshots__/useAuthenticatorRoute.spec.ts.snap @@ -0,0 +1,181 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`useAuthenticatorRoute returns the expected values for the confirmResetPassword route 1`] = ` +Object { + "Component": [Function], + "props": Object { + "Footer": [Function], + "FormFields": [Function], + "Header": [Function], + "error": "error", + "handleBlur": [MockFunction], + "handleChange": [MockFunction], + "handleSubmit": [MockFunction], + "isPending": false, + "resendCode": [MockFunction], + "validationErrors": Object {}, + }, +} +`; + +exports[`useAuthenticatorRoute returns the expected values for the confirmSignIn route 1`] = ` +Object { + "Component": [Function], + "props": Object { + "Footer": [Function], + "FormFields": [Function], + "Header": [Function], + "challengeName": "CUSTOM_CHALLENGE", + "error": "error", + "handleBlur": [MockFunction], + "handleChange": [MockFunction], + "handleSubmit": [MockFunction], + "isPending": false, + "toSignIn": [MockFunction], + }, +} +`; + +exports[`useAuthenticatorRoute returns the expected values for the confirmSignUp route 1`] = ` +Object { + "Component": [Function], + "props": Object { + "Footer": [Function], + "FormFields": [Function], + "Header": [Function], + "codeDeliveryDetails": Object {}, + "error": "error", + "handleBlur": [MockFunction], + "handleChange": [MockFunction], + "handleSubmit": [MockFunction], + "isPending": false, + "resendCode": [MockFunction], + }, +} +`; + +exports[`useAuthenticatorRoute returns the expected values for the confirmVerifyUser route 1`] = ` +Object { + "Component": [Function], + "props": Object { + "Footer": [Function], + "FormFields": [Function], + "Header": [Function], + "error": "error", + "handleBlur": [MockFunction], + "handleChange": [MockFunction], + "handleSubmit": [MockFunction], + "isPending": false, + "skipVerification": [MockFunction], + }, +} +`; + +exports[`useAuthenticatorRoute returns the expected values for the forceNewPassword route 1`] = ` +Object { + "Component": [Function], + "props": Object { + "Footer": [Function], + "FormFields": [Function], + "Header": [Function], + "error": "error", + "handleBlur": [MockFunction], + "handleChange": [MockFunction], + "handleSubmit": [MockFunction], + "isPending": false, + "toSignIn": [MockFunction], + "validationErrors": Object {}, + }, +} +`; + +exports[`useAuthenticatorRoute returns the expected values for the resetPassword route 1`] = ` +Object { + "Component": [Function], + "props": Object { + "Footer": [Function], + "FormFields": [Function], + "Header": [Function], + "error": "error", + "handleBlur": [MockFunction], + "handleChange": [MockFunction], + "handleSubmit": [MockFunction], + "isPending": false, + "toSignIn": [MockFunction], + }, +} +`; + +exports[`useAuthenticatorRoute returns the expected values for the setupTOTP route 1`] = ` +Object { + "Component": [Function], + "props": Object { + "Footer": [Function], + "FormFields": [Function], + "Header": [Function], + "error": "error", + "getTotpSecretCode": [MockFunction], + "handleBlur": [MockFunction], + "handleChange": [MockFunction], + "handleSubmit": [MockFunction], + "isPending": false, + "toSignIn": [MockFunction], + }, +} +`; + +exports[`useAuthenticatorRoute returns the expected values for the signIn route 1`] = ` +Object { + "Component": [Function], + "props": Object { + "Footer": [Function], + "FormFields": [Function], + "Header": [Function], + "error": "error", + "handleBlur": [MockFunction], + "handleChange": [MockFunction], + "handleSubmit": [MockFunction], + "hideSignUp": false, + "isPending": false, + "socialProviders": Array [], + "toFederatedSignIn": [MockFunction], + "toResetPassword": [MockFunction], + "toSignUp": [MockFunction], + }, +} +`; + +exports[`useAuthenticatorRoute returns the expected values for the signUp route 1`] = ` +Object { + "Component": [Function], + "props": Object { + "Footer": [Function], + "FormFields": [Function], + "Header": [Function], + "error": "error", + "handleBlur": [MockFunction], + "handleChange": [MockFunction], + "handleSubmit": [MockFunction], + "isPending": false, + "toSignIn": [MockFunction], + "validationErrors": Object {}, + }, +} +`; + +exports[`useAuthenticatorRoute returns the expected values for the verifyUser route 1`] = ` +Object { + "Component": [Function], + "props": Object { + "Footer": [Function], + "FormFields": [Function], + "Header": [Function], + "error": "error", + "handleBlur": [MockFunction], + "handleChange": [MockFunction], + "handleSubmit": [MockFunction], + "isPending": false, + "skipVerification": [MockFunction], + }, +} +`; diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/__tests__/useAuthenticatorRoute.spec.ts b/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/__tests__/useAuthenticatorRoute.spec.ts new file mode 100644 index 00000000000..e333a4a15a1 --- /dev/null +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/__tests__/useAuthenticatorRoute.spec.ts @@ -0,0 +1,38 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import { RenderNothing } from '../../../../components'; +import { COMPONENT_ROUTE_KEYS } from '../../constants'; +import { DEFAULTS } from '../../__mock__/components'; +import { mockUseAuthenticatorOutput } from '../../useAuthenticator/__mock__/useAuthenticator'; +import { useAuthenticator } from '../../useAuthenticator'; + +import { useAuthenticatorRoute } from '..'; + +jest.mock('../../useAuthenticator'); + +describe('useAuthenticatorRoute', () => { + it.each(COMPONENT_ROUTE_KEYS)( + 'returns the expected values for the %s route', + (route) => { + (useAuthenticator as jest.Mock).mockReturnValue({ + ...mockUseAuthenticatorOutput, + route, + }); + const { result } = renderHook(() => + useAuthenticatorRoute({ components: DEFAULTS }) + ); + expect(result.current).toMatchSnapshot(); + } + ); + + it('returns the expected values for a non-component route', () => { + (useAuthenticator as jest.Mock).mockReturnValueOnce({ route: 'idle' }); + const { result } = renderHook(() => + useAuthenticatorRoute({ components: DEFAULTS }) + ); + expect(result.current).toStrictEqual({ + Component: RenderNothing, + props: {}, + }); + }); +}); diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/__tests__/utils.spec.ts b/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/__tests__/utils.spec.ts new file mode 100644 index 00000000000..2aeaff942d4 --- /dev/null +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/__tests__/utils.spec.ts @@ -0,0 +1,215 @@ +import { AuthenticatorRoute } from '@aws-amplify/ui'; +import { RenderNothing } from '../../../../components'; +import { + AuthenticatorRouteComponentName, + DefaultComponentType, +} from '../../types'; +import { UseAuthenticator } from '../../useAuthenticator/types'; + +import { DEFAULTS } from '../../__mock__/components'; +import { + mockMachineContext, + mockUseAuthenticatorOutput, +} from '../../useAuthenticator/__mock__/useAuthenticator'; +import { UseAuthenticatorRoute } from '../types'; + +import { + getRouteMachineSelector, + routeSelector, + resolveConfirmResetPasswordRoute, + resolveConfirmSignInRoute, + resolveConfirmSignUpRoute, + resolveDefault, + resolveConfirmVerifyUserRoute, + resolveForceNewPasswordRoute, + resolveResetPasswordRoute, + resolveSetupTOTPRoute, + resolveSignInRoute, + resolveSignUpRoute, + resolveVerifyUserRoute, +} from '../utils'; + +type PropsResolver = ( + Component: DefaultComponentType, + selectedProps: UseAuthenticator +) => UseAuthenticatorRoute; + +const { + codeDeliveryDetails, + error, + getTotpSecretCode, + isPending, + resendCode, + route, + skipVerification, + socialProviders, + submitForm, + toFederatedSignIn, + toResetPassword, + toSignIn, + toSignUp, + updateBlur, + updateForm, + user, + validationErrors, +} = mockUseAuthenticatorOutput; + +const { challengeName } = user; + +const machineContext = mockMachineContext; + +const useAuthenticatorOutput = mockUseAuthenticatorOutput; + +const commonSelectorProps = [ + error, + isPending, + submitForm, + updateBlur, + updateForm, +]; + +describe('getRouteMachineSelector', () => { + it.each([ + [ + 'confirmResetPassword', + [...commonSelectorProps, resendCode, validationErrors, route], + ], + ['confirmSignIn', [...commonSelectorProps, toSignIn, user, route]], + [ + 'confirmSignUp', + [...commonSelectorProps, codeDeliveryDetails, resendCode, route], + ], + ['confirmVerifyUser', [...commonSelectorProps, skipVerification, route]], + [ + 'forceNewPassword', + [...commonSelectorProps, toSignIn, validationErrors, route], + ], + ['idle', [route]], + ['resetPassword', [...commonSelectorProps, toSignIn, route]], + [ + 'signIn', + [ + ...commonSelectorProps, + socialProviders, + toFederatedSignIn, + toResetPassword, + toSignUp, + route, + ], + ], + ['signUp', [...commonSelectorProps, toSignIn, validationErrors, route]], + ['setupTOTP', [...commonSelectorProps, toSignIn, route]], + ['verifyUser', [...commonSelectorProps, skipVerification, route]], + ])('returns the expected route selector for %s', (route, expected) => { + const selector = getRouteMachineSelector(route as AuthenticatorRoute); + const output = selector(machineContext); + expect(output).toStrictEqual(expected); + }); +}); + +describe('props resolver functions', () => { + it.each([ + [ + 'ConfirmResetPassword', + resolveConfirmResetPasswordRoute, + { resendCode, validationErrors }, + ], + ['ConfirmSignIn', resolveConfirmSignInRoute, { challengeName, toSignIn }], + [ + 'ConfirmSignUp', + resolveConfirmSignUpRoute, + { codeDeliveryDetails, resendCode }, + ], + [ + 'ConfirmVerifyUser', + resolveConfirmVerifyUserRoute, + { error, isPending, skipVerification }, + ], + [ + 'ForceNewPassword', + resolveForceNewPasswordRoute, + { error, isPending, toSignIn, validationErrors }, + ], + [ + 'ResetPassword', + resolveResetPasswordRoute, + { error, isPending, toSignIn }, + ], + ['SetupTOTP', resolveSetupTOTPRoute, { getTotpSecretCode, toSignIn }], + [ + 'SignIn', + resolveSignInRoute, + { + error, + hideSignUp: false, + isPending, + socialProviders, + toFederatedSignIn, + toResetPassword, + toSignUp, + }, + ], + [ + 'SignUp', + resolveSignUpRoute, + { error, isPending, toSignIn, validationErrors }, + ], + [ + 'VerifyUser', + resolveVerifyUserRoute, + { error, isPending, skipVerification }, + ], + ])( + 'resolve%s returns the expected values', + (key, resolver, routeSpecificProps) => { + const Component = DEFAULTS[key as AuthenticatorRouteComponentName]; + + const commonProps = { error, isPending }; + const componentSlots = { + Footer: Component.Footer, + FormFields: Component.FormFields, + Header: Component.Header, + }; + const eventHandlerProps = { + handleBlur: updateBlur, + handleChange: updateForm, + handleSubmit: submitForm, + }; + + const expected = { + Component, + props: { + ...commonProps, + ...componentSlots, + ...eventHandlerProps, + ...routeSpecificProps, + }, + }; + + const output = (resolver as PropsResolver)( + Component, + useAuthenticatorOutput + ); + expect(output).toStrictEqual(expected); + } + ); + + describe('resolveDefault', () => { + it('returns the expected values', () => { + const output = resolveDefault(); + const expected = { Component: RenderNothing, props: {} }; + + expect(output).toStrictEqual(expected); + }); + }); +}); + +describe('routeSelector', () => { + it('only selects the value of route', () => { + const route = 'idle' as UseAuthenticator['route']; + const machineContext = { ...mockUseAuthenticatorOutput, route }; + + const output = routeSelector(machineContext); + expect(output).toStrictEqual([route]); + }); +}); diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/constants.ts b/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/constants.ts new file mode 100644 index 00000000000..16cae7c6b5d --- /dev/null +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/constants.ts @@ -0,0 +1,101 @@ +import { + AuthenticatorMachineContextKey, + AuthenticatorRouteComponentKey, +} from '../types'; +import { + CommonRouteMachineKey, + ConfirmResetPasswordMachineKey, + ConfirmSignInMachineKey, + ConfirmSignUpMachineKey, + ConfirmVerifyUserMachineKey, + ForceNewPasswordMachineKey, + FormEventHandlerMachineKey, + FormEventHandlerPropKey, + ResetPasswordMachineKey, + SignInMachineKey, + SignUpMachineKey, + SetupTOTPMachineKey, + VerifyUserMachineKey, +} from './types'; + +export const EVENT_HANDLER_KEY_MAP: Record< + FormEventHandlerMachineKey, + FormEventHandlerPropKey +> = { + updateBlur: 'handleBlur', + updateForm: 'handleChange', + submitForm: 'handleSubmit', +}; + +const COMMON_ROUTE_MACHINE_KEYS: CommonRouteMachineKey[] = [ + 'error', + 'isPending', + 'submitForm', + 'updateBlur', + 'updateForm', +]; + +const CONFIRM_RESET_PASSWORD_MACHINE_KEYS: ConfirmResetPasswordMachineKey[] = [ + ...COMMON_ROUTE_MACHINE_KEYS, + 'resendCode', + 'validationErrors', +]; +const CONFIRM_SIGN_IN_MACHINE_KEYS: ConfirmSignInMachineKey[] = [ + ...COMMON_ROUTE_MACHINE_KEYS, + 'toSignIn', + 'user', +]; +const CONFIRM_SIGN_UP_MACHINE_KEYS: ConfirmSignUpMachineKey[] = [ + ...COMMON_ROUTE_MACHINE_KEYS, + 'codeDeliveryDetails', + 'resendCode', +]; +const CONFIRM_VERIFY_USER_MACHINE_KEYS: ConfirmVerifyUserMachineKey[] = [ + ...COMMON_ROUTE_MACHINE_KEYS, + 'skipVerification', +]; +const FORCE_NEW_PASSWORD_MACHINE_KEYS: ForceNewPasswordMachineKey[] = [ + ...COMMON_ROUTE_MACHINE_KEYS, + 'toSignIn', + 'validationErrors', +]; +const RESET_PASSWORD_MACHINE_KEYS: ResetPasswordMachineKey[] = [ + ...COMMON_ROUTE_MACHINE_KEYS, + 'toSignIn', +]; +const SIGN_IN_MACHINE_KEYS: SignInMachineKey[] = [ + ...COMMON_ROUTE_MACHINE_KEYS, + 'socialProviders', + 'toFederatedSignIn', + 'toResetPassword', + 'toSignUp', +]; +const SIGN_UP_MACHINE_KEYS: SignUpMachineKey[] = [ + ...COMMON_ROUTE_MACHINE_KEYS, + 'toSignIn', + 'validationErrors', +]; +const SETUP_TOTP_MACHINE_KEYS: SetupTOTPMachineKey[] = [ + ...COMMON_ROUTE_MACHINE_KEYS, + 'toSignIn', +]; +const VERIFY_USER_MACHINE_KEYS: VerifyUserMachineKey[] = [ + ...COMMON_ROUTE_MACHINE_KEYS, + 'skipVerification', +]; + +export const MACHINE_PROP_KEYS: Record< + AuthenticatorRouteComponentKey, + AuthenticatorMachineContextKey[] +> = { + confirmResetPassword: CONFIRM_RESET_PASSWORD_MACHINE_KEYS, + confirmSignIn: CONFIRM_SIGN_IN_MACHINE_KEYS, + confirmSignUp: CONFIRM_SIGN_UP_MACHINE_KEYS, + confirmVerifyUser: CONFIRM_VERIFY_USER_MACHINE_KEYS, + forceNewPassword: FORCE_NEW_PASSWORD_MACHINE_KEYS, + signIn: SIGN_IN_MACHINE_KEYS, + signUp: SIGN_UP_MACHINE_KEYS, + resetPassword: RESET_PASSWORD_MACHINE_KEYS, + setupTOTP: SETUP_TOTP_MACHINE_KEYS, + verifyUser: VERIFY_USER_MACHINE_KEYS, +}; diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/index.ts b/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/index.ts new file mode 100644 index 00000000000..a08713a63b8 --- /dev/null +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/index.ts @@ -0,0 +1,2 @@ +export { default as useAuthenticatorRoute } from './useAuthenticatorRoute'; +export * from './types'; diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/types.ts b/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/types.ts new file mode 100644 index 00000000000..e7f4b3c849f --- /dev/null +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/types.ts @@ -0,0 +1,113 @@ +import { + AuthenticatorMachineContext, + AuthenticatorMachineContextKey, + AuthenticatorRouteComponentName, + CommonRouteProps, + ConfirmResetPasswordBaseProps, + ConfirmSignInBaseProps, + ConfirmSignUpBaseProps, + Defaults, + DefaultProps, + ForceResetPasswordBaseProps, + ResetPasswordBaseProps, + SetupTOTPBaseProps, + SignInBaseProps, + SignUpBaseProps, + VerifyUserProps, + ConfirmVerifyUserProps, +} from '../types'; + +export type UseAuthenticatorRouteParams = { + components: Defaults; +}; +export type UseAuthenticatorRoute< + ComponentName extends AuthenticatorRouteComponentName, + FieldType = {} +> = { + Component: Defaults[ComponentName]; + props: DefaultProps[ComponentName]; +}; + +export type UseAuthenticatorRouteDefault = { + Component: Defaults[AuthenticatorRouteComponentName]; + props: DefaultProps[AuthenticatorRouteComponentName]; +}; + +// extract machine prop keys required for a sub-component route +type ExtractMachineKey = Extract< + AuthenticatorMachineContextKey, + keyof RouteProps +>; + +// map to `handleBlur`, `handleChange`, and `handleSubmit` props +export type FormEventHandlerMachineKey = + | 'updateBlur' + | 'updateForm' + | 'submitForm'; + +export type FormEventHandlerPropKey = + | `handleBlur` + | `handleChange` + | `handleSubmit`; + +// common route keys shared by all routes +export type CommonRouteMachineKey = + | ExtractMachineKey + | FormEventHandlerMachineKey; + +/** + * `route` sub-component machine selector key types + */ +export type ConfirmResetPasswordMachineKey = + | ExtractMachineKey + | CommonRouteMachineKey; + +export type ConfirmSignInMachineKey = + | ExtractMachineKey + | CommonRouteMachineKey + // ConfirmSignIn requires `user` to extract value needed for `challengeName` + | 'user'; + +export type ConfirmSignUpMachineKey = + | ExtractMachineKey + | CommonRouteMachineKey; + +export type ConfirmVerifyUserMachineKey = + | ExtractMachineKey + | CommonRouteMachineKey; + +export type ForceNewPasswordMachineKey = + | ExtractMachineKey + | CommonRouteMachineKey; + +export type ResetPasswordMachineKey = + | ExtractMachineKey + | CommonRouteMachineKey; + +export type SetupTOTPMachineKey = + | ExtractMachineKey + | CommonRouteMachineKey; + +export type SignInMachineKey = + | ExtractMachineKey + | CommonRouteMachineKey; + +export type SignUpMachineKey = + | ExtractMachineKey + | CommonRouteMachineKey; + +export type VerifyUserMachineKey = + | ExtractMachineKey + | CommonRouteMachineKey; + +/** + * machine values with machine form event handlers keys mapped to UI form event handlers + */ +export type ConvertedMachineProps = Omit< + AuthenticatorMachineContext, + FormEventHandlerMachineKey +> & { + handleBlur: AuthenticatorMachineContext['updateBlur']; + handleChange: AuthenticatorMachineContext['updateForm']; + handleSubmit: AuthenticatorMachineContext['submitForm']; +}; diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/useAuthenticatorRoute.ts b/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/useAuthenticatorRoute.ts new file mode 100644 index 00000000000..777eb32622e --- /dev/null +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/useAuthenticatorRoute.ts @@ -0,0 +1,126 @@ +import { useMemo } from 'react'; +import { useAuthenticator } from '../useAuthenticator'; + +import { + UseAuthenticatorRoute, + UseAuthenticatorRouteDefault, + UseAuthenticatorRouteParams, +} from './types'; +import { + getRouteMachineSelector, + routeSelector, + resolveConfirmResetPasswordRoute, + resolveConfirmSignInRoute, + resolveConfirmSignUpRoute, + resolveConfirmVerifyUserRoute, + resolveDefault, + resolveForceNewPasswordRoute, + resolveResetPasswordRoute, + resolveSetupTOTPRoute, + resolveSignInRoute, + resolveSignUpRoute, + resolveVerifyUserRoute, +} from './utils'; + +export default function useAuthenticatorRoute( + params: UseAuthenticatorRouteParams +): UseAuthenticatorRoute<'ConfirmResetPassword'>; +export default function useAuthenticatorRoute( + params: UseAuthenticatorRouteParams +): UseAuthenticatorRoute<'ConfirmSignIn'>; +export default function useAuthenticatorRoute( + params: UseAuthenticatorRouteParams +): UseAuthenticatorRoute<'ConfirmSignUp'>; +export default function useAuthenticatorRoute( + params: UseAuthenticatorRouteParams +): UseAuthenticatorRoute<'ConfirmVerifyUser'>; +export default function useAuthenticatorRoute( + params: UseAuthenticatorRouteParams +): UseAuthenticatorRoute<'ForceNewPassword'>; +export default function useAuthenticatorRoute( + params: UseAuthenticatorRouteParams +): UseAuthenticatorRoute<'ResetPassword'>; +export default function useAuthenticatorRoute( + params: UseAuthenticatorRouteParams +): UseAuthenticatorRoute<'SetupTOTP'>; +export default function useAuthenticatorRoute( + params: UseAuthenticatorRouteParams +): UseAuthenticatorRoute<'SignIn'>; +export default function useAuthenticatorRoute( + params: UseAuthenticatorRouteParams +): UseAuthenticatorRoute<'SignUp'>; +export default function useAuthenticatorRoute( + params: UseAuthenticatorRouteParams +): UseAuthenticatorRoute<'VerifyUser'>; +export default function useAuthenticatorRoute({ + components, +}: UseAuthenticatorRouteParams): UseAuthenticatorRouteDefault { + const { route } = useAuthenticator(routeSelector); + + const routeMachineSelector = useMemo( + () => getRouteMachineSelector(route), + [route] + ); + + // `useAuthenticator` exposes both state machine (example: `toSignIn`) and non-state machine + // props (example: `getTotpSecretCode`). `routeSelector` specifies which state machine props + // should be returned for a specific route. + // Only state machine props specified by the current `routeSelector` will have their current value + // returned by `useAuthenticator`, non-machine props returned will always be the current value + const routeSelectorProps = useAuthenticator(routeMachineSelector); + + const { + ConfirmResetPassword, + ConfirmSignIn, + ConfirmSignUp, + ConfirmVerifyUser, + ForceNewPassword, + ResetPassword, + SetupTOTP, + SignIn, + SignUp, + VerifyUser, + } = components; + + switch (route) { + case 'confirmResetPassword': { + return resolveConfirmResetPasswordRoute( + ConfirmResetPassword, + routeSelectorProps + ); + } + case 'confirmSignIn': { + return resolveConfirmSignInRoute(ConfirmSignIn, routeSelectorProps); + } + case 'confirmSignUp': { + return resolveConfirmSignUpRoute(ConfirmSignUp, routeSelectorProps); + } + case 'confirmVerifyUser': { + return resolveConfirmVerifyUserRoute( + ConfirmVerifyUser, + routeSelectorProps + ); + } + case 'forceNewPassword': { + return resolveForceNewPasswordRoute(ForceNewPassword, routeSelectorProps); + } + case 'resetPassword': { + return resolveResetPasswordRoute(ResetPassword, routeSelectorProps); + } + case 'setupTOTP': { + return resolveSetupTOTPRoute(SetupTOTP, routeSelectorProps); + } + case 'signIn': { + return resolveSignInRoute(SignIn, routeSelectorProps); + } + case 'signUp': { + return resolveSignUpRoute(SignUp, routeSelectorProps); + } + case 'verifyUser': { + return resolveVerifyUserRoute(VerifyUser, routeSelectorProps); + } + default: { + return resolveDefault(); + } + } +} diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/utils.ts b/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/utils.ts new file mode 100644 index 00000000000..629ffea5bf6 --- /dev/null +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticatorRoute/utils.ts @@ -0,0 +1,208 @@ +import { AuthenticatorRoute } from '@aws-amplify/ui'; + +import { RenderNothing } from '../../../components'; +import { + AuthenticatorMachineContext, + AuthenticatorMachineContextKey, + AuthenticatorRouteComponentKey, + DefaultComponentType, + DefaultPropsType, + Defaults, +} from '../types'; + +import { + UseAuthenticator, + UseAuthenticatorSelector, +} from '../useAuthenticator'; +import { isComponentRouteKey } from '../utils'; +import { MACHINE_PROP_KEYS, EVENT_HANDLER_KEY_MAP } from './constants'; +import { + ConvertedMachineProps, + FormEventHandlerMachineKey, + FormEventHandlerPropKey, + UseAuthenticatorRoute, + UseAuthenticatorRouteDefault, +} from './types'; + +// only select `route` from machine context +export const routeSelector: UseAuthenticatorSelector = ({ route }) => [route]; + +const createSelector = + (selectorKeys: AuthenticatorMachineContextKey[]): UseAuthenticatorSelector => + (context) => { + const dependencies = selectorKeys.map((key) => context[key]); + // route should always be part of deps, so hook knows when route changes. + return [...dependencies, context.route]; + }; + +export const getRouteMachineSelector = ( + route: AuthenticatorRoute +): UseAuthenticatorSelector => + isComponentRouteKey(route) + ? createSelector(MACHINE_PROP_KEYS[route]) + : routeSelector; + +const isFormEventHandlerKey = ( + key: AuthenticatorMachineContextKey +): key is FormEventHandlerMachineKey => + ['updateBlur', 'updateForm', 'submitForm'].includes(key); + +const convertEventHandlerKey = ( + key: FormEventHandlerMachineKey +): FormEventHandlerPropKey => EVENT_HANDLER_KEY_MAP[key]; + +const getConvertedMachineProps = ( + route: AuthenticatorRouteComponentKey, + context: AuthenticatorMachineContext +): ConvertedMachineProps => + MACHINE_PROP_KEYS[route].reduce( + (acc, key) => ({ + ...acc, + [isFormEventHandlerKey(key) ? convertEventHandlerKey(key) : key]: + context[key], + }), + {} as ConvertedMachineProps + ); + +export function resolveConfirmResetPasswordRoute( + Component: Defaults['ConfirmResetPassword'], + props: UseAuthenticator +): UseAuthenticatorRoute<'ConfirmResetPassword', FieldType> { + return { + Component, + props: { + ...Component, + ...getConvertedMachineProps('confirmResetPassword', props), + }, + }; +} + +export function resolveConfirmSignInRoute( + Component: Defaults['ConfirmSignIn'], + props: UseAuthenticator +): UseAuthenticatorRoute<'ConfirmSignIn', FieldType> { + const { user, ...machineProps } = getConvertedMachineProps( + 'confirmSignIn', + props + ); + + // prior to the `confirmSignIn` route, `user.username` is populated + const challengeName = user.challengeName!; + + return { Component, props: { ...Component, ...machineProps, challengeName } }; +} + +export function resolveConfirmSignUpRoute( + Component: Defaults['ConfirmSignUp'], + props: UseAuthenticator +): UseAuthenticatorRoute<'ConfirmSignUp', FieldType> { + return { + Component, + props: { + ...Component, + ...getConvertedMachineProps('confirmSignUp', props), + }, + }; +} + +export function resolveConfirmVerifyUserRoute( + Component: Defaults['ConfirmVerifyUser'], + props: UseAuthenticator +): UseAuthenticatorRoute<'ConfirmVerifyUser', FieldType> { + return { + Component, + props: { + ...Component, + ...getConvertedMachineProps('confirmVerifyUser', props), + }, + }; +} + +export function resolveForceNewPasswordRoute( + Component: Defaults['ForceNewPassword'], + props: UseAuthenticator +): UseAuthenticatorRoute<'ForceNewPassword', FieldType> { + return { + Component, + props: { + ...Component, + ...getConvertedMachineProps('forceNewPassword', props), + }, + }; +} + +export function resolveResetPasswordRoute( + Component: Defaults['ResetPassword'], + props: UseAuthenticator +): UseAuthenticatorRoute<'ResetPassword', FieldType> { + return { + Component, + props: { + ...Component, + ...getConvertedMachineProps('resetPassword', props), + }, + }; +} + +export function resolveSetupTOTPRoute( + Component: Defaults['SetupTOTP'], + { getTotpSecretCode, ...props }: UseAuthenticator +): UseAuthenticatorRoute<'SetupTOTP', FieldType> { + return { + Component, + props: { + ...Component, + ...getConvertedMachineProps('setupTOTP', props), + getTotpSecretCode, + }, + }; +} + +export function resolveSignInRoute( + Component: Defaults['SignIn'], + props: UseAuthenticator +): UseAuthenticatorRoute<'SignIn', FieldType> { + // default `hideSignUp` to false + const hideSignUp = false; + + return { + Component, + props: { + ...Component, + ...getConvertedMachineProps('signIn', props), + hideSignUp, + }, + }; +} + +export function resolveSignUpRoute( + Component: Defaults['SignUp'], + props: UseAuthenticator +): UseAuthenticatorRoute<'SignUp', FieldType> { + return { + Component, + props: { ...Component, ...getConvertedMachineProps('signUp', props) }, + }; +} + +export function resolveVerifyUserRoute( + Component: Defaults['VerifyUser'], + props: UseAuthenticator +): UseAuthenticatorRoute<'VerifyUser', FieldType> { + return { + Component, + props: { + ...Component, + ...getConvertedMachineProps('verifyUser', props), + }, + }; +} + +export function resolveDefault< + FieldType = {} +>(): UseAuthenticatorRouteDefault { + return { + Component: RenderNothing as DefaultComponentType, + props: {} as DefaultPropsType, + }; +} diff --git a/packages/react-core/src/Authenticator/hooks/utils.ts b/packages/react-core/src/Authenticator/hooks/utils.ts new file mode 100644 index 00000000000..56f154676d4 --- /dev/null +++ b/packages/react-core/src/Authenticator/hooks/utils.ts @@ -0,0 +1,38 @@ +import { AuthenticatorRoute } from '@aws-amplify/ui'; + +import { COMPONENT_ROUTE_KEYS, COMPONENT_ROUTE_NAMES } from './constants'; +import { AuthenticatorRouteComponentKey, Defaults, Overrides } from './types'; + +export const isComponentRouteKey = ( + route: AuthenticatorRoute +): route is AuthenticatorRouteComponentKey => + COMPONENT_ROUTE_KEYS.some((componentRoute) => componentRoute === route); + +export function resolveAuthenticatorComponents( + defaults: Defaults, + overrides?: Overrides +): Defaults { + if (!overrides) { + return defaults; + } + + return COMPONENT_ROUTE_NAMES.reduce((components, route) => { + const Default = defaults[route]; + const Override = overrides[route]; + + if (typeof Override !== 'function') { + return { ...components, [route]: Default }; + } + + const { Footer, FormFields, Header } = Default; + + // cast to allow assigning of component slots + const Component = Override as typeof Default; + + Component.Footer = Footer; + Component.FormFields = FormFields; + Component.Header = Header; + + return { ...components, [route]: Component }; + }, {} as Defaults); +} diff --git a/packages/react-core/src/Authenticator/index.ts b/packages/react-core/src/Authenticator/index.ts index 472021f7e91..6b4f6401a57 100644 --- a/packages/react-core/src/Authenticator/index.ts +++ b/packages/react-core/src/Authenticator/index.ts @@ -1,2 +1,23 @@ -export * from './context'; -export * from './hooks'; +export { AuthenticatorProvider, AuthenticatorContext } from './context'; +export { + resolveAuthenticatorComponents, + useAuthenticator, + useAuthenticatorRoute, + UseAuthenticator, + useAuthenticatorInitMachine, + UseAuthenticatorRoute, +} from './hooks'; +export { + Overrides as AuthenticatorComponentOverrides, + Defaults as AuthenticatorComponentDefaults, + DefaultProps as AuthenticatorComponentDefaultProps, + FooterComponent as AuthenticatorFooterComponent, + FormFieldsComponent as AuthenticatorFormFieldsComponent, + HeaderComponent as AuthenticatorHeaderComponent, + isComponentRouteKey as isAuthenticatorComponentRouteKey, + AuthenticatorRouteComponentKey, + AuthenticatorRouteComponentName, + AuthenticatorLegacyField, + AuthenticatorMachineContext, + FormFieldsComponent, +} from './hooks'; diff --git a/packages/react-core/src/__tests__/__snapshots__/index.spec.ts.snap b/packages/react-core/src/__tests__/__snapshots__/index.spec.ts.snap index ca177b63225..96306831d99 100644 --- a/packages/react-core/src/__tests__/__snapshots__/index.spec.ts.snap +++ b/packages/react-core/src/__tests__/__snapshots__/index.spec.ts.snap @@ -5,8 +5,14 @@ Array [ "AuthenticatorProvider", "InAppMessagingProvider", "handleMessageAction", + "isAuthenticatorComponentRouteKey", + "resolveAuthenticatorComponents", "useAuthenticator", + "useAuthenticatorInitMachine", + "useAuthenticatorRoute", + "useHasValueUpdated", "useInAppMessaging", "useMessage", + "usePreviousValue", ] `; diff --git a/packages/react-core/src/components/RenderNothing/RenderNothing.tsx b/packages/react-core/src/components/RenderNothing/RenderNothing.tsx index 8f4adf54ef0..b8b35a490c8 100644 --- a/packages/react-core/src/components/RenderNothing/RenderNothing.tsx +++ b/packages/react-core/src/components/RenderNothing/RenderNothing.tsx @@ -1,6 +1,6 @@ /** * Utility component for rendering nothing. */ -export default function RenderNothng(_: Props): JSX.Element | null { +export default function RenderNothing(_: Props): JSX.Element | null { return null; } diff --git a/packages/react-core/src/components/RenderNothing/__tests__/RenderNothing.spec.tsx b/packages/react-core/src/components/RenderNothing/__tests__/RenderNothing.spec.tsx index 01c182d08de..11aeb580b2b 100644 --- a/packages/react-core/src/components/RenderNothing/__tests__/RenderNothing.spec.tsx +++ b/packages/react-core/src/components/RenderNothing/__tests__/RenderNothing.spec.tsx @@ -1,11 +1,11 @@ import React from 'react'; import TestRenderer from 'react-test-renderer'; -import RenderNothng from '../RenderNothing'; +import RenderNothing from '../RenderNothing'; -describe('RenderNothng', () => { +describe('RenderNothing', () => { it('renders nothing', () => { - const renderer = TestRenderer.create(); + const renderer = TestRenderer.create(); expect(renderer.toJSON()).toBeNull(); }); diff --git a/packages/react-core/src/hooks/__tests__/useHasValueUpdated.spec.ts b/packages/react-core/src/hooks/__tests__/useHasValueUpdated.spec.ts new file mode 100644 index 00000000000..f5ed79adead --- /dev/null +++ b/packages/react-core/src/hooks/__tests__/useHasValueUpdated.spec.ts @@ -0,0 +1,51 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import useHasValueUpdated from '../useHasValueUpdated'; + +describe('useHasValueUpdated', () => { + it('return true on initial render and ignoring first render', () => { + const value = 'value'; + const { result } = renderHook(() => useHasValueUpdated(value)); + + expect(result.current).toBe(true); + }); + + it('return false on initial render when not ignoring first render', () => { + const value = 'value'; + const { result } = renderHook(() => useHasValueUpdated(value, true)); + + expect(result.current).toBe(false); + }); + + it('return false when current and updated values are equal', () => { + const value = 'value'; + + const { result, rerender } = renderHook((nextValue = value) => + useHasValueUpdated(nextValue, true) + ); + + expect(result.current).toBe(false); + + const sameValue = 'value'; + + rerender(sameValue); + + expect(result.current).toBe(false); + }); + + it('returns true when value has updated', () => { + const value = 'value'; + + const { result, rerender } = renderHook((nextValue = value) => + useHasValueUpdated(nextValue, true) + ); + + expect(result.current).toBe(false); + + const updatedValue = 'updatedValue'; + + rerender(updatedValue); + + expect(result.current).toBe(true); + }); +}); diff --git a/packages/react-core/src/hooks/__tests__/usePreviousValue.spec.ts b/packages/react-core/src/hooks/__tests__/usePreviousValue.spec.ts new file mode 100644 index 00000000000..5231e7d5c5d --- /dev/null +++ b/packages/react-core/src/hooks/__tests__/usePreviousValue.spec.ts @@ -0,0 +1,22 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import usePreviousValue from '../usePreviousValue'; + +describe('usePreviousValue', () => { + it('returns undefined on the initial render', () => { + const value = 'value'; + const { result } = renderHook(() => usePreviousValue(value)); + + expect(result.current).toBeUndefined(); + }); + it('returns the previous value after the initial render', () => { + const value = 'value'; + const { result, rerender } = renderHook(() => usePreviousValue(value)); + + expect(result.current).toBeUndefined(); + + rerender(); + + expect(result.current).toBe(value); + }); +}); diff --git a/packages/react-core/src/hooks/index.ts b/packages/react-core/src/hooks/index.ts new file mode 100644 index 00000000000..47507475a9d --- /dev/null +++ b/packages/react-core/src/hooks/index.ts @@ -0,0 +1,2 @@ +export { default as usePreviousValue } from './usePreviousValue'; +export { default as useHasValueUpdated } from './useHasValueUpdated'; diff --git a/packages/react-core/src/hooks/useHasValueUpdated.ts b/packages/react-core/src/hooks/useHasValueUpdated.ts new file mode 100644 index 00000000000..0c6a268cd21 --- /dev/null +++ b/packages/react-core/src/hooks/useHasValueUpdated.ts @@ -0,0 +1,14 @@ +import usePreviousValue from './usePreviousValue'; +import isUndefined from 'lodash/isUndefined'; + +export default function useHasValueUpdated( + value: Value, + ignoreFirstRender = false +): boolean { + const previous = usePreviousValue(value); + const shouldIgnoreChange = isUndefined(previous) && ignoreFirstRender; + if (shouldIgnoreChange) { + return false; + } + return previous !== value; +} diff --git a/packages/react-core/src/hooks/usePreviousValue.ts b/packages/react-core/src/hooks/usePreviousValue.ts new file mode 100644 index 00000000000..7824e2fdd6c --- /dev/null +++ b/packages/react-core/src/hooks/usePreviousValue.ts @@ -0,0 +1,15 @@ +import { useEffect, useRef } from 'react'; + +export default function usePreviousValue( + value: Value +): Value | undefined { + const previous = useRef(); + + // update ref post render + useEffect(() => { + previous.current = value; + }, [value]); + + // return previous ref + return previous.current; +} diff --git a/packages/react-core/src/index.ts b/packages/react-core/src/index.ts index a9429d82bab..4d08607b64c 100644 --- a/packages/react-core/src/index.ts +++ b/packages/react-core/src/index.ts @@ -1,7 +1,23 @@ +// features export { + AuthenticatorComponentDefaults, + AuthenticatorComponentDefaultProps, + AuthenticatorComponentOverrides, + AuthenticatorFooterComponent, + AuthenticatorFormFieldsComponent, + AuthenticatorHeaderComponent, + AuthenticatorLegacyField, + AuthenticatorMachineContext, AuthenticatorProvider, + AuthenticatorRouteComponentKey, + AuthenticatorRouteComponentName, + isAuthenticatorComponentRouteKey, + resolveAuthenticatorComponents, useAuthenticator, + useAuthenticatorRoute, UseAuthenticator, + useAuthenticatorInitMachine, + UseAuthenticatorRoute, } from './Authenticator'; export { @@ -28,3 +44,6 @@ export { useInAppMessaging, useMessage, } from './InAppMessaging'; + +// components/hooks/utils +export { useHasValueUpdated, usePreviousValue } from './hooks'; diff --git a/packages/react-native/.eslintrc.js b/packages/react-native/.eslintrc.js index 02b7ad48251..c35381ac610 100644 --- a/packages/react-native/.eslintrc.js +++ b/packages/react-native/.eslintrc.js @@ -1,7 +1,13 @@ module.exports = { env: { node: true }, root: true, - ignorePatterns: ['dist', '.eslintrc.js', 'babel.config.js', 'jest.config.js'], + ignorePatterns: [ + 'dist', + '.eslintrc.js', + 'babel.config.js', + 'jest.config.js', + 'jest.setup.js', + ], extends: [ 'eslint:recommended', 'plugin:jest/recommended', diff --git a/packages/react-native/jest.config.js b/packages/react-native/jest.config.js index 924c90102cc..a07d7631417 100644 --- a/packages/react-native/jest.config.js +++ b/packages/react-native/jest.config.js @@ -17,4 +17,5 @@ module.exports = { statements: 90, }, }, + setupFiles: ['/jest.setup.js'], }; diff --git a/packages/react-native/jest.setup.js b/packages/react-native/jest.setup.js new file mode 100644 index 00000000000..d4c1369d7a3 --- /dev/null +++ b/packages/react-native/jest.setup.js @@ -0,0 +1,3 @@ +global.navigator = { + ClientDevice_Browser: jest.fn().mockImplementation(() => Promise.resolve()), +}; diff --git a/packages/react-native/package.json b/packages/react-native/package.json index f3118c18ee1..e2133ac34a5 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -12,13 +12,15 @@ "dist": "tsc --project tsconfig.dist.json", "lint": "tsc --noEmit && eslint src", "prebuild": "rimraf dist", - "test": "jest --coverage", + "test": "jest --coverage --verbose", "test:watch": "yarn test --watch" }, "devDependencies": { "@babel/cli": "^7.17.10", "@babel/core": "^7.17.10", "@babel/preset-env": "^7.17.10", + "@react-native-clipboard/clipboard": "^1.11.1", + "@react-native-picker/picker": "^2.4.8", "@testing-library/react-hooks": "^8.0.0", "@testing-library/react-native": "^11.1.0", "@types/react": "^17.0.2", @@ -39,7 +41,8 @@ "react-native": "^0.68.1", "react-native-safe-area-context": "^4.2.5", "react-test-renderer": "^17.0.2", - "rimraf": "^3.0.2" + "rimraf": "^3.0.2", + "type-fest": "^2.3.4" }, "dependencies": { "@aws-amplify/ui": "5.0.0", diff --git a/packages/react-native/src/Authenticator/Authenticator.tsx b/packages/react-native/src/Authenticator/Authenticator.tsx new file mode 100644 index 00000000000..701bfb3aecd --- /dev/null +++ b/packages/react-native/src/Authenticator/Authenticator.tsx @@ -0,0 +1,97 @@ +import React, { useMemo } from 'react'; + +import { + AuthenticatorProvider as Provider, + AuthenticatorMachineContext, + resolveAuthenticatorComponents, + useAuthenticator, + useAuthenticatorRoute, + useAuthenticatorInitMachine, + UseAuthenticator, +} from '@aws-amplify/ui-react-core'; + +import { DefaultContainer, InnerContainer } from './common'; +import { TypedField, getRouteTypedFields } from './hooks'; +import { AuthenticatorProps } from './types'; + +import { + ConfirmResetPassword, + ConfirmSignIn, + ConfirmSignUp, + ConfirmVerifyUser, + ForceNewPassword, + ResetPassword, + SetupTOTP, + SignIn, + SignUp, + VerifyUser, +} from './Defaults'; + +const DEFAULTS = { + ConfirmResetPassword, + ConfirmSignIn, + ConfirmSignUp, + ConfirmVerifyUser, + ForceNewPassword, + ResetPassword, + SetupTOTP, + SignIn, + SignUp, + VerifyUser, +}; + +const isAuthenticatedRoute = (route: UseAuthenticator['route']) => + route === 'authenticated' || route === 'signOut'; + +const routePropSelector = ({ + route, +}: AuthenticatorMachineContext): AuthenticatorMachineContext['route'][] => [ + route, +]; + +function Authenticator({ + children, + components: overrides, + ...options +}: AuthenticatorProps): JSX.Element | null { + useAuthenticatorInitMachine(options); + + const { fields, route } = useAuthenticator(routePropSelector); + + const components = useMemo( + // allow any to prevent TS from assuming that all fields are of type `TextFieldOptions` + () => resolveAuthenticatorComponents(DEFAULTS, overrides), + [overrides] + ); + + const { Component, props } = useAuthenticatorRoute({ components }); + + const typedFields = getRouteTypedFields({ fields, route }); + + if (isAuthenticatedRoute(route)) { + return children ? <>{children} : null; + } + + return ( + + + + + + ); +} + +// assign slot components +Authenticator.Provider = Provider; +Authenticator.ConfirmResetPassword = ConfirmResetPassword; +Authenticator.ConfirmSignIn = ConfirmSignIn; +Authenticator.ConfirmSignUp = ConfirmSignUp; +Authenticator.ConfirmVerifyUser = ConfirmVerifyUser; +Authenticator.ForceNewPassword = ForceNewPassword; +Authenticator.ResetPassword = ResetPassword; +Authenticator.SetupTOTP = SetupTOTP; +Authenticator.SignIn = SignIn; +Authenticator.SignUp = SignUp; +Authenticator.VerifyUser = VerifyUser; + +export default Authenticator; diff --git a/packages/react-native/src/Authenticator/Defaults/ConfirmResetPassword/ConfirmResetPassword.tsx b/packages/react-native/src/Authenticator/Defaults/ConfirmResetPassword/ConfirmResetPassword.tsx new file mode 100644 index 00000000000..c365ce3041a --- /dev/null +++ b/packages/react-native/src/Authenticator/Defaults/ConfirmResetPassword/ConfirmResetPassword.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { authenticatorTextUtil } from '@aws-amplify/ui'; + +import { Button, ErrorMessage } from '../../../primitives'; +import { + DefaultFooter, + DefaultTextFormFields, + DefaultHeader, +} from '../../common'; +import { useFieldValues } from '../../hooks'; + +import { DefaultConfirmResetPasswordComponent } from '../types'; +import { styles } from './styles'; + +const COMPONENT_NAME = 'ConfirmResetPassword'; + +const { + getResetYourPasswordText, + getSubmitText, + getSubmittingText, + getResendCodeText, +} = authenticatorTextUtil; + +const ConfirmResetPassword: DefaultConfirmResetPasswordComponent = ({ + error, + fields, + Footer, + FormFields, + handleBlur, + handleChange, + handleSubmit, + Header, + isPending, + resendCode, +}) => { + const { fields: fieldsWithHandlers, handleFormSubmit } = useFieldValues({ + componentName: COMPONENT_NAME, + fields, + handleBlur, + handleChange, + handleSubmit, + }); + + return ( + <> +

{getResetYourPasswordText()}
+ + {error ? {error} : null} + + +