diff --git a/examples/next/pages/ui/components/authenticator/sign-in-with-email-mfa-selection/aws-exports.js b/examples/next/pages/ui/components/authenticator/sign-in-with-email-mfa-selection/aws-exports.js new file mode 100644 index 00000000000..57989e0a0e1 --- /dev/null +++ b/examples/next/pages/ui/components/authenticator/sign-in-with-email-mfa-selection/aws-exports.js @@ -0,0 +1,2 @@ +import awsExports from '@environments/auth/gen2/auth-with-email-mfa/amplify_outputs.json'; +export default awsExports; diff --git a/examples/next/pages/ui/components/authenticator/sign-in-with-email-mfa-selection/index.page.tsx b/examples/next/pages/ui/components/authenticator/sign-in-with-email-mfa-selection/index.page.tsx new file mode 100644 index 00000000000..8e8149a5d11 --- /dev/null +++ b/examples/next/pages/ui/components/authenticator/sign-in-with-email-mfa-selection/index.page.tsx @@ -0,0 +1,59 @@ +import { Amplify } from 'aws-amplify'; + +import { Authenticator } from '@aws-amplify/ui-react'; +import '@aws-amplify/ui-react/styles.css'; + +import awsExports from './aws-exports'; +import { AuthContext } from '@aws-amplify/ui'; +Amplify.configure(awsExports); + +const customServices: AuthContext['services'] = { + handleSignIn: async () => { + return { + isSignedIn: false, + nextStep: { + signInStep: 'CONTINUE_SIGN_IN_WITH_MFA_SELECTION', + allowedMFATypes: ['EMAIL', 'TOTP'], + }, + }; + }, + handleConfirmSignIn: async ({ challengeResponse }) => { + if (challengeResponse === 'EMAIL') { + return { + isSignedIn: false, + nextStep: { + signInStep: 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE', + }, + }; + } + + if (/^\d{6}$/.test(challengeResponse)) { + return { + isSignedIn: true, + nextStep: { + signInStep: 'DONE', + }, + }; + } + throw new Error('Invalid code or auth state for the user.'); + }, + getCurrentUser: async () => { + return { + userId: '******************', + username: 'james', + }; + }, +}; + +export default function App() { + return ( + + {({ signOut, user }) => ( +
+

Hello {user.username}

+ +
+ )} +
+ ); +} diff --git a/examples/next/pages/ui/components/authenticator/sign-in-with-email-mfa-setup-selection/aws-exports.js b/examples/next/pages/ui/components/authenticator/sign-in-with-email-mfa-setup-selection/aws-exports.js new file mode 100644 index 00000000000..57989e0a0e1 --- /dev/null +++ b/examples/next/pages/ui/components/authenticator/sign-in-with-email-mfa-setup-selection/aws-exports.js @@ -0,0 +1,2 @@ +import awsExports from '@environments/auth/gen2/auth-with-email-mfa/amplify_outputs.json'; +export default awsExports; diff --git a/examples/next/pages/ui/components/authenticator/sign-in-with-email-mfa-setup-selection/index.page.tsx b/examples/next/pages/ui/components/authenticator/sign-in-with-email-mfa-setup-selection/index.page.tsx new file mode 100644 index 00000000000..96b115d8691 --- /dev/null +++ b/examples/next/pages/ui/components/authenticator/sign-in-with-email-mfa-setup-selection/index.page.tsx @@ -0,0 +1,89 @@ +import { Amplify } from 'aws-amplify'; + +import { Authenticator } from '@aws-amplify/ui-react'; +import '@aws-amplify/ui-react/styles.css'; + +import awsExports from './aws-exports'; +import { AuthContext, emailRegex } from '@aws-amplify/ui'; +Amplify.configure(awsExports); + +const customServices: AuthContext['services'] = { + handleSignUp: async () => { + return { + isSignUpComplete: true, + userId: '******************', + nextStep: { + signUpStep: 'COMPLETE_AUTO_SIGN_IN', + }, + }; + }, + handleAutoSignIn: async () => { + return { + isSignedIn: false, + nextStep: { + signInStep: 'CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION', + allowedMFATypes: ['EMAIL', 'TOTP'], + }, + }; + }, + handleSignIn: async () => { + return { + isSignedIn: false, + nextStep: { + signInStep: 'CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION', + allowedMFATypes: ['EMAIL', 'TOTP'], + }, + }; + }, + handleConfirmSignIn: async ({ challengeResponse }) => { + if (challengeResponse === 'EMAIL') { + return { + isSignedIn: false, + nextStep: { + signInStep: 'CONTINUE_SIGN_IN_WITH_EMAIL_SETUP', + }, + }; + } + if (emailRegex.test(challengeResponse)) { + return { + isSignedIn: false, + nextStep: { + signInStep: 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE', + codeDeliveryDetails: { + destination: 'a***@e***.com', + deliveryMedium: 'EMAIL', + attributeName: 'email', + }, + }, + }; + } + if (/^\d{6}$/.test(challengeResponse)) { + return { + isSignedIn: true, + nextStep: { + signInStep: 'DONE', + }, + }; + } + throw new Error('Invalid code or auth state for the user.'); + }, + getCurrentUser: async () => { + return { + userId: '******************', + username: 'james', + }; + }, +}; + +export default function App() { + return ( + + {({ signOut, user }) => ( +
+

Hello {user.username}

+ +
+ )} +
+ ); +} diff --git a/examples/next/pages/ui/components/authenticator/sign-in-with-email-mfa/aws-exports.js b/examples/next/pages/ui/components/authenticator/sign-in-with-email-mfa/aws-exports.js new file mode 100644 index 00000000000..57989e0a0e1 --- /dev/null +++ b/examples/next/pages/ui/components/authenticator/sign-in-with-email-mfa/aws-exports.js @@ -0,0 +1,2 @@ +import awsExports from '@environments/auth/gen2/auth-with-email-mfa/amplify_outputs.json'; +export default awsExports; diff --git a/examples/next/pages/ui/components/authenticator/sign-in-with-email-mfa/index.page.tsx b/examples/next/pages/ui/components/authenticator/sign-in-with-email-mfa/index.page.tsx new file mode 100644 index 00000000000..4becc4d5a50 --- /dev/null +++ b/examples/next/pages/ui/components/authenticator/sign-in-with-email-mfa/index.page.tsx @@ -0,0 +1,54 @@ +import { Amplify } from 'aws-amplify'; + +import { Authenticator } from '@aws-amplify/ui-react'; +import '@aws-amplify/ui-react/styles.css'; + +import awsExports from './aws-exports'; +import { AuthContext } from '@aws-amplify/ui'; +Amplify.configure(awsExports); + +const customServices: AuthContext['services'] = { + handleSignIn: async () => { + return { + isSignedIn: false, + nextStep: { + signInStep: 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE', + codeDeliveryDetails: { + destination: 'a***@e***.com', + deliveryMedium: 'EMAIL', + attributeName: 'email', + }, + }, + }; + }, + handleConfirmSignIn: async ({ challengeResponse }) => { + if (/^\d{6}$/.test(challengeResponse)) { + return { + isSignedIn: true, + nextStep: { + signInStep: 'DONE', + }, + }; + } + throw new Error('Invalid code or auth state for the user.'); + }, + getCurrentUser: async () => { + return { + userId: '******************', + username: 'james', + }; + }, +}; + +export default function App() { + return ( + + {({ signOut, user }) => ( +
+

Hello {user.username}

+ +
+ )} +
+ ); +} diff --git a/examples/react-native/.env.example b/examples/react-native/.env.example index 6f76171fe62..18ed2c54672 100644 --- a/examples/react-native/.env.example +++ b/examples/react-native/.env.example @@ -1 +1 @@ -EXAMPLE_APP_NAME='DemoExample|BasicExample|ComponentExample|ComponentSlotsExample|FieldsExample|LabelHiddenExample|SlotsExample|StylesExample|InAppMessaging|ThemingExample|DarkModeExample' +EXAMPLE_APP_NAME='DemoExample|BasicExample|ComponentExample|ComponentSlotsExample|FieldsExample|LabelHiddenExample|SlotsExample|StylesExample|InAppMessaging|ThemingExample|DarkModeExample|EmailMfaExample' diff --git a/examples/react-native/ios/Podfile.lock b/examples/react-native/ios/Podfile.lock index 9cc732586ea..29d1b8aa5f5 100644 --- a/examples/react-native/ios/Podfile.lock +++ b/examples/react-native/ios/Podfile.lock @@ -501,8 +501,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - AmplifyRTNCore: 7aabcf40316c2f5c853c1ef73139af3b24a5406b - AmplifyRTNWebBrowser: c80e90e76b89ed768370e1a2d2400fe2fd672bfc + AmplifyRTNCore: 61f4fc669a2284d2cb50e8c3d4563fac1e4505bd + AmplifyRTNWebBrowser: c84ed53a38c31aede2a29ddc78184b3ef33b8549 boost: 7dcd2de282d72e344012f7d6564d024930a6a440 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 FBLazyVector: 9840513ec2766e31fb9e34c2dabb2c4671400fcd @@ -511,29 +511,29 @@ SPEC CHECKSUMS: glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b hermes-engine: 2382506846564caf4152c45390dc24f08fce7057 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 + RCT-Folly: 8dc08ca5a393b48b1c523ab6220dfdcc0fe000ad RCTRequired: 44a3cda52ccac0be738fbf43fef90f3546a48c52 RCTTypeSafety: da7fbf9826fc898ca8b10dc840f2685562039a64 React: defd955b6d6ffb9791bb66dee08d90b87a8e2c0c React-callinvoker: 39ea57213d56ec9c527d51bd0dfb45cbb12ef447 - React-Codegen: 71cbc1bc384f9d19a41e4d00dfd0e7762ec5ef4a - React-Core: 898cb2f7785640e21d381b23fc64a2904628b368 - React-CoreModules: 7f71e7054395d61585048061a66d67b58d3d27b7 - React-cxxreact: 57fca29dd6995de0ee360980709c4be82d40cda1 - React-hermes: 33229fc1867df496665b36b222a82a0f850bcae1 - React-jsi: 3a55652789df6ddd777cce9601bf000e18d6b9df - React-jsiexecutor: 9b2a87f674f30da4706af52520e4860974cec149 + React-Codegen: a383556237715e2f75fb0678a932bc5ad53995a5 + React-Core: d28fd78dc9ba11686213ef1304b876fbe14504b0 + React-CoreModules: f1e28e36e71add156b108ff1dd00cfdb5399da68 + React-cxxreact: e0f18fd5ccd178950aeaca8e5e71bea4c1854f69 + React-hermes: 82799e534d3329c041d7b736ea201e7b95da1112 + React-jsi: 4f0c076e37f6c6b9e1ebf5783918a2b3de3697f7 + React-jsiexecutor: 173c86f9ab3434c9134ade7294f8be06398b4f0a React-jsinspector: b3b341764ccda14f3659c00a9bc5b098b334db2b - React-logger: dc96fadd2f7f6bc38efc3cfb5fef876d4e6290a2 - react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb - react-native-launch-arguments: 8e21f656fb7ade515fd974209b06be1b9279c91e - react-native-netinfo: 1a6035d3b9780221d407c277ebfb5722ace00658 - react-native-safe-area-context: 238cd8b619e05cb904ccad97ef42e84d1b5ae6ec + React-logger: 9fce62c1d7893429ce7558b9f6b83c5c79f946d1 + react-native-get-random-values: 419569b6ed3d15bfb9b6781b2f2e058f8e8d2698 + react-native-launch-arguments: c16edb82a61942e0be7c2542b8a0eae4ee501460 + react-native-netinfo: 98ba850c436e81a5e811abb5055952db52d5d023 + react-native-safe-area-context: 71b3a0d71549684af7f975f12f3bd7039ea54b14 React-perflogger: c944b06edad34f5ecded0f29a6e66290a005d365 React-RCTActionSheet: fa467f37777dacba2c72da4be7ae065da4482d7d React-RCTAnimation: 0591ee5f9e3d8c864a0937edea2165fe968e7099 - React-RCTAppDelegate: 8b7f60103a83ad1670bda690571e73efddba29a0 - React-RCTBlob: 082e8612f48b0ec12ca6dc949fb7c310593eff83 + React-RCTAppDelegate: 04d2661dee11a68f5fd32c4b5d7ffa0dc0721094 + React-RCTBlob: 8f263e84a89652c58899d2444c2a915aa5057feb React-RCTImage: 6300660ef04d0e8a710ad9ea5d2fb4d9521c200d React-RCTLinking: 7703ee1b10d3568c143a489ae74adc419c3f3ef3 React-RCTNetwork: 5748c647e09c66b918f81ae15ed7474327f653be @@ -541,8 +541,8 @@ SPEC CHECKSUMS: React-RCTText: d5e53c0741e3e2df91317781e993b5e42bd70448 React-RCTVibration: 052dd488ba95f461a37c992b9e01bd4bcc59a7b6 React-runtimeexecutor: b5abe02558421897cd9f73d4f4b6adb4bc297083 - ReactCommon: a1a263d94f02a0dc8442f341d5a11b3d7a9cd44d - RNCAsyncStorage: b90b71f45b8b97be43bc4284e71a6af48ac9f547 + ReactCommon: a9f87edf02caaec81cc0873f8261c863db124663 + RNCAsyncStorage: 01062b75ce749e3a18091a9ad7749effdf09ea43 Yoga: e29645ec5a66fb00934fad85338742d1c247d4cb PODFILE CHECKSUM: 86255707601fa0f502375c1c40775dbd535ed624 diff --git a/examples/react-native/src/App/App.tsx b/examples/react-native/src/App/App.tsx index dd718a7a6a6..e0fd298ccdb 100644 --- a/examples/react-native/src/App/App.tsx +++ b/examples/react-native/src/App/App.tsx @@ -36,6 +36,9 @@ const SlotsExample = React.lazy( const StylesExample = React.lazy( () => import('../features/Authenticator/Styles/Example') ); +const EmailMfaExample = React.lazy( + () => import('../features/Authenticator/EmailMfa/Example') +); /** * `InAppMessaging` Example and Demo Apps @@ -91,6 +94,21 @@ const ForgotPassword = React.lazy( const WithAuthenticator = React.lazy( () => import('../ui/components/authenticator/with-authenticator/Example') ); +const SignInWithEmailMfa = React.lazy( + () => import('../ui/components/authenticator/sign-in-with-email-mfa/Example') +); +const SignInWithEmailMfaSelection = React.lazy( + () => + import( + '../ui/components/authenticator/sign-in-with-email-mfa-selection/Example' + ) +); +const SignInWithEmailMfaSetupSelection = React.lazy( + () => + import( + '../ui/components/authenticator/sign-in-with-email-mfa-setup-selection/Example' + ) +); export const ExampleComponent = () => { const appName = getExampleAppName(); @@ -120,6 +138,8 @@ export const ExampleComponent = () => { return ; case 'DarkModeExample': return ; + case 'EmailMfaExample': + return ; // Detox-Cucumber e2e tests // below apps are not meant to be run as example apps, they are part of integration testing in CI @@ -148,6 +168,12 @@ export const ExampleComponent = () => { return ; case 'ui/components/in-app-messaging/demo': return ; + case 'ui/components/authenticator/sign-in-with-email-mfa': + return ; + case 'ui/components/authenticator/sign-in-with-email-mfa-selection': + return ; + case 'ui/components/authenticator/sign-in-with-email-mfa-setup-selection': + return ; default: console.warn( 'EXAMPLE_APP_NAME environment variable not configured correctly, running default example app' diff --git a/examples/react-native/src/features/Authenticator/EmailMfa/Example.tsx b/examples/react-native/src/features/Authenticator/EmailMfa/Example.tsx new file mode 100644 index 00000000000..f5fd8660f8e --- /dev/null +++ b/examples/react-native/src/features/Authenticator/EmailMfa/Example.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Button, StyleSheet, View } from 'react-native'; + +import { Authenticator, useAuthenticator } from '@aws-amplify/ui-react-native'; +import { Amplify } from 'aws-amplify'; + +import awsconfig from './aws-exports'; +Amplify.configure(awsconfig); + +function SignOutButton() { + const { signOut } = useAuthenticator(); + return + + + + + + + + +`; diff --git a/packages/react/src/components/Authenticator/SelectMfaType/index.ts b/packages/react/src/components/Authenticator/SelectMfaType/index.ts new file mode 100644 index 00000000000..e0020dc5388 --- /dev/null +++ b/packages/react/src/components/Authenticator/SelectMfaType/index.ts @@ -0,0 +1 @@ +export { SelectMfaType } from './SelectMfaType'; diff --git a/packages/react/src/components/Authenticator/SetupEmail/SetupEmail.tsx b/packages/react/src/components/Authenticator/SetupEmail/SetupEmail.tsx new file mode 100644 index 00000000000..7ceffbac778 --- /dev/null +++ b/packages/react/src/components/Authenticator/SetupEmail/SetupEmail.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; + +import { authenticatorTextUtil } from '@aws-amplify/ui'; + +import { Flex } from '../../../primitives/Flex'; +import { Heading } from '../../../primitives/Heading'; +import { useAuthenticator } from '@aws-amplify/ui-react-core'; +import { useCustomComponents } from '../hooks/useCustomComponents'; +import { useFormHandlers } from '../hooks/useFormHandlers'; +import { ConfirmSignInFooter } from '../shared/ConfirmSignInFooter'; +import { RemoteErrorMessage } from '../shared/RemoteErrorMessage'; +import { FormFields } from '../shared/FormFields'; +import { RouteContainer, RouteProps } from '../RouteContainer'; + +const { getSetupEmailText } = authenticatorTextUtil; + +export const SetupEmail = ({ + className, + variation, +}: RouteProps): JSX.Element => { + const { isPending } = useAuthenticator((context) => [context.isPending]); + + const { handleChange, handleSubmit } = useFormHandlers(); + + const { + components: { + // @ts-ignore + SetupEmail: { Header = SetupEmail.Header, Footer = SetupEmail.Footer }, + }, + } = useCustomComponents(); + + return ( + +
+ +
+ + + + + + + +