diff --git a/infra-gen2/tool/deploy_gen2.dart b/infra-gen2/tool/deploy_gen2.dart index 15ffcf10f4..e5f8cf1a1e 100644 --- a/infra-gen2/tool/deploy_gen2.dart +++ b/infra-gen2/tool/deploy_gen2.dart @@ -19,11 +19,6 @@ import 'package:path/path.dart' as p; /// 3. Add the backend to a category or create a new category /// 4. Run `dart tool/deploy_gen2.dart` to deploy the backend const List infraConfig = [ - AmplifyBackendGroup( - category: Category.analytics, - defaultOutput: '', - backends: [], - ), AmplifyBackendGroup( category: Category.api, defaultOutput: 'packages/api/amplify_api/example/lib', diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart index 02b5a8e2df..e6a848919e 100644 --- a/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart +++ b/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart @@ -481,45 +481,45 @@ class AmplifyAuthCognitoDart extends AuthPluginInterface } CognitoSignInResult _processSignInResult(SignInState result) { - return switch (result) { - SignInNotStarted _ || - SignInInitiating _ => + switch (result) { + case SignInNotStarted(): + case SignInInitiating(): // This should never happen. throw UnknownException( 'Sign in could not be completed', underlyingException: result, - ), - SignInCancelling _ => throw const UserCancelledException( + ); + + case SignInCancelling(): + throw const UserCancelledException( 'The user canceled the sign-in flow', - ), - SignInChallenge( - :final challengeName, - :final challengeParameters, - :final codeDeliveryDetails, - :final requiredAttributes, - :final allowedMfaTypes, - :final totpSetupResult, - ) => - CognitoSignInResult( + ); + + case final SignInChallenge challenge: + return CognitoSignInResult( isSignedIn: false, nextStep: AuthNextSignInStep( - signInStep: challengeName.signInStep, - codeDeliveryDetails: codeDeliveryDetails, - additionalInfo: challengeParameters, - missingAttributes: requiredAttributes, - allowedMfaTypes: allowedMfaTypes, - totpSetupDetails: totpSetupResult, + signInStep: challenge.challengeName.signInStep, + codeDeliveryDetails: challenge.codeDeliveryDetails, + additionalInfo: challenge.challengeParameters, + missingAttributes: challenge.requiredAttributes, + allowedMfaTypes: challenge.allowedMfaTypes, + totpSetupDetails: challenge.totpSetupResult, ), - ), - SignInSuccess _ => const CognitoSignInResult( + ); + + case SignInSuccess(): + return const CognitoSignInResult( isSignedIn: true, nextStep: AuthNextSignInStep( signInStep: AuthSignInStep.done, ), - ), - SignInFailure(:final exception, :final stackTrace) => - Error.throwWithStackTrace(exception, stackTrace), - }; + ); + + case final SignInFailure failure: + Error.throwWithStackTrace(failure.exception, failure.stackTrace); +// To satisfy Dart's requirements, even if unreachable + } } @override diff --git a/packages/authenticator/amplify_authenticator/lib/amplify_authenticator.dart b/packages/authenticator/amplify_authenticator/lib/amplify_authenticator.dart index bd71bfdf0b..55abb9c220 100644 --- a/packages/authenticator/amplify_authenticator/lib/amplify_authenticator.dart +++ b/packages/authenticator/amplify_authenticator/lib/amplify_authenticator.dart @@ -76,7 +76,9 @@ export 'src/widgets/form.dart' ConfirmSignInMFAForm, ConfirmSignInNewPasswordForm, ContinueSignInWithMfaSelectionForm, + ContinueSignInWithMfaSetupSelectionForm, ContinueSignInWithTotpSetupForm, + ContinueSignInWithEmailMfaSetupForm, ConfirmSignUpForm, ResetPasswordForm, ConfirmResetPasswordForm, @@ -710,9 +712,14 @@ class _AuthenticatorState extends State { confirmSignInMFAForm: ConfirmSignInMFAForm(), continueSignInWithMfaSelectionForm: ContinueSignInWithMfaSelectionForm(), + continueSignInWithMfaSetupSelectionForm: + ContinueSignInWithMfaSetupSelectionForm(), continueSignInWithTotpSetupForm: ContinueSignInWithTotpSetupForm(), + continueSignInWithEmailMfaSetupForm: + ContinueSignInWithEmailMfaSetupForm(), confirmSignInWithTotpMfaCodeForm: ConfirmSignInMFAForm(), + confirmSignInWithEmailMfaCodeForm: ConfirmSignInMFAForm(), verifyUserForm: VerifyUserForm(), confirmVerifyUserForm: ConfirmVerifyUserForm(), child: widget.child, diff --git a/packages/authenticator/amplify_authenticator/lib/src/blocs/auth/auth_bloc.dart b/packages/authenticator/amplify_authenticator/lib/src/blocs/auth/auth_bloc.dart index 1ae97d13b2..353db05de4 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/blocs/auth/auth_bloc.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/blocs/auth/auth_bloc.dart @@ -340,7 +340,7 @@ class StateMachineBloc ), ); case AuthSignInStep.continueSignInWithMfaSetupSelection: - await _handleMfaSetupSelection(result); + _emit(await _handleMfaSetupSelection(result)); case AuthSignInStep.continueSignInWithEmailMfaSetup: _emit(UnauthenticatedState.continueSignInWithEmailMfaSetup); case AuthSignInStep.confirmSignInWithTotpMfaCode: diff --git a/packages/authenticator/amplify_authenticator/lib/src/enums/email_setup_types.dart b/packages/authenticator/amplify_authenticator/lib/src/enums/email_setup_types.dart new file mode 100644 index 0000000000..88252cb4e5 --- /dev/null +++ b/packages/authenticator/amplify_authenticator/lib/src/enums/email_setup_types.dart @@ -0,0 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +enum EmailSetupField { + email, +} diff --git a/packages/authenticator/amplify_authenticator/lib/src/enums/enums.dart b/packages/authenticator/amplify_authenticator/lib/src/enums/enums.dart index f523de2aa8..3a4b577678 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/enums/enums.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/enums/enums.dart @@ -4,6 +4,7 @@ export 'authenticator_step.dart'; export 'confirm_signin_types.dart'; export 'confirm_signup_types.dart'; +export 'email_setup_types.dart'; export 'gender.dart'; export 'reset_password_field.dart'; export 'signin_types.dart'; diff --git a/packages/authenticator/amplify_authenticator/lib/src/keys.dart b/packages/authenticator/amplify_authenticator/lib/src/keys.dart index 49510ea7cf..2ad76b7c01 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/keys.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/keys.dart @@ -52,6 +52,8 @@ const keyCustomChallengeConfirmSignInFormField = Key('customChallengeConfirmSignInFormField'); const keyMfaMethodRadioConfirmSignInFormField = Key('mfaMethodRadioConfirmSignInFormField'); +const keyMfaSetupMethodRadioConfirmSignInFormField = + Key('mfaSetupMethodRadioConfirmSignInFormField'); const keyUsernameConfirmSignInFormField = Key('usernameConfirmSignInFormField'); const keyPasswordConfirmSignInFormField = Key('passwordConfirmSignInFormField'); const keyNewPasswordConfirmSignInFormField = @@ -107,6 +109,10 @@ const keyGoToSignInButton = Key('goToSignInButton'); const keyConfirmSignInButton = Key('confirmSignInButton'); const keyConfirmSignInMfaSelectionButton = Key('confirmSignInMfaSelectionButton'); +const keyConfirmSignInMfaSetupSelectionButton = + Key('confirmSignInMfaSetupSelectionButton'); +const keyConfirmSignInWithEmailMfaSetupButton = + Key('confirmSignInWithEmailMfaSetupButton'); const keyConfirmSignInCustomButton = Key('confirmSignInCustomButton'); const keyLostCodeButton = Key('lostCodeButton'); const keySendCodeButton = Key('sendCodeButton'); @@ -140,3 +146,6 @@ const keyAuthenticatorBanner = Key('authenticatorBanner'); const keyQrCodeTotpSetupFormField = Key('qrCodeTotpSetupFormField'); const keyCopyKeyTotpSetupFormField = Key('copyKeyTotpSetupFormField'); const keyTotpSetupFormField = Key('totpSetupFormField'); + +// Email setup form keys +const keyEmailSetupFormField = Key('emailSetupFormField'); diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/title_localizations.dart b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/title_localizations.dart index 6d69ec3571..852f8e4d7b 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/title_localizations.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/title_localizations.dart @@ -141,6 +141,24 @@ abstract class AuthenticatorTitleLocalizations { /// **'Enter your one-time passcode'** String get confirmSignInWithTotpMfaCode; + /// Title of the Confirm Sign In with Email MFA Code step and form + /// + /// In en, this message translates to: + /// **'Enter your one-time passcode'** + String get confirmSignInWithEmailMfaCode; + + /// Title of the Continue Sign In with Email MFA Setup step and form + /// + /// In en, this message translates to: + /// **'Add Email for Two-Factor Authentication'** + String get continueSignInWithEmailMfaSetup; + + /// Title of the Continue Sign In with MFA Setup Selection step and form + /// + /// In en, this message translates to: + /// **'Choose your preferred two-factor authentication method to set up'** + String get continueSignInWithMfaSetupSelection; + /// Title of the Reset Password step and form /// /// In en, this message translates to: diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/title_localizations_en.dart b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/title_localizations_en.dart index 02fb43da7d..692f579fa5 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/title_localizations_en.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/title_localizations_en.dart @@ -30,6 +30,17 @@ class AuthenticatorTitleLocalizationsEn @override String get confirmSignInWithTotpMfaCode => 'Enter your one-time passcode'; + @override + String get confirmSignInWithEmailMfaCode => 'Enter your one-time passcode'; + + @override + String get continueSignInWithEmailMfaSetup => + 'Add Email for Two-Factor Authentication'; + + @override + String get continueSignInWithMfaSetupSelection => + 'Choose your preferred two-factor authentication method to set up'; + @override String get resetPassword => 'Send Code'; diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/input_resolver.dart b/packages/authenticator/amplify_authenticator/lib/src/l10n/input_resolver.dart index 4b04f86962..0eb96b26f1 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/input_resolver.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/input_resolver.dart @@ -174,7 +174,7 @@ class InputResolverKey { static const selectEmail = InputResolverKey._( InputResolverKeyType.title, - field: InputField.email, + field: InputField.selectEmail, ); static const totpCodePrompt = InputResolverKey._( diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/src/inputs/inputs_en.arb b/packages/authenticator/amplify_authenticator/lib/src/l10n/src/inputs/inputs_en.arb index 90399598d1..e32dd76731 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/src/inputs/inputs_en.arb +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/src/inputs/inputs_en.arb @@ -216,5 +216,9 @@ "totpCodePrompt": "Please enter the code from your registered Authenticator app", "@totpCodePrompt": { "description": "The instructional text for submitting a TOTP pass code" - } + }, + "selectEmail": "Email", + "@selectEmail": { + "description": "Label for the radio button to select email as the user's chosen MFA method." + }, } diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/title_resolver.dart b/packages/authenticator/amplify_authenticator/lib/src/l10n/title_resolver.dart index bbbf34925c..e5c1b19480 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/title_resolver.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/title_resolver.dart @@ -49,6 +49,24 @@ class TitleResolver extends Resolver { .confirmSignInWithTotpMfaCode; } + /// The title for the confirm sign in (email MFA code) Widget. + String confirmSignInWithEmailMfaCode(BuildContext context) { + return AuthenticatorLocalizations.titlesOf(context) + .confirmSignInWithEmailMfaCode; + } + + /// The title for the continue sign in (email MFA setup) Widget. + String continueSignInWithEmailMfaSetup(BuildContext context) { + return AuthenticatorLocalizations.titlesOf(context) + .continueSignInWithEmailMfaSetup; + } + + /// The title for the continue sign in (mfa setup selection) Widget. + String continueSignInWithMfaSetupSelection(BuildContext context) { + return AuthenticatorLocalizations.titlesOf(context) + .continueSignInWithMfaSetupSelection; + } + /// The title for the reset password Widget. String resetPassword(BuildContext context) { return AuthenticatorLocalizations.titlesOf(context).resetPassword; @@ -81,6 +99,12 @@ class TitleResolver extends Resolver { return continueSignInWithTotpSetup(context); case AuthenticatorStep.confirmSignInWithTotpMfaCode: return confirmSignInWithTotpMfaCode(context); + case AuthenticatorStep.confirmSignInWithEmailMfaCode: + return confirmSignInWithEmailMfaCode(context); + case AuthenticatorStep.continueSignInWithEmailMfaSetup: + return continueSignInWithEmailMfaSetup(context); + case AuthenticatorStep.continueSignInWithMfaSetupSelection: + return continueSignInWithMfaSetupSelection(context); case AuthenticatorStep.resetPassword: return resetPassword(context); case AuthenticatorStep.confirmResetPassword: diff --git a/packages/authenticator/amplify_authenticator/lib/src/screens/authenticator_screen.dart b/packages/authenticator/amplify_authenticator/lib/src/screens/authenticator_screen.dart index 181816a0ba..e405a6a812 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/screens/authenticator_screen.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/screens/authenticator_screen.dart @@ -40,6 +40,18 @@ class AuthenticatorScreen extends StatelessAuthenticatorComponent { const AuthenticatorScreen.confirmSignInWithTotpMfaCode({Key? key}) : this(key: key, step: AuthenticatorStep.confirmSignInWithTotpMfaCode); + const AuthenticatorScreen.confirmSignInWithEmailMfaCode({Key? key}) + : this(key: key, step: AuthenticatorStep.confirmSignInWithEmailMfaCode); + + const AuthenticatorScreen.continueSignInWithEmailMfaSetup({Key? key}) + : this(key: key, step: AuthenticatorStep.continueSignInWithEmailMfaSetup); + + const AuthenticatorScreen.continueSignInWithMfaSetupSelection({Key? key}) + : this( + key: key, + step: AuthenticatorStep.continueSignInWithMfaSetupSelection, + ); + const AuthenticatorScreen.resetPassword({Key? key}) : this(key: key, step: AuthenticatorStep.resetPassword); @@ -91,6 +103,9 @@ class AuthenticatorScreen extends StatelessAuthenticatorComponent { case AuthenticatorStep.confirmResetPassword: case AuthenticatorStep.verifyUser: case AuthenticatorStep.confirmVerifyUser: + case AuthenticatorStep.confirmSignInWithEmailMfaCode: + case AuthenticatorStep.continueSignInWithEmailMfaSetup: + case AuthenticatorStep.continueSignInWithMfaSetupSelection: child = _FormWrapperView(step: step); case AuthenticatorStep.loading: throw StateError('Invalid step: $this'); @@ -299,6 +314,9 @@ extension on AuthenticatorStep { case AuthenticatorStep.verifyUser: case AuthenticatorStep.confirmVerifyUser: case AuthenticatorStep.loading: + case AuthenticatorStep.confirmSignInWithEmailMfaCode: + case AuthenticatorStep.continueSignInWithEmailMfaSetup: + case AuthenticatorStep.continueSignInWithMfaSetupSelection: throw StateError('Invalid step: $this'); } } diff --git a/packages/authenticator/amplify_authenticator/lib/src/state/authenticator_state.dart b/packages/authenticator/amplify_authenticator/lib/src/state/authenticator_state.dart index 10bdef835b..836c4f1a01 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/state/authenticator_state.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/state/authenticator_state.dart @@ -160,6 +160,18 @@ class AuthenticatorState extends ChangeNotifier { notifyListeners(); } + /// The value for the email MFA setup form field + /// + /// This value will be used during continue email MFA setup + String get mfaEmail => _mfaEmail; + + set mfaEmail(String value) { + _mfaEmail = value.trim(); + notifyListeners(); + } + + String _mfaEmail = ''; + MfaType? _selectedMfaMethod; TotpSetupDetails? get totpSetupDetails { @@ -395,6 +407,23 @@ class AuthenticatorState extends ChangeNotifier { _setIsBusy(false); } + /// Select MFA setup preference using the values for [selectedMfaMethod] + Future continueSignInWithMfaSetupSelection() async { + if (!_formKey.currentState!.validate()) { + return; + } + + _setIsBusy(true); + + final confirm = AuthConfirmSignInData( + confirmationValue: _selectedMfaMethod!.name, + ); + + _authBloc.add(AuthConfirmSignIn(confirm)); + await nextBlocEvent(); + _setIsBusy(false); + } + /// Complete TOTP setup using the values for [confirmationCode] /// and any user attributes. Future confirmTotp() async { @@ -414,6 +443,40 @@ class AuthenticatorState extends ChangeNotifier { _setIsBusy(false); } + /// complete Email MFA setup using the values for [confirmationCode] + Future confirmEmailMfa() async { + if (!_formKey.currentState!.validate()) { + return; + } + + _setIsBusy(true); + + final confirm = AuthConfirmSignInData( + confirmationValue: _confirmationCode.trim(), + ); + + _authBloc.add(AuthConfirmSignIn(confirm)); + await nextBlocEvent(); + _setIsBusy(false); + } + + /// Complete MFA setup using the values for [confirmationCode] + Future continueEmailMfaSetup() async { + if (!_formKey.currentState!.validate()) { + return; + } + + _setIsBusy(true); + + final confirm = AuthConfirmSignInData( + confirmationValue: _mfaEmail.trim(), + ); + + _authBloc.add(AuthConfirmSignIn(confirm)); + await nextBlocEvent(); + _setIsBusy(false); + } + /// Complete the force password change with [newPassword] Future confirmSignInNewPassword() async { if (!_formKey.currentState!.validate()) { @@ -644,6 +707,7 @@ class AuthenticatorState extends ChangeNotifier { authAttributes.clear(); _publicChallengeParams.clear(); _selectedMfaMethod = null; + _mfaEmail = ''; } void _resetFormKey() { diff --git a/packages/authenticator/amplify_authenticator/lib/src/state/inherited_forms.dart b/packages/authenticator/amplify_authenticator/lib/src/state/inherited_forms.dart index a2a0dfa5ae..2b75184038 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/state/inherited_forms.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/state/inherited_forms.dart @@ -16,8 +16,11 @@ class InheritedForms extends InheritedWidget { required this.confirmResetPasswordForm, required this.confirmSignInNewPasswordForm, required this.continueSignInWithMfaSelectionForm, + required this.continueSignInWithMfaSetupSelectionForm, required this.continueSignInWithTotpSetupForm, + required this.continueSignInWithEmailMfaSetupForm, required this.confirmSignInWithTotpMfaCodeForm, + required this.confirmSignInWithEmailMfaCodeForm, required this.verifyUserForm, required this.confirmVerifyUserForm, required super.child, @@ -30,8 +33,12 @@ class InheritedForms extends InheritedWidget { final ConfirmSignInMFAForm confirmSignInMFAForm; final ConfirmSignInNewPasswordForm confirmSignInNewPasswordForm; final ContinueSignInWithMfaSelectionForm continueSignInWithMfaSelectionForm; + final ContinueSignInWithMfaSetupSelectionForm + continueSignInWithMfaSetupSelectionForm; final ContinueSignInWithTotpSetupForm continueSignInWithTotpSetupForm; + final ContinueSignInWithEmailMfaSetupForm continueSignInWithEmailMfaSetupForm; final ConfirmSignInMFAForm confirmSignInWithTotpMfaCodeForm; + final ConfirmSignInMFAForm confirmSignInWithEmailMfaCodeForm; final ResetPasswordForm resetPasswordForm; final ConfirmResetPasswordForm confirmResetPasswordForm; final VerifyUserForm verifyUserForm; @@ -55,10 +62,16 @@ class InheritedForms extends InheritedWidget { return confirmSignInNewPasswordForm; case AuthenticatorStep.continueSignInWithMfaSelection: return continueSignInWithMfaSelectionForm; + case AuthenticatorStep.continueSignInWithMfaSetupSelection: + return continueSignInWithMfaSetupSelectionForm; case AuthenticatorStep.continueSignInWithTotpSetup: return continueSignInWithTotpSetupForm; case AuthenticatorStep.confirmSignInWithTotpMfaCode: return confirmSignInWithTotpMfaCodeForm; + case AuthenticatorStep.continueSignInWithEmailMfaSetup: + return continueSignInWithEmailMfaSetupForm; + case AuthenticatorStep.confirmSignInWithEmailMfaCode: + return confirmSignInWithEmailMfaCodeForm; case AuthenticatorStep.resetPassword: return resetPasswordForm; case AuthenticatorStep.confirmResetPassword: @@ -104,7 +117,13 @@ class InheritedForms extends InheritedWidget { oldWidget.continueSignInWithTotpSetupForm != continueSignInWithTotpSetupForm || oldWidget.confirmSignInWithTotpMfaCodeForm != - confirmSignInWithTotpMfaCodeForm; + confirmSignInWithTotpMfaCodeForm || + oldWidget.confirmSignInWithEmailMfaCodeForm != + confirmSignInWithEmailMfaCodeForm || + oldWidget.continueSignInWithEmailMfaSetupForm != + continueSignInWithEmailMfaSetupForm || + oldWidget.continueSignInWithMfaSetupSelectionForm != + continueSignInWithMfaSetupSelectionForm; } } diff --git a/packages/authenticator/amplify_authenticator/lib/src/widgets/button.dart b/packages/authenticator/amplify_authenticator/lib/src/widgets/button.dart index c2c92e87bb..ccd43114da 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/widgets/button.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/widgets/button.dart @@ -253,6 +253,50 @@ class ContinueSignInMFASelectionButton extends AuthenticatorElevatedButton { state.continueSignInWithMfaSelection(); } +/// {@category Prebuilt Widgets} +/// {@template amplify_authenticator.continue_sign_in_mfa_setup_selection_button} +/// A prebuilt button for Sign In with MFA setup selection. +/// +/// Uses [ButtonResolverKey.continueLabel] for localization +/// {@endtemplate} +class ContinueSignInMFASetupSelectionButton + extends AuthenticatorElevatedButton { + /// {@macro amplify_authenticator.continue_sign_in_mfa_setup_selection_button} + const ContinueSignInMFASetupSelectionButton({Key? key}) + : super( + key: key ?? keyConfirmSignInMfaSetupSelectionButton, + ); + + @override + ButtonResolverKey get labelKey => ButtonResolverKey.continueLabel; + + @override + void onPressed(BuildContext context, AuthenticatorState state) => + state.continueSignInWithMfaSetupSelection(); +} + +/// {@category Prebuilt Widgets} +/// {@template amplify_authenticator.continue_sign_in_with_email_mfa_setup_button} +/// A prebuilt button for Sign In with Email MFA setup. +/// +/// Uses [ButtonResolverKey.continueLabel] for localization +/// {@endtemplate} +class ContinueSignInWithEmailMfaSetupButton + extends AuthenticatorElevatedButton { + /// {@macro amplify_authenticator.continue_sign_in_with_email_mfa_setup_button} + const ContinueSignInWithEmailMfaSetupButton({Key? key}) + : super( + key: key ?? keyConfirmSignInWithEmailMfaSetupButton, + ); + + @override + ButtonResolverKey get labelKey => ButtonResolverKey.continueLabel; + + @override + void onPressed(BuildContext context, AuthenticatorState state) => + state.continueEmailMfaSetup(); +} + /// {@category Prebuilt Widgets} /// {@template amplify_authenticator.confirm_sign_in_mfa_button} /// A prebuilt button for completing Sign In with and MFA code. diff --git a/packages/authenticator/amplify_authenticator/lib/src/widgets/form.dart b/packages/authenticator/amplify_authenticator/lib/src/widgets/form.dart index 301b61177c..b8a7bd15b1 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/widgets/form.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/widgets/form.dart @@ -544,7 +544,7 @@ class ConfirmSignInCustomAuthForm extends AuthenticatorForm { /// {@category Prebuilt Widgets} /// {@template amplify_authenticator.confirm_sign_in_mfa_form} /// A prebuilt form for completing the sign in process with an MFA code, from -/// either SMS or TOTP. +/// either SMS, TOTP, or Email. /// {@endtemplate} class ConfirmSignInMFAForm extends AuthenticatorForm { /// {@macro amplify_authenticator.confirm_sign_in_mfa_form} @@ -623,6 +623,30 @@ class ContinueSignInWithMfaSelectionForm extends AuthenticatorForm { AuthenticatorFormState(); } +/// {@category Prebuilt Widgets} +/// {@template amplify_authenticator.continue_sign_in_with__mfa_setup_selection_form} +/// A prebuilt form for selecting an MFA method during setup. +/// {@endtemplate} +class ContinueSignInWithMfaSetupSelectionForm extends AuthenticatorForm { + /// {@macro amplify_authenticator.continue_sign_in_with__mfa_setup_selection_form} + ContinueSignInWithMfaSetupSelectionForm({ + super.key, + }) : super._( + fields: [ + ConfirmSignInFormField.mfaSetupSelection(), + ], + actions: const [ + ContinueSignInMFASetupSelectionButton(), + BackToSignInButton(), + ], + ); + + @override + AuthenticatorFormState + createState() => + AuthenticatorFormState(); +} + /// {@category Prebuilt Widgets} /// {@template amplify_authenticator.continue_sign_in_with_totp_setup_form} /// A prebuilt form for completing the totp setup process. @@ -646,6 +670,28 @@ class ContinueSignInWithTotpSetupForm extends AuthenticatorForm { AuthenticatorFormState(); } +/// {@category Prebuilt Widgets} +/// {@template amplify_authenticator.continue_sign_in_with_email_mfa_setup_form} +/// A prebuilt form for completing the email mfa setup process. +/// {@endtemplate} +class ContinueSignInWithEmailMfaSetupForm extends AuthenticatorForm { + ContinueSignInWithEmailMfaSetupForm({ + super.key, + }) : super._( + fields: [ + EmailSetupFormField.email(), + ], + actions: const [ + ContinueSignInWithEmailMfaSetupButton(), + BackToSignInButton(), + ], + ); + + @override + AuthenticatorFormState createState() => + AuthenticatorFormState(); +} + /// {@category Prebuilt Widgets} /// {@template amplify_authenticator.send_code_form} /// A prebuilt form for initiating the reset password flow. diff --git a/packages/authenticator/amplify_authenticator/lib/src/widgets/form_field.dart b/packages/authenticator/amplify_authenticator/lib/src/widgets/form_field.dart index f53a94b831..cc538d7dc8 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/widgets/form_field.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/widgets/form_field.dart @@ -28,7 +28,9 @@ import 'package:qr_flutter/qr_flutter.dart'; part 'form_fields/confirm_sign_in_form_field.dart'; part 'form_fields/confirm_sign_up_form_field.dart'; +part 'form_fields/email_setup_form_field.dart'; part 'form_fields/mfa_selection_form_field.dart'; +part 'form_fields/mfa_setup_selection_form_field.dart'; part 'form_fields/phone_number_field.dart'; part 'form_fields/reset_password_form_field.dart'; part 'form_fields/sign_in_form_field.dart'; @@ -229,11 +231,17 @@ abstract class AuthenticatorFormFieldState< state.confirmSignInCustomAuth(); case AuthenticatorStep.confirmSignInMfa: state.confirmSignInMFA(); + case AuthenticatorStep.confirmSignInWithEmailMfaCode: + state.confirmEmailMfa(); case AuthenticatorStep.confirmSignInNewPassword: state.confirmSignInNewPassword(); case AuthenticatorStep.confirmSignInWithTotpMfaCode: case AuthenticatorStep.continueSignInWithTotpSetup: state.confirmTotp(); + case AuthenticatorStep.continueSignInWithEmailMfaSetup: + state.continueEmailMfaSetup(); + case AuthenticatorStep.continueSignInWithMfaSetupSelection: + state.continueSignInWithMfaSetupSelection(); case AuthenticatorStep.resetPassword: state.resetPassword(); case AuthenticatorStep.confirmResetPassword: diff --git a/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/confirm_sign_in_form_field.dart b/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/confirm_sign_in_form_field.dart index d4b39d1d22..22d1253f39 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/confirm_sign_in_form_field.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/confirm_sign_in_form_field.dart @@ -79,7 +79,7 @@ abstract class ConfirmSignInFormField autofillHints: autofillHints, ); - /// Creates a mfa preference selection component. + /// Creates an mfa preference selection component. static ConfirmSignInFormField mfaSelection({ Key? key, }) => @@ -88,6 +88,15 @@ abstract class ConfirmSignInFormField field: ConfirmSignInField.mfaMethod, ); + /// creates an mfa preference setup selection component. + static ConfirmSignInFormField mfaSetupSelection({ + Key? key, + }) => + _MfaSetupMethodRadioField( + key: key ?? keyMfaSetupMethodRadioConfirmSignInFormField, + field: ConfirmSignInField.mfaMethod, + ); + /// Creates a verification code component. static ConfirmSignInFormField verificationCode({ Key? key, diff --git a/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/email_setup_form_field.dart b/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/email_setup_form_field.dart new file mode 100644 index 0000000000..9884b69122 --- /dev/null +++ b/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/email_setup_form_field.dart @@ -0,0 +1,73 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +part of '../form_field.dart'; + +/// {@category Prebuilt Widgets} +/// {@template amplify_authenticator.email_setup_form_field} +/// A prebuilt form field widget for use on the Email MFA setup step. +/// {@endtemplate} +class EmailSetupFormField + extends AuthenticatorFormField { + /// {@macro amplify_authenticator.email_setup_form_field} + /// + /// Either [titleKey] or [title] is required. + const EmailSetupFormField._({ + super.key, + required super.field, + super.titleKey, + super.hintTextKey, + super.title, + super.hintText, + super.validator, + super.autofillHints, + }) : super._(); + + /// Creates an email FormField for the email setup step. + const EmailSetupFormField.email({ + Key? key, + FormFieldValidator? validator, + Iterable? autofillHints, + }) : this._( + key: key ?? keyEmailSetupFormField, + field: EmailSetupField.email, + titleKey: InputResolverKey.emailTitle, + hintTextKey: InputResolverKey.emailHint, + validator: validator, + autofillHints: autofillHints, + ); + + @override + bool get required => true; + + @override + AuthenticatorFormFieldState + createState() => _EmailSetupFormFieldState(); +} + +class _EmailSetupFormFieldState extends AuthenticatorFormFieldState< + EmailSetupField, String, EmailSetupFormField> with AuthenticatorTextField { + @override + TextInputType get keyboardType { + return TextInputType.emailAddress; + } + + @override + Iterable? get autofillHints { + return [AutofillHints.email]; + } + + @override + ValueChanged get onChanged { + return (v) => state.mfaEmail = v; + } + + @override + FormFieldValidator get validator { + return validateEmail( + isOptional: isOptional, + context: context, + inputResolver: stringResolver.inputs, + ); + } +} diff --git a/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/mfa_selection_form_field.dart b/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/mfa_selection_form_field.dart index f5587071d0..8d73d1aae2 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/mfa_selection_form_field.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/mfa_selection_form_field.dart @@ -41,6 +41,11 @@ class _MfaSelectionFieldState extends _ConfirmSignInFormFieldState label: InputResolverKey.selectSms, value: MfaType.sms, ), + if (_allowedMfaTypes.contains(MfaType.email)) + const InputSelection( + label: InputResolverKey.selectEmail, + value: MfaType.email, + ), ]; @override diff --git a/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/mfa_setup_selection_form_field.dart b/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/mfa_setup_selection_form_field.dart new file mode 100644 index 0000000000..76b4084337 --- /dev/null +++ b/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/mfa_setup_selection_form_field.dart @@ -0,0 +1,63 @@ +// mfa_setup_selection_form_field.dart + +// Copyright Amazon.com, Inc. or its affiliates. +// All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +part of '../form_field.dart'; + +/// {@category Prebuilt Widgets} +/// {@template amplify_authenticator.mfa_setup_selection_form_field} +/// A prebuilt form widget for use on the MFA setup selection step. +/// {@endtemplate} +class _MfaSetupMethodRadioField extends ConfirmSignInFormField { + const _MfaSetupMethodRadioField({ + super.key, + required super.field, + }) : super._(); + + @override + _MfaSetupSelectionFieldState createState() => _MfaSetupSelectionFieldState(); +} + +class _MfaSetupSelectionFieldState extends _ConfirmSignInFormFieldState + with AuthenticatorRadioField { + Set get _allowedMfaTypes { + final state = InheritedAuthBloc.of(context).currentState; + assert( + state is ContinueSignInWithMfaSetupSelection, + 'Expected ContinueSignInWithMfaSetupSelection for current screen', + ); + return (state as ContinueSignInWithMfaSetupSelection).allowedMfaTypes; + } + + @override + List> get selections => [ + if (_allowedMfaTypes.contains(MfaType.totp)) + const InputSelection( + label: InputResolverKey.selectTotp, + value: MfaType.totp, + ), + if (_allowedMfaTypes.contains(MfaType.email)) + const InputSelection( + label: InputResolverKey.selectEmail, + value: MfaType.email, + ), + ]; + + @override + MfaType get initialValue => selections.first.value; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + state.selectedMfaMethod = initialValue; + }); + } + + @override + ValueChanged get onChanged { + return (MfaType method) => state.selectedMfaMethod = method; + } +}