diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c11d22e11a..93a15d4fac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,6 +20,7 @@ Thank you for your interest in contributing to our project! <3 Whether it's a bu - [Unit Tests](#unit-tests) - [Integration Tests](#integration-tests) - [Provision Resources For Integration Tests](#provision-resources-for-integration-tests) + - [Screenshot Tests](#screenshot-tests) - [Code of Conduct](#code-of-conduct) - [Security issue notifications](#security-issue-notifications) - [Licensing](#licensing) @@ -248,6 +249,17 @@ $ tool/provision_integration_test_resources.sh This script can be re-run anytime the environments need to be updated. Further information can be found in the [infra](infra/README.md) package. +## Screenshot Tests + +The Amplify Authenticator package contains a screenshot test suite called `goldens`. If your changes include UI changes within this package please regenerate the goldens. + +To regenerate, navigate to the root of the Authenticator package and run: + +```bash +$ cd packages/authenticator/amplify_authenticator +$ flutter test --update-goldens +``` + ## Code of Conduct This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). diff --git a/packages/api/amplify_api/example/linux/flutter/generated_plugin_registrant.cc b/packages/api/amplify_api/example/linux/flutter/generated_plugin_registrant.cc index 92551e878e..a66c59350a 100644 --- a/packages/api/amplify_api/example/linux/flutter/generated_plugin_registrant.cc +++ b/packages/api/amplify_api/example/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) amplify_db_common_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "AmplifyDbCommonPlugin"); amplify_db_common_plugin_register_with_registrar(amplify_db_common_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/packages/api/amplify_api/example/linux/flutter/generated_plugins.cmake b/packages/api/amplify_api/example/linux/flutter/generated_plugins.cmake index 5fdff50d61..a7fd6afb7c 100644 --- a/packages/api/amplify_api/example/linux/flutter/generated_plugins.cmake +++ b/packages/api/amplify_api/example/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST amplify_db_common + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/packages/api/amplify_api/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/api/amplify_api/example/macos/Flutter/GeneratedPluginRegistrant.swift index 6f6fe42d59..3e3c6f2144 100644 --- a/packages/api/amplify_api/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/packages/api/amplify_api/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -11,6 +11,7 @@ import connectivity_plus import device_info_plus import package_info_plus import path_provider_foundation +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AmplifyAuthCognitoPlugin.register(with: registry.registrar(forPlugin: "AmplifyAuthCognitoPlugin")) @@ -19,4 +20,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/packages/api/amplify_api/example/windows/flutter/generated_plugin_registrant.cc b/packages/api/amplify_api/example/windows/flutter/generated_plugin_registrant.cc index 4eb2a755eb..ae8caf6fe8 100644 --- a/packages/api/amplify_api/example/windows/flutter/generated_plugin_registrant.cc +++ b/packages/api/amplify_api/example/windows/flutter/generated_plugin_registrant.cc @@ -8,10 +8,13 @@ #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { AmplifyDbCommonPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("AmplifyDbCommonPlugin")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/packages/api/amplify_api/example/windows/flutter/generated_plugins.cmake b/packages/api/amplify_api/example/windows/flutter/generated_plugins.cmake index ff805705ce..b9bcdaf9aa 100644 --- a/packages/api/amplify_api/example/windows/flutter/generated_plugins.cmake +++ b/packages/api/amplify_api/example/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST amplify_db_common connectivity_plus + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/packages/auth/amplify_auth_cognito/example/integration_test/mfa_sms_totp_optional_test.dart b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_sms_totp_optional_test.dart index 5d1f0e6e3b..d9f859f516 100644 --- a/packages/auth/amplify_auth_cognito/example/integration_test/mfa_sms_totp_optional_test.dart +++ b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_sms_totp_optional_test.dart @@ -8,7 +8,6 @@ import 'package:checks/checks.dart'; import 'package:flutter_test/flutter_test.dart'; import 'test_runner.dart'; -import 'utils/totp_utils.dart'; void main() { testRunner.setupTests(); diff --git a/packages/auth/amplify_auth_cognito/example/integration_test/mfa_sms_totp_required_test.dart b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_sms_totp_required_test.dart index 7e38387665..6172db226c 100644 --- a/packages/auth/amplify_auth_cognito/example/integration_test/mfa_sms_totp_required_test.dart +++ b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_sms_totp_required_test.dart @@ -9,7 +9,6 @@ import 'package:checks/checks.dart'; import 'package:flutter_test/flutter_test.dart'; import 'test_runner.dart'; -import 'utils/totp_utils.dart'; void main() { testRunner.setupTests(); diff --git a/packages/auth/amplify_auth_cognito/example/integration_test/mfa_totp_optional_test.dart b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_totp_optional_test.dart index cdfb78bab3..5c7e1ba29d 100644 --- a/packages/auth/amplify_auth_cognito/example/integration_test/mfa_totp_optional_test.dart +++ b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_totp_optional_test.dart @@ -8,7 +8,6 @@ import 'package:checks/checks.dart'; import 'package:flutter_test/flutter_test.dart'; import 'test_runner.dart'; -import 'utils/totp_utils.dart'; void main() { testRunner.setupTests(); diff --git a/packages/auth/amplify_auth_cognito/example/integration_test/mfa_totp_required_test.dart b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_totp_required_test.dart index aa84323c18..85594cb7b9 100644 --- a/packages/auth/amplify_auth_cognito/example/integration_test/mfa_totp_required_test.dart +++ b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_totp_required_test.dart @@ -9,7 +9,6 @@ import 'package:checks/checks.dart'; import 'package:flutter_test/flutter_test.dart'; import 'test_runner.dart'; -import 'utils/totp_utils.dart'; void main() { testRunner.setupTests(); diff --git a/packages/auth/amplify_auth_cognito/example/linux/flutter/generated_plugin_registrant.cc b/packages/auth/amplify_auth_cognito/example/linux/flutter/generated_plugin_registrant.cc index 92551e878e..a66c59350a 100644 --- a/packages/auth/amplify_auth_cognito/example/linux/flutter/generated_plugin_registrant.cc +++ b/packages/auth/amplify_auth_cognito/example/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) amplify_db_common_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "AmplifyDbCommonPlugin"); amplify_db_common_plugin_register_with_registrar(amplify_db_common_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/packages/auth/amplify_auth_cognito/example/linux/flutter/generated_plugins.cmake b/packages/auth/amplify_auth_cognito/example/linux/flutter/generated_plugins.cmake index 5fdff50d61..a7fd6afb7c 100644 --- a/packages/auth/amplify_auth_cognito/example/linux/flutter/generated_plugins.cmake +++ b/packages/auth/amplify_auth_cognito/example/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST amplify_db_common + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/packages/auth/amplify_auth_cognito/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/auth/amplify_auth_cognito/example/macos/Flutter/GeneratedPluginRegistrant.swift index 6f6fe42d59..3e3c6f2144 100644 --- a/packages/auth/amplify_auth_cognito/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/packages/auth/amplify_auth_cognito/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -11,6 +11,7 @@ import connectivity_plus import device_info_plus import package_info_plus import path_provider_foundation +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AmplifyAuthCognitoPlugin.register(with: registry.registrar(forPlugin: "AmplifyAuthCognitoPlugin")) @@ -19,4 +20,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/packages/auth/amplify_auth_cognito/example/windows/flutter/generated_plugin_registrant.cc b/packages/auth/amplify_auth_cognito/example/windows/flutter/generated_plugin_registrant.cc index 4eb2a755eb..ae8caf6fe8 100644 --- a/packages/auth/amplify_auth_cognito/example/windows/flutter/generated_plugin_registrant.cc +++ b/packages/auth/amplify_auth_cognito/example/windows/flutter/generated_plugin_registrant.cc @@ -8,10 +8,13 @@ #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { AmplifyDbCommonPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("AmplifyDbCommonPlugin")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/packages/auth/amplify_auth_cognito/example/windows/flutter/generated_plugins.cmake b/packages/auth/amplify_auth_cognito/example/windows/flutter/generated_plugins.cmake index ff805705ce..b9bcdaf9aa 100644 --- a/packages/auth/amplify_auth_cognito/example/windows/flutter/generated_plugins.cmake +++ b/packages/auth/amplify_auth_cognito/example/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST amplify_db_common connectivity_plus + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/packages/authenticator/amplify_authenticator/example/integration_test/main_test.dart b/packages/authenticator/amplify_authenticator/example/integration_test/main_test.dart index 2838a73216..fa1cbc1f8a 100644 --- a/packages/authenticator/amplify_authenticator/example/integration_test/main_test.dart +++ b/packages/authenticator/amplify_authenticator/example/integration_test/main_test.dart @@ -13,7 +13,9 @@ import 'http_test.dart' as http_tests; import 'reset_password_test.dart' as reset_password_tests; import 'sign_in_force_new_password_test.dart' as sign_in_force_new_password_tests; -import 'sign_in_mfa_test.dart' as sign_in_mfa_tests; +import 'sign_in_mfa_sms_test.dart' as sign_in_mfa_sms_tests; +import 'sign_in_mfa_sms_totp_test.dart' as sign_in_mfa_sms_totp_tests; +import 'sign_in_mfa_totp_test.dart' as sign_in_mfa_totp_tests; import 'sign_in_with_email_test.dart' as sign_in_with_email_tests; import 'sign_in_with_phone_test.dart' as sign_in_with_phone_tests; import 'sign_in_with_username_test.dart' as sign_in_with_username_tests; @@ -37,7 +39,9 @@ void main() { http_tests.main(); reset_password_tests.main(); sign_in_force_new_password_tests.main(); - sign_in_mfa_tests.main(); + sign_in_mfa_sms_tests.main(); + sign_in_mfa_sms_totp_tests.main(); + sign_in_mfa_totp_tests.main(); sign_in_with_email_tests.main(); sign_in_with_phone_tests.main(); sign_in_with_username_tests.main(); diff --git a/packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_test.dart b/packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_sms_test.dart similarity index 100% rename from packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_test.dart rename to packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_sms_test.dart diff --git a/packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_sms_totp_test.dart b/packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_sms_totp_test.dart new file mode 100644 index 0000000000..718923d29f --- /dev/null +++ b/packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_sms_totp_test.dart @@ -0,0 +1,228 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_auth_integration_test/amplify_auth_integration_test.dart'; +import 'package:amplify_authenticator_test/amplify_authenticator_test.dart'; +import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:amplify_integration_test/amplify_integration_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_runner.dart'; +import 'utils/test_utils.dart'; + +void main() { + testRunner.setupTests(); + + group('sign-in-sms-totp-mfa', () { + testRunner.withEnvironment(MfaEnvironment.mfaRequiredSmsTotp, () { + // Scenario: Sign in using a totp code when both SMS and TOTP are enabled + testWidgets('can select TOTP MFA', (tester) async { + final username = generateUsername(); + final password = generatePassword(); + final phoneNumber = generateUSPhoneNumber(); + + await adminCreateUser( + username, + password, + autoConfirm: true, + verifyAttributes: false, + attributes: { + AuthUserAttributeKey.phoneNumber: phoneNumber.toE164(), + }, + ); + + await loadAuthenticator(tester: tester); + + expect( + tester.bloc.stream, + emitsInOrder([ + UnauthenticatedState.signIn, + UnauthenticatedState.confirmSignInMfa, + isA(), + UnauthenticatedState.signIn, + isA(), + UnauthenticatedState.confirmSignInWithTotpMfaCode, + isA(), + emitsDone, + ]), + ); + + final signInPage = SignInPage(tester: tester); + final confirmSignInPage = ConfirmSignInPage(tester: tester); + + final smsResult = + await getOtpCode(UserAttribute.phone(phoneNumber.toE164())); + + // When I type my "username" + await signInPage.enterUsername(username); + + // And I type my password + await signInPage.enterPassword(password); + + // And I click the "Sign in" button + await signInPage.submitSignIn(); + + // Then I will be redirected to the confirm sms mfa page + await confirmSignInPage.expectConfirmSignInMFAIsPresent(); + + // When I type a valid confirmation code + await confirmSignInPage.enterVerificationCode(await smsResult.code); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignIn(); + + // Then I see the authenticated app + await signInPage.expectAuthenticated(); + + // When I enable TOTP for MFA instead of the default set up by cognito (SMS) + await setUpTotp(); + + // And I sign out using Auth.signOut() + await Amplify.Auth.signOut(); + await tester.pumpAndSettle(); + + // Then I see the sign in page + signInPage.expectUsername(); + + // When I type my "username" + await signInPage.enterUsername(username); + + // And I type my password + await signInPage.enterPassword(password); + + // And I click the "Sign in" button + await signInPage.submitSignIn(); + + // Then I will be redirected to the MFA selection page + await confirmSignInPage.expectConfirmSignInMfaSelectionIsPresent(); + + // When I select "TOTP" + await confirmSignInPage.selectMfaMethod(mfaMethod: MfaType.totp); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignInMfaSelection(); + + // Then I will be redirected to the TOTP MFA code page + await confirmSignInPage.expectConfirmSignInWithTotpMfaCodeIsPresent(); + + final code_2 = await generateTotpCode(); + + // When I type a valid TOTP code + await confirmSignInPage.enterVerificationCode(code_2); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignIn(); + + // Then I see the authenticated app + await signInPage.expectAuthenticated(); + + await tester.bloc.close(); + }); + + // Scenario: Sign in using a SMS code when both SMS and TOTP are enabled + testWidgets('can select SMS MFA', (tester) async { + final username = generateUsername(); + final password = generatePassword(); + final phoneNumber = generateUSPhoneNumber(); + + await adminCreateUser( + username, + password, + autoConfirm: true, + verifyAttributes: false, + attributes: { + AuthUserAttributeKey.phoneNumber: phoneNumber.toE164(), + }, + ); + + await loadAuthenticator(tester: tester); + + expect( + tester.bloc.stream, + emitsInOrder([ + UnauthenticatedState.signIn, + UnauthenticatedState.confirmSignInMfa, + isA(), + UnauthenticatedState.signIn, + isA(), + UnauthenticatedState.confirmSignInMfa, + isA(), + emitsDone, + ]), + ); + + final signInPage = SignInPage(tester: tester); + final confirmSignInPage = ConfirmSignInPage(tester: tester); + + final smsResult_1 = + await getOtpCode(UserAttribute.phone(phoneNumber.toE164())); + + // When I type my "username" + await signInPage.enterUsername(username); + + // And I type my password + await signInPage.enterPassword(password); + + // And I click the "Sign in" button + await signInPage.submitSignIn(); + + // Then I will be redirected to the confirm sms mfa page + await confirmSignInPage.expectConfirmSignInMFAIsPresent(); + + // When I type a valid confirmation code + await confirmSignInPage.enterVerificationCode(await smsResult_1.code); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignIn(); + + // Then I see the authenticated app + await signInPage.expectAuthenticated(); + + // When I enable TOTP for MFA instead of the default set up by cognito (SMS) + await setUpTotp(); + + // And I sign out using Auth.signOut() + await Amplify.Auth.signOut(); + await tester.pumpAndSettle(); + + // Then I see the sign in page + signInPage.expectUsername(); + + // When I type my "username" + await signInPage.enterUsername(username); + + // And I type my password + await signInPage.enterPassword(password); + + // And I click the "Sign in" button + await signInPage.submitSignIn(); + + // Then I will be redirected to the MFA selection page + await confirmSignInPage.expectConfirmSignInMfaSelectionIsPresent(); + + final smsResult_2 = + await getOtpCode(UserAttribute.phone(phoneNumber.toE164())); + + // When I select "SMS" + await confirmSignInPage.selectMfaMethod(mfaMethod: MfaType.sms); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignInMfaSelection(); + + // Then I will be redirected to the confirm sms mfa page + await confirmSignInPage.expectConfirmSignInMFAIsPresent(); + + // When I type a valid confirmation code + await confirmSignInPage.enterVerificationCode(await smsResult_2.code); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignIn(); + + // Then I see the authenticated app + await signInPage.expectAuthenticated(); + + await tester.bloc.close(); + }); + }); + }); +} diff --git a/packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_totp_test.dart b/packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_totp_test.dart new file mode 100644 index 0000000000..241221ec53 --- /dev/null +++ b/packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_totp_test.dart @@ -0,0 +1,114 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_auth_integration_test/amplify_auth_integration_test.dart'; +import 'package:amplify_authenticator_test/amplify_authenticator_test.dart'; +import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:amplify_integration_test/amplify_integration_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_runner.dart'; +import 'utils/test_utils.dart'; + +void main() { + testRunner.setupTests(); + + group('sign-in-totp-mfa', () { + testRunner.withEnvironment(MfaEnvironment.mfaRequiredTotp, () { + // Scenario: Sign in using a totp code + testWidgets('Setup & Sign in with TOTP MFA', (tester) async { + final username = generateUsername(); + final password = generatePassword(); + late String sharedSecret; + + await adminCreateUser( + username, + password, + autoConfirm: true, + autoFillAttributes: false, + ); + + await loadAuthenticator(tester: tester); + + tester.bloc.stream.listen((event) { + if (event is ContinueSignInTotpSetup) { + sharedSecret = event.totpSetupDetails.sharedSecret; + } + }); + + expect( + tester.bloc.stream, + emitsInOrder([ + UnauthenticatedState.signIn, + isA(), + isA(), + UnauthenticatedState.signIn, + UnauthenticatedState.confirmSignInWithTotpMfaCode, + isA(), + emitsDone, + ]), + ); + + final signInPage = SignInPage(tester: tester); + final confirmSignInPage = ConfirmSignInPage(tester: tester); + + // When I type my "username" + await signInPage.enterUsername(username); + + // And I type my password + await signInPage.enterPassword(password); + + // And I click the "Sign in" button + await signInPage.submitSignIn(); + + // Then I will be redirected to the totp setup page + await confirmSignInPage.expectSignInTotpSetupIsPresent(); + + final code_1 = await generateTotpCode(sharedSecret); + // And I type a valid TOTP code + await confirmSignInPage.enterVerificationCode(code_1); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignIn(); + + // Then I see the authenticated app + await signInPage.expectAuthenticated(); + + /// Sign out and login again with TOTP code + /// validates [AuthenticatorStep.confirmSignInWithTotpMfaCode] + + // When I sign out using Auth.signOut() + await Amplify.Auth.signOut(); + await tester.pumpAndSettle(); + + // Then I see the sign in page + signInPage.expectUsername(); + + // When I type my "username" + await signInPage.enterUsername(username); + + // And I type my password + await signInPage.enterPassword(password); + + // And I click the "Sign in" button + await signInPage.submitSignIn(); + + // Then I will be redirected to the TOTP MFA code page + await confirmSignInPage.expectConfirmSignInWithTotpMfaCodeIsPresent(); + + final code_2 = await generateTotpCode(sharedSecret); + + // When I type a valid TOTP code + await confirmSignInPage.enterVerificationCode(code_2); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignIn(); + + // Then I see the authenticated app + await signInPage.expectAuthenticated(); + + await tester.bloc.close(); + }); + }); + }); +} diff --git a/packages/authenticator/amplify_authenticator/example/linux/flutter/generated_plugin_registrant.cc b/packages/authenticator/amplify_authenticator/example/linux/flutter/generated_plugin_registrant.cc index 92551e878e..a66c59350a 100644 --- a/packages/authenticator/amplify_authenticator/example/linux/flutter/generated_plugin_registrant.cc +++ b/packages/authenticator/amplify_authenticator/example/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) amplify_db_common_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "AmplifyDbCommonPlugin"); amplify_db_common_plugin_register_with_registrar(amplify_db_common_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/packages/authenticator/amplify_authenticator/example/linux/flutter/generated_plugins.cmake b/packages/authenticator/amplify_authenticator/example/linux/flutter/generated_plugins.cmake index 5fdff50d61..a7fd6afb7c 100644 --- a/packages/authenticator/amplify_authenticator/example/linux/flutter/generated_plugins.cmake +++ b/packages/authenticator/amplify_authenticator/example/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST amplify_db_common + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/packages/authenticator/amplify_authenticator/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/authenticator/amplify_authenticator/example/macos/Flutter/GeneratedPluginRegistrant.swift index 6f6fe42d59..3e3c6f2144 100644 --- a/packages/authenticator/amplify_authenticator/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/packages/authenticator/amplify_authenticator/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -11,6 +11,7 @@ import connectivity_plus import device_info_plus import package_info_plus import path_provider_foundation +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AmplifyAuthCognitoPlugin.register(with: registry.registrar(forPlugin: "AmplifyAuthCognitoPlugin")) @@ -19,4 +20,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/packages/authenticator/amplify_authenticator/example/windows/flutter/generated_plugin_registrant.cc b/packages/authenticator/amplify_authenticator/example/windows/flutter/generated_plugin_registrant.cc index 4eb2a755eb..ae8caf6fe8 100644 --- a/packages/authenticator/amplify_authenticator/example/windows/flutter/generated_plugin_registrant.cc +++ b/packages/authenticator/amplify_authenticator/example/windows/flutter/generated_plugin_registrant.cc @@ -8,10 +8,13 @@ #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { AmplifyDbCommonPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("AmplifyDbCommonPlugin")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/packages/authenticator/amplify_authenticator/example/windows/flutter/generated_plugins.cmake b/packages/authenticator/amplify_authenticator/example/windows/flutter/generated_plugins.cmake index ff805705ce..b9bcdaf9aa 100644 --- a/packages/authenticator/amplify_authenticator/example/windows/flutter/generated_plugins.cmake +++ b/packages/authenticator/amplify_authenticator/example/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST amplify_db_common connectivity_plus + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/packages/authenticator/amplify_authenticator/lib/amplify_authenticator.dart b/packages/authenticator/amplify_authenticator/lib/amplify_authenticator.dart index 8b41bbb9bc..1770ee8926 100644 --- a/packages/authenticator/amplify_authenticator/lib/amplify_authenticator.dart +++ b/packages/authenticator/amplify_authenticator/lib/amplify_authenticator.dart @@ -16,6 +16,7 @@ import 'package:amplify_authenticator/src/l10n/auth_strings_resolver.dart'; import 'package:amplify_authenticator/src/l10n/authenticator_localizations.dart'; import 'package:amplify_authenticator/src/models/authenticator_builder.dart'; import 'package:amplify_authenticator/src/models/authenticator_exception.dart'; +import 'package:amplify_authenticator/src/models/totp_options.dart'; import 'package:amplify_authenticator/src/screens/authenticator_screen.dart'; import 'package:amplify_authenticator/src/screens/loading_screen.dart'; import 'package:amplify_authenticator/src/services/amplify_auth_service.dart'; @@ -46,6 +47,7 @@ export 'package:amplify_flutter/amplify_flutter.dart' export 'src/enums/enums.dart' show AuthenticatorStep, Gender; export 'src/l10n/auth_strings_resolver.dart' hide ButtonResolverKeyType; export 'src/models/authenticator_exception.dart'; +export 'src/models/totp_options.dart'; export 'src/models/username_input.dart' show UsernameType, UsernameInput, UsernameSelection; export 'src/state/authenticator_state.dart'; @@ -74,6 +76,8 @@ export 'src/widgets/form.dart' ConfirmSignInCustomAuthForm, ConfirmSignInMFAForm, ConfirmSignInNewPasswordForm, + ContinueSignInWithMfaSelectionForm, + ContinueSignInWithTotpSetupForm, ConfirmSignUpForm, ResetPasswordForm, ConfirmResetPasswordForm, @@ -86,6 +90,7 @@ export 'src/widgets/form_field.dart' ResetPasswordFormField, SignInFormField, SignUpFormField, + TotpSetupFormField, VerifyUserFormField; /// {@template amplify_authenticator.authenticator} @@ -165,7 +170,7 @@ export 'src/widgets/form_field.dart' /// /// ## Customization /// -/// ### Themeing +/// ### Theming /// /// By default, the Authenticator uses your app's Material theme for its styling. /// @@ -250,7 +255,7 @@ export 'src/widgets/form_field.dart' /// {@template amplify_authenticator.custom_builder} /// The authenticator provides prebuilt widgets for each step /// of the authentication flow based on the amplify config for -/// your app. Some customizations can be acheived by providing +/// your app. Some customizations can be achieved by providing /// custom forms (see [signInForm] and [signUpForm]) or through /// theming. To fully customize the authenticator UI, /// you can provide a custom builder method. @@ -309,6 +314,7 @@ class Authenticator extends StatefulWidget { this.authenticatorBuilder, this.padding = const EdgeInsets.all(32), this.dialCodeOptions = const DialCodeOptions(), + this.totpOptions, }) : // ignore: prefer_asserts_with_message assert(() { @@ -351,6 +357,9 @@ class Authenticator extends StatefulWidget { // Padding around each authenticator view final EdgeInsets padding; + /// {@macro amplify_authenticator.totp_options} + final TotpOptions? totpOptions; + /// A method to build a custom UI for the authenticator /// /// {@macro amplify_authenticator.custom_builder} @@ -382,7 +391,7 @@ class Authenticator extends StatefulWidget { /// To fully customize the UI, see authenticatorBuilder final SignUpForm? signUpForm; - /// The form displayed when promted for a password change upon signing in. + /// The form displayed when prompted for a password change upon signing in. /// /// This will be shown to users that are in the state `FORCE_CHANGE_PASSWORD`. /// By default, the form will require the user to enter and confirm a new password. @@ -417,7 +426,7 @@ class Authenticator extends StatefulWidget { /// The initial step that the authenticator will display if the user is not /// already authenticated. /// - /// Defauls to AuthenticatorStep.signIn. Other acceptable values are: + /// Defaults to AuthenticatorStep.signIn. Other acceptable values are: /// AuthenticatorStep.signUp, AuthenticatorStep.resetPassword, and /// AuthenticatorStep.onboarding. /// @@ -475,7 +484,8 @@ class Authenticator extends StatefulWidget { 'dialCodeOptions', dialCodeOptions, ), - ); + ) + ..add(DiagnosticsProperty('totpOptions', totpOptions)); } } @@ -501,6 +511,7 @@ class _AuthenticatorState extends State { authService: _authService, preferPrivateSession: widget.preferPrivateSession, initialStep: widget.initialStep, + totpOptions: widget.totpOptions, )..add(const AuthLoad()); _authenticatorState = AuthenticatorState( _stateMachineBloc, @@ -685,6 +696,11 @@ class _AuthenticatorState extends State { confirmSignUpForm: ConfirmSignUpForm(), confirmSignInCustomAuthForm: ConfirmSignInCustomAuthForm(), confirmSignInMFAForm: ConfirmSignInMFAForm(), + continueSignInWithMfaSelectionForm: + ContinueSignInWithMfaSelectionForm(), + continueSignInWithTotpSetupForm: + ContinueSignInWithTotpSetupForm(), + confirmSignInWithTotpMfaCodeForm: 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 b8fd4de732..13bd54b514 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 @@ -24,6 +24,7 @@ class StateMachineBloc required AuthService authService, required this.preferPrivateSession, this.initialStep = AuthenticatorStep.signIn, + this.totpOptions, }) : _authService = authService { _hubSubscription = _authService.hubEvents.listen(_mapHubEvent); final blocStream = _authEventStream.asyncExpand((event) async* { @@ -39,6 +40,7 @@ class StateMachineBloc final AuthService _authService; final bool preferPrivateSession; final AuthenticatorStep initialStep; + final TotpOptions? totpOptions; @override String get runtimeTypeName => 'StateMachineBloc'; @@ -212,6 +214,21 @@ class StateMachineBloc ); case AuthSignInStep.confirmSignInWithNewPassword: yield UnauthenticatedState.confirmSignInNewPassword; + case AuthSignInStep.confirmSignInWithTotpMfaCode: + yield UnauthenticatedState.confirmSignInWithTotpMfaCode; + case AuthSignInStep.continueSignInWithMfaSelection: + yield ContinueSignInWithMfaSelection( + allowedMfaTypes: result.nextStep.allowedMfaTypes, + ); + case AuthSignInStep.continueSignInWithTotpSetup: + assert( + result.nextStep.totpSetupDetails != null, + 'Sign In Result should have totpSetupDetails', + ); + yield await ContinueSignInTotpSetup.setupURI( + result.nextStep.totpSetupDetails!, + totpOptions, + ); case AuthSignInStep.resetPassword: yield UnauthenticatedState.resetPassword; case AuthSignInStep.confirmSignUp: @@ -245,6 +262,9 @@ class StateMachineBloc } on Exception catch (e) { _exceptionController.add(AuthenticatorException(e)); } + + // Emit empty event to resolve bug with broken event handling on web (possible DDC issue) + yield* const Stream.empty(); } Stream _confirmResetPassword( @@ -296,6 +316,25 @@ class StateMachineBloc ); case AuthSignInStep.confirmSignInWithNewPassword: _emit(UnauthenticatedState.confirmSignInNewPassword); + case AuthSignInStep.continueSignInWithMfaSelection: + _emit( + ContinueSignInWithMfaSelection( + allowedMfaTypes: result.nextStep.allowedMfaTypes, + ), + ); + case AuthSignInStep.continueSignInWithTotpSetup: + assert( + result.nextStep.totpSetupDetails != null, + 'Sign In Result should have totpSetupDetails', + ); + _emit( + await ContinueSignInTotpSetup.setupURI( + result.nextStep.totpSetupDetails!, + totpOptions, + ), + ); + case AuthSignInStep.confirmSignInWithTotpMfaCode: + _emit(UnauthenticatedState.confirmSignInWithTotpMfaCode); case AuthSignInStep.resetPassword: _emit(UnauthenticatedState.confirmResetPassword); case AuthSignInStep.confirmSignUp: diff --git a/packages/authenticator/amplify_authenticator/lib/src/blocs/auth/auth_event.dart b/packages/authenticator/amplify_authenticator/lib/src/blocs/auth/auth_event.dart index d1bfb20d65..444e05fe73 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/blocs/auth/auth_event.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/blocs/auth/auth_event.dart @@ -58,7 +58,7 @@ class AuthUpdatePassword extends AuthEvent { } class AuthConfirmSignIn extends AuthEvent { - const AuthConfirmSignIn(this.data, {required this.rememberDevice}); + const AuthConfirmSignIn(this.data, {this.rememberDevice = false}); final AuthConfirmSignInData data; final bool rememberDevice; } diff --git a/packages/authenticator/amplify_authenticator/lib/src/enums/authenticator_step.dart b/packages/authenticator/amplify_authenticator/lib/src/enums/authenticator_step.dart index 8b2f3d023a..a90f0f8b0a 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/enums/authenticator_step.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/enums/authenticator_step.dart @@ -55,6 +55,24 @@ enum AuthenticatorStep { /// the sign in process. confirmSignInNewPassword, + /// The user is on the Continue Sign In with MFA Selection step. + /// + /// The sign-in is not complete and the user must select and set up + /// an MFA method. + continueSignInWithMfaSelection, + + /// The user is on the Continue Sign In with TOTP setup step. + /// + /// The sign-in is not complete and a TOTP authenticator app must be + /// registered before continuing. + continueSignInWithTotpSetup, + + /// The user is on the Confirm Sign In with TOTP MFA step. + /// + /// The sign-in is not complete and must be confirmed with a TOTP code + /// from a registered authenticator app. + confirmSignInWithTotpMfaCode, + /// The user is on the Reset Password step. resetPassword, diff --git a/packages/authenticator/amplify_authenticator/lib/src/enums/confirm_signin_types.dart b/packages/authenticator/amplify_authenticator/lib/src/enums/confirm_signin_types.dart index 7b86ea7bb6..5e0c4f5e6f 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/enums/confirm_signin_types.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/enums/confirm_signin_types.dart @@ -26,7 +26,8 @@ enum ConfirmSignInField { // updatedAt, // website, custom, - customChallenge + customChallenge, + mfaMethod, } extension ConfirmSignInFieldX on ConfirmSignInField { @@ -35,6 +36,7 @@ extension ConfirmSignInFieldX on ConfirmSignInField { case ConfirmSignInField.code: case ConfirmSignInField.newPassword: case ConfirmSignInField.custom: + case ConfirmSignInField.mfaMethod: throw StateError('Can only be called on attribute types'); default: final key = diff --git a/packages/authenticator/amplify_authenticator/lib/src/enums/enums.dart b/packages/authenticator/amplify_authenticator/lib/src/enums/enums.dart index 6024424701..f523de2aa8 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/enums/enums.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/enums/enums.dart @@ -9,4 +9,5 @@ export 'reset_password_field.dart'; export 'signin_types.dart'; export 'signup_types.dart'; export 'status_type.dart'; +export 'totp_setup_types.dart'; export 'verify_attribute_field_types.dart'; diff --git a/packages/authenticator/amplify_authenticator/lib/src/enums/totp_setup_types.dart b/packages/authenticator/amplify_authenticator/lib/src/enums/totp_setup_types.dart new file mode 100644 index 0000000000..7034c2d6c4 --- /dev/null +++ b/packages/authenticator/amplify_authenticator/lib/src/enums/totp_setup_types.dart @@ -0,0 +1,8 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +enum TotpSetupField { + totpSetup, + totpQrCode, + totpCopyKey, +} diff --git a/packages/authenticator/amplify_authenticator/lib/src/keys.dart b/packages/authenticator/amplify_authenticator/lib/src/keys.dart index d4adcc06b5..49510ea7cf 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/keys.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/keys.dart @@ -50,6 +50,8 @@ const keyCodeConfirmSignUpFormField = Key('codeConfirmSignUpFormField'); const keyCodeConfirmSignInFormField = Key('codeConfirmSignInFormField'); const keyCustomChallengeConfirmSignInFormField = Key('customChallengeConfirmSignInFormField'); +const keyMfaMethodRadioConfirmSignInFormField = + Key('mfaMethodRadioConfirmSignInFormField'); const keyUsernameConfirmSignInFormField = Key('usernameConfirmSignInFormField'); const keyPasswordConfirmSignInFormField = Key('passwordConfirmSignInFormField'); const keyNewPasswordConfirmSignInFormField = @@ -103,6 +105,8 @@ const keyBackToSignInButton = Key('backToSignInButton'); const keyGoToSignUpButton = Key('goToSignUpButton'); const keyGoToSignInButton = Key('goToSignInButton'); const keyConfirmSignInButton = Key('confirmSignInButton'); +const keyConfirmSignInMfaSelectionButton = + Key('confirmSignInMfaSelectionButton'); const keyConfirmSignInCustomButton = Key('confirmSignInCustomButton'); const keyLostCodeButton = Key('lostCodeButton'); const keySendCodeButton = Key('sendCodeButton'); @@ -131,3 +135,8 @@ const keyInheritedAuthBloc = Key('inheritedAuthBloc'); // Banner Keys const keyAuthenticatorBanner = Key('authenticatorBanner'); + +// Totp setup form keys +const keyQrCodeTotpSetupFormField = Key('qrCodeTotpSetupFormField'); +const keyCopyKeyTotpSetupFormField = Key('copyKeyTotpSetupFormField'); +const keyTotpSetupFormField = Key('totpSetupFormField'); diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/auth_strings_resolver.dart b/packages/authenticator/amplify_authenticator/lib/src/l10n/auth_strings_resolver.dart index e97e0617b8..4b25c57226 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/auth_strings_resolver.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/auth_strings_resolver.dart @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import 'package:amplify_authenticator/amplify_authenticator.dart'; +import 'package:amplify_authenticator/src/l10n/instructions_resolver.dart'; import 'package:flutter/material.dart'; export 'button_resolver.dart'; @@ -27,11 +28,13 @@ class AuthStringResolver { InputResolver? inputs, MessageResolver? messages, TitleResolver? titles, + InstructionsResolver? instructions, }) : buttons = buttons ?? const ButtonResolver(), dialCodes = dialCodes ?? countries ?? const DialCodeResolver(), inputs = inputs ?? const InputResolver(), titles = titles ?? const TitleResolver(), - messages = messages ?? const MessageResolver(); + messages = messages ?? const MessageResolver(), + instruction = instructions ?? const InstructionsResolver(); /// The resolver class for shared button Widgets final ButtonResolver buttons; @@ -49,9 +52,12 @@ class AuthStringResolver { /// The resolver class for titles final TitleResolver titles; - /// The resolver class for titles + /// The resolver class for messages final MessageResolver messages; + /// The resolver class for instructions + final InstructionsResolver instruction; + @override bool operator ==(Object other) => other is AuthStringResolver && diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/authenticator_localizations.dart b/packages/authenticator/amplify_authenticator/lib/src/l10n/authenticator_localizations.dart index d46afbf80c..e407bbdd7e 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/authenticator_localizations.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/authenticator_localizations.dart @@ -7,6 +7,8 @@ import 'package:amplify_authenticator/src/l10n/generated/country_localizations.d import 'package:amplify_authenticator/src/l10n/generated/country_localizations_en.dart'; import 'package:amplify_authenticator/src/l10n/generated/input_localizations.dart'; import 'package:amplify_authenticator/src/l10n/generated/input_localizations_en.dart'; +import 'package:amplify_authenticator/src/l10n/generated/instructions_localizations.dart'; +import 'package:amplify_authenticator/src/l10n/generated/instructions_localizations_en.dart'; import 'package:amplify_authenticator/src/l10n/generated/message_localizations.dart'; import 'package:amplify_authenticator/src/l10n/generated/message_localizations_en.dart'; import 'package:amplify_authenticator/src/l10n/generated/title_localizations.dart'; @@ -34,6 +36,8 @@ abstract class AuthenticatorLocalizations { static final _titlesFallback = AuthenticatorTitleLocalizationsEn(); static final _messagesFallback = AuthenticatorMessageLocalizationsEn(); static final _countriesFallback = AuthenticatorCountryLocalizationsEn(); + static final _instructionsFallback = + AuthenticatorInstructionsLocalizationsEn(); /// Retrieves the [AuthenticatorButtonLocalizations] instance, falling back /// to English if unavailable for this locale. @@ -64,4 +68,13 @@ abstract class AuthenticatorLocalizations { static AuthenticatorCountryLocalizations countriesOf(BuildContext context) { return AuthenticatorCountryLocalizations.of(context) ?? _countriesFallback; } + + /// Retrieves the [AuthenticatorInstructionsLocalizations] instance, falling back + /// to English if unavailable for this locale. + static AuthenticatorInstructionsLocalizations instructionsOf( + BuildContext context, + ) { + return AuthenticatorInstructionsLocalizations.of(context) ?? + _instructionsFallback; + } } diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/button_resolver.dart b/packages/authenticator/amplify_authenticator/lib/src/l10n/button_resolver.dart index 0539c8051e..265c7e259d 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/button_resolver.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/button_resolver.dart @@ -11,6 +11,7 @@ enum ButtonResolverKeyType { signIn, signUp, confirm, + continueLabel, submit, changePassword, sendCode, @@ -24,6 +25,7 @@ enum ButtonResolverKeyType { confirmResetPassword, backTo, skip, + copyKey, } class ButtonResolverKey { @@ -44,6 +46,8 @@ class ButtonResolverKey { static const signIn = ButtonResolverKey._(ButtonResolverKeyType.signIn); static const signUp = ButtonResolverKey._(ButtonResolverKeyType.signUp); static const confirm = ButtonResolverKey._(ButtonResolverKeyType.confirm); + static const continueLabel = + ButtonResolverKey._(ButtonResolverKeyType.continueLabel); static const submit = ButtonResolverKey._(ButtonResolverKeyType.submit); static const changePassword = ButtonResolverKey._(ButtonResolverKeyType.changePassword); @@ -60,6 +64,7 @@ class ButtonResolverKey { static const confirmResetPassword = ButtonResolverKey._(ButtonResolverKeyType.confirmResetPassword); static const skip = ButtonResolverKey._(ButtonResolverKeyType.skip); + static const copyKey = ButtonResolverKey._(ButtonResolverKeyType.copyKey); @override String toString() => describeEnum(type); @@ -84,6 +89,11 @@ class ButtonResolver extends Resolver { return AuthenticatorLocalizations.buttonsOf(context).confirm; } + /// Label of confirm forms' button + String continueLabel(BuildContext context) { + return AuthenticatorLocalizations.buttonsOf(context).continueLabel; + } + /// Label of submit button String submit(BuildContext context) { return AuthenticatorLocalizations.buttonsOf(context).submit; @@ -151,6 +161,11 @@ class ButtonResolver extends Resolver { return AuthenticatorLocalizations.buttonsOf(context).skip; } + /// Label of button to copy a value. + String copyKey(BuildContext context) { + return AuthenticatorLocalizations.buttonsOf(context).copyKey; + } + @override String resolve(BuildContext context, ButtonResolverKey key) { switch (key.type) { @@ -160,6 +175,8 @@ class ButtonResolver extends Resolver { return signUp(context); case ButtonResolverKeyType.confirm: return confirm(context); + case ButtonResolverKeyType.continueLabel: + return continueLabel(context); case ButtonResolverKeyType.submit: return submit(context); case ButtonResolverKeyType.changePassword: @@ -186,6 +203,8 @@ class ButtonResolver extends Resolver { return backTo(context, key.previousStep!); case ButtonResolverKeyType.skip: return skip(context); + case ButtonResolverKeyType.copyKey: + return copyKey(context); } } } diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/button_localizations.dart b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/button_localizations.dart index 2cc4eedf9f..ec0e34dca7 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/button_localizations.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/button_localizations.dart @@ -95,7 +95,9 @@ abstract class AuthenticatorButtonLocalizations { ]; /// A list of this localizations delegate's supported locales. - static const List supportedLocales = [Locale('en')]; + static const List supportedLocales = [ + Locale('en'), + ]; /// Label of the button to sign in the user. /// @@ -115,6 +117,12 @@ abstract class AuthenticatorButtonLocalizations { /// **'Confirm'** String get confirm; + /// Label of button to continue to the next action + /// + /// In en, this message translates to: + /// **'Continue'** + String get continueLabel; + /// Label of button to submit a form /// /// In en, this message translates to: @@ -175,6 +183,12 @@ abstract class AuthenticatorButtonLocalizations { /// **'Skip'** String get skip; + /// Label of button to copy a value. + /// + /// In en, this message translates to: + /// **'Copy Key'** + String get copyKey; + /// Label of button to sign out the user /// /// In en, this message translates to: diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/button_localizations_en.dart b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/button_localizations_en.dart index 6a6252e274..fe27a1737c 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/button_localizations_en.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/button_localizations_en.dart @@ -18,6 +18,9 @@ class AuthenticatorButtonLocalizationsEn @override String get confirm => 'Confirm'; + @override + String get continueLabel => 'Continue'; + @override String get submit => 'Submit'; @@ -48,6 +51,9 @@ class AuthenticatorButtonLocalizationsEn @override String get skip => 'Skip'; + @override + String get copyKey => 'Copy Key'; + @override String get signOut => 'Sign Out'; diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/country_localizations.dart b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/country_localizations.dart index 8c480fcf0a..ab765aaa1f 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/country_localizations.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/country_localizations.dart @@ -95,7 +95,9 @@ abstract class AuthenticatorCountryLocalizations { ]; /// A list of this localizations delegate's supported locales. - static const List supportedLocales = [Locale('en')]; + static const List supportedLocales = [ + Locale('en'), + ]; /// Title of select dial code modal /// diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/input_localizations.dart b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/input_localizations.dart index f31b3a6778..178e5b3eeb 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/input_localizations.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/input_localizations.dart @@ -95,7 +95,9 @@ abstract class AuthenticatorInputLocalizations { ]; /// A list of this localizations delegate's supported locales. - static const List supportedLocales = [Locale('en')]; + static const List supportedLocales = [ + Locale('en'), + ]; /// User's chosen username. /// @@ -282,6 +284,24 @@ abstract class AuthenticatorInputLocalizations { /// In en, this message translates to: /// **'Confirmation Code'** String get customChallenge; + + /// Label for the radio button to select SMS as the user's chosen MFA method.. + /// + /// In en, this message translates to: + /// **'Text Message (SMS)'** + String get selectSms; + + /// Label for the radio button to select TOTP as the user's chosen MFA method. + /// + /// In en, this message translates to: + /// **'Authenticator App (TOTP)'** + String get selectTotp; + + /// The instructional text for submitting a TOTP pass code + /// + /// In en, this message translates to: + /// **'Please enter the code from your registered Authenticator app'** + String get totpCodePrompt; } class _AuthenticatorInputLocalizationsDelegate diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/input_localizations_en.dart b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/input_localizations_en.dart index aa0176c644..dcd9c0139c 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/input_localizations_en.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/input_localizations_en.dart @@ -146,4 +146,14 @@ class AuthenticatorInputLocalizationsEn @override String get customChallenge => 'Confirmation Code'; + + @override + String get selectSms => 'Text Message (SMS)'; + + @override + String get selectTotp => 'Authenticator App (TOTP)'; + + @override + String get totpCodePrompt => + 'Please enter the code from your registered Authenticator app'; } diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/instructions_localizations.dart b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/instructions_localizations.dart new file mode 100644 index 0000000000..868ee308cc --- /dev/null +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/instructions_localizations.dart @@ -0,0 +1,173 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:async'; + +import 'package:amplify_authenticator/src/l10n/generated/instructions_localizations_en.dart' + deferred as instructions_localizations_en; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +/// Callers can lookup localized strings with an instance of AuthenticatorInstructionsLocalizations +/// returned by `AuthenticatorInstructionsLocalizations.of(context)`. +/// +/// Applications need to include `AuthenticatorInstructionsLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'generated/instructions_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AuthenticatorInstructionsLocalizations.localizationsDelegates, +/// supportedLocales: AuthenticatorInstructionsLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AuthenticatorInstructionsLocalizations.supportedLocales +/// property. +abstract class AuthenticatorInstructionsLocalizations { + AuthenticatorInstructionsLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale); + + final String localeName; + + static AuthenticatorInstructionsLocalizations? of(BuildContext context) { + return Localizations.of( + context, + AuthenticatorInstructionsLocalizations, + ); + } + + static const LocalizationsDelegate + delegate = _AuthenticatorInstructionsLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en'), + ]; + + /// The header for the first step of TOTP setup + /// + /// In en, this message translates to: + /// **'Step 1: Download an Authenticator app'** + String get totpStep1Title; + + /// The header for the second step of TOTP setup + /// + /// In en, this message translates to: + /// **'Step 2: Enter the QR code'** + String get totpStep2Title; + + /// The header for the third step of TOTP setup + /// + /// In en, this message translates to: + /// **'Step 3: Verify your code'** + String get totpStep3Title; + + /// The instructional text for step one of TOTP setup + /// + /// In en, this message translates to: + /// **'Authenticator apps generate one-time codes that can be used to verify your identity'** + String get totpStep1Body; + + /// The instructional text for step two of TOTP setup + /// + /// In en, this message translates to: + /// **'Open then Authenticator app and scan the QR code or enter the key to get your verification code'** + String get totpStep2Body; + + /// The instructional text for step three of TOTP setup + /// + /// In en, this message translates to: + /// **'Enter the 6 digit code from your Authenticator app'** + String get totpStep3Body; +} + +class _AuthenticatorInstructionsLocalizationsDelegate + extends LocalizationsDelegate { + const _AuthenticatorInstructionsLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return lookupAuthenticatorInstructionsLocalizations(locale); + } + + @override + bool isSupported(Locale locale) => + ['en'].contains(locale.languageCode); + + @override + bool shouldReload(_AuthenticatorInstructionsLocalizationsDelegate old) => + false; +} + +Future + lookupAuthenticatorInstructionsLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return instructions_localizations_en.loadLibrary().then( + (dynamic _) => instructions_localizations_en + .AuthenticatorInstructionsLocalizationsEn(), + ); + } + + throw FlutterError( + 'AuthenticatorInstructionsLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.'); +} diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/instructions_localizations_en.dart b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/instructions_localizations_en.dart new file mode 100644 index 0000000000..9857a2d4b1 --- /dev/null +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/instructions_localizations_en.dart @@ -0,0 +1,31 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_authenticator/src/l10n/generated/instructions_localizations.dart'; + +/// The translations for English (`en`). +class AuthenticatorInstructionsLocalizationsEn + extends AuthenticatorInstructionsLocalizations { + AuthenticatorInstructionsLocalizationsEn([super.locale = 'en']); + + @override + String get totpStep1Title => 'Step 1: Download an Authenticator app'; + + @override + String get totpStep2Title => 'Step 2: Enter the QR code'; + + @override + String get totpStep3Title => 'Step 3: Verify your code'; + + @override + String get totpStep1Body => + 'Authenticator apps generate one-time codes that can be used to verify your identity'; + + @override + String get totpStep2Body => + 'Open then Authenticator app and scan the QR code or enter the key to get your verification code'; + + @override + String get totpStep3Body => + 'Enter the 6 digit code from your Authenticator app'; +} diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/message_localizations.dart b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/message_localizations.dart index 72be246a5b..8a4979709f 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/message_localizations.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/message_localizations.dart @@ -95,7 +95,9 @@ abstract class AuthenticatorMessageLocalizations { ]; /// A list of this localizations delegate's supported locales. - static const List supportedLocales = [Locale('en')]; + static const List supportedLocales = [ + Locale('en'), + ]; /// The message that is displayed after a new confirmation code is sent via Email/SMS. /// @@ -108,6 +110,18 @@ abstract class AuthenticatorMessageLocalizations { /// In en, this message translates to: /// **'A confirmation code has been sent.'** String get codeSentUnknown; + + /// The message that is displayed after a value was copied to the clipboard + /// + /// In en, this message translates to: + /// **'Copied to clipboard!'** + String get copySucceeded; + + /// The message that is displayed after a value failed to copy to the clipboard + /// + /// In en, this message translates to: + /// **'Copy to clipboard failed.'** + String get copyFailed; } class _AuthenticatorMessageLocalizationsDelegate diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/message_localizations_en.dart b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/message_localizations_en.dart index 0d1731dc89..aa14e2f9b2 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/message_localizations_en.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/message_localizations_en.dart @@ -15,4 +15,10 @@ class AuthenticatorMessageLocalizationsEn @override String get codeSentUnknown => 'A confirmation code has been sent.'; + + @override + String get copySucceeded => 'Copied to clipboard!'; + + @override + String get copyFailed => 'Copy to clipboard failed.'; } 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 734537e52f..6d69ec3571 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 @@ -95,7 +95,9 @@ abstract class AuthenticatorTitleLocalizations { ]; /// A list of this localizations delegate's supported locales. - static const List supportedLocales = [Locale('en')]; + static const List supportedLocales = [ + Locale('en'), + ]; /// Title of the Confirm Sign Up step and form /// @@ -121,6 +123,24 @@ abstract class AuthenticatorTitleLocalizations { /// **'Change your password to sign in'** String get confirmSignInNewPassword; + /// Title of the SignIn with MFA selection step and form + /// + /// In en, this message translates to: + /// **'Select your preferred Two-Factor Auth method'** + String get continueSignInWithMfaSelection; + + /// Title of the SignIn with TOTP setup step and form + /// + /// In en, this message translates to: + /// **'Enable Two-Factor Auth'** + String get continueSignInWithTotpSetup; + + /// Title of the Confirm Sign In with Totp MFA Code step and form + /// + /// In en, this message translates to: + /// **'Enter your one-time passcode'** + String get confirmSignInWithTotpMfaCode; + /// 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 b866a93cf2..02fb43da7d 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 @@ -20,6 +20,16 @@ class AuthenticatorTitleLocalizationsEn @override String get confirmSignInNewPassword => 'Change your password to sign in'; + @override + String get continueSignInWithMfaSelection => + 'Select your preferred Two-Factor Auth method'; + + @override + String get continueSignInWithTotpSetup => 'Enable Two-Factor Auth'; + + @override + String get confirmSignInWithTotpMfaCode => 'Enter your one-time passcode'; + @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 e170cfb25f..1641e7acb6 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/input_resolver.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/input_resolver.dart @@ -31,7 +31,10 @@ enum InputField { // updatedAt, // website, rememberDevice, - usernameType + selectSms, + selectTotp, + totpCodePrompt, + usernameType, } enum InputResolverKeyType { @@ -158,6 +161,21 @@ class InputResolverKey { field: InputField.customAuthChallenge, ); + static const selectTotp = InputResolverKey._( + InputResolverKeyType.title, + field: InputField.selectTotp, + ); + + static const selectSms = InputResolverKey._( + InputResolverKeyType.title, + field: InputField.selectSms, + ); + + static const totpCodePrompt = InputResolverKey._( + InputResolverKeyType.title, + field: InputField.totpCodePrompt, + ); + static const verificationCodeTitle = InputResolverKey._( InputResolverKeyType.title, field: InputField.verificationCode, @@ -434,6 +452,12 @@ class InputResolver extends Resolver { // break; case InputField.rememberDevice: return AuthenticatorLocalizations.inputsOf(context).rememberDevice; + case InputField.selectSms: + return AuthenticatorLocalizations.inputsOf(context).selectSms; + case InputField.selectTotp: + return AuthenticatorLocalizations.inputsOf(context).selectTotp; + case InputField.totpCodePrompt: + return AuthenticatorLocalizations.inputsOf(context).totpCodePrompt; case InputField.usernameType: return AuthenticatorLocalizations.inputsOf(context).usernameType; } diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/instructions_resolver.dart b/packages/authenticator/amplify_authenticator/lib/src/l10n/instructions_resolver.dart new file mode 100644 index 0000000000..482872bef9 --- /dev/null +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/instructions_resolver.dart @@ -0,0 +1,67 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_authenticator/src/l10n/authenticator_localizations.dart'; +import 'package:amplify_authenticator/src/l10n/resolver.dart'; +import 'package:flutter/material.dart'; + +enum InstructionsKeyType { + totpStep1Title, + totpStep1Body, + totpStep2Title, + totpStep2Body, + totpStep3Title, + totpStep3Body, +} + +class InstructionsResolver extends Resolver { + const InstructionsResolver(); + + /// The header for the first step of TOTP setup + String totpStep1Title(BuildContext context) { + return AuthenticatorLocalizations.instructionsOf(context).totpStep1Title; + } + + /// The header for the second step of TOTP setup + String totpStep2Title(BuildContext context) { + return AuthenticatorLocalizations.instructionsOf(context).totpStep2Title; + } + + /// The header for the third step of TOTP setup + String totpStep3Title(BuildContext context) { + return AuthenticatorLocalizations.instructionsOf(context).totpStep3Title; + } + + /// The Body text for step first step of TOTP setup + String totpStep1Body(BuildContext context) { + return AuthenticatorLocalizations.instructionsOf(context).totpStep1Body; + } + + /// The Body text for step second step of TOTP setup + String totpStep2Body(BuildContext context) { + return AuthenticatorLocalizations.instructionsOf(context).totpStep2Body; + } + + /// The Body text for step three step of TOTP setup + String totpStep3Body(BuildContext context) { + return AuthenticatorLocalizations.instructionsOf(context).totpStep3Body; + } + + @override + String resolve(BuildContext context, InstructionsKeyType key) { + switch (key) { + case InstructionsKeyType.totpStep1Title: + return totpStep1Title(context); + case InstructionsKeyType.totpStep2Title: + return totpStep2Title(context); + case InstructionsKeyType.totpStep3Title: + return totpStep3Title(context); + case InstructionsKeyType.totpStep1Body: + return totpStep1Body(context); + case InstructionsKeyType.totpStep2Body: + return totpStep2Body(context); + case InstructionsKeyType.totpStep3Body: + return totpStep3Body(context); + } + } +} diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/message_resolver.dart b/packages/authenticator/amplify_authenticator/lib/src/l10n/message_resolver.dart index 5fb611b1c6..429fce4fb6 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/message_resolver.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/message_resolver.dart @@ -7,6 +7,8 @@ import 'package:flutter/material.dart'; enum MessageResolverKeyType { codeSent, + copySucceeded, + copyFailed, } class MessageResolverKey { @@ -37,6 +39,16 @@ class MessageResolver extends Resolver { return AuthenticatorLocalizations.messagesOf(context).codeSentUnknown; } + /// The message that is displayed after a TOTP Key was copied to the clipboard + String copySucceeded(BuildContext context) { + return AuthenticatorLocalizations.messagesOf(context).copySucceeded; + } + + /// The message that is displayed after a TOTP Key failed to copy to the clipboard + String copyFailed(BuildContext context) { + return AuthenticatorLocalizations.messagesOf(context).copyFailed; + } + @override String resolve(BuildContext context, MessageResolverKey key) { switch (key.type) { @@ -46,6 +58,10 @@ class MessageResolver extends Resolver { return codeSent(context, destination); } return codeSentUnknown(context); + case MessageResolverKeyType.copySucceeded: + return copySucceeded(context); + case MessageResolverKeyType.copyFailed: + return copyFailed(context); } } } diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/src/buttons/buttons_en.arb b/packages/authenticator/amplify_authenticator/lib/src/l10n/src/buttons/buttons_en.arb index 17d736f932..08b1c69477 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/src/buttons/buttons_en.arb +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/src/buttons/buttons_en.arb @@ -13,6 +13,10 @@ "@confirm": { "description": "Label of button to confirm an action" }, + "continueLabel": "Continue", + "@continueLabel": { + "description": "Label of button to continue to the next action" + }, "submit": "Submit", "@submit": { "description": "Label of button to submit a form" @@ -53,6 +57,10 @@ "@skip": { "description": "Label of button to skip the current step or action." }, + "copyKey": "Copy Key", + "@copyKey": { + "description": "Label of button to copy a value." + }, "signOut": "Sign Out", "@signOut": { "description": "Label of button to sign out the user" 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 ce72960b5f..90399598d1 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 @@ -204,5 +204,17 @@ "@customChallenge": { "description": "The answer to the custom auth challenge", "example": "123456" + }, + "selectSms": "Text Message (SMS)", + "@selectSms": { + "description": "Label for the radio button to select SMS as the user's chosen MFA method.." + }, + "selectTotp": "Authenticator App (TOTP)", + "@selectTotp": { + "description": "Label for the radio button to select TOTP as the user's chosen MFA method." + }, + "totpCodePrompt": "Please enter the code from your registered Authenticator app", + "@totpCodePrompt": { + "description": "The instructional text for submitting a TOTP pass code" } } diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/src/instructions/instructions_en.arb b/packages/authenticator/amplify_authenticator/lib/src/l10n/src/instructions/instructions_en.arb new file mode 100644 index 0000000000..139e2ec4d3 --- /dev/null +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/src/instructions/instructions_en.arb @@ -0,0 +1,28 @@ +{ + "@@locale": "en", + "@@context": "amplify_authenticator:instructions", + "totpStep1Title": "Step 1: Download an Authenticator app", + "@totpStep1Title": { + "description": "The title for the first step of TOTP setup" + }, + "totpStep2Title": "Step 2: Enter the QR code", + "@totpStep2Title": { + "description": "The title for the second step of TOTP setup" + }, + "totpStep3Title": "Step 3: Verify your code", + "@totpStep3Title": { + "description": "The title for the third step of TOTP setup" + }, + "totpStep1Body": "Authenticator apps generate one-time codes that can be used to verify your identity", + "@totpStep1Body": { + "description": "The body text for step one of TOTP setup" + }, + "totpStep2Body": "Open then Authenticator app and scan the QR code or enter the key to get your verification code", + "@totpStep2Body": { + "description": "The body text for step two of TOTP setup" + }, + "totpStep3Body": "Enter the 6 digit code from your Authenticator app", + "@totpStep3Body": { + "description": "The body text for step three of TOTP setup" + } +} diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/src/messages/messages_en.arb b/packages/authenticator/amplify_authenticator/lib/src/l10n/src/messages/messages_en.arb index 058ff4fa47..e43fd3f8f5 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/src/messages/messages_en.arb +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/src/messages/messages_en.arb @@ -15,5 +15,13 @@ "codeSentUnknown": "A confirmation code has been sent.", "@codeSentUnknown": { "description": "The message that is displayed after a new confirmation code is sent via an unknown delivery medium" + }, + "copySucceeded": "Copied to clipboard!", + "@copySucceeded": { + "description": "The message that is displayed after a value was copied to the clipboard" + }, + "copyFailed": "Copy to clipboard failed.", + "@copyFailed": { + "description": "The message that is displayed after a value failed to copy to the clipboard" } -} \ No newline at end of file +} diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/src/titles/titles_en.arb b/packages/authenticator/amplify_authenticator/lib/src/l10n/src/titles/titles_en.arb index a129842c41..71f8794322 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/src/titles/titles_en.arb +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/src/titles/titles_en.arb @@ -17,6 +17,18 @@ "@confirmSignInNewPassword": { "description": "Title of the Confirm Sign In with New Password step and form" }, + "continueSignInWithMfaSelection": "Select your preferred Two-Factor Auth method", + "@continueSignInWithMfaSelection": { + "description": "Title of the SignIn with MFA selection step and form" + }, + "continueSignInWithTotpSetup": "Enable Two-Factor Auth", + "@continueSignInWithTotpSetup": { + "description": "Title of the SignIn with TOTP setup step and form" + }, + "confirmSignInWithTotpMfaCode": "Enter your one-time passcode", + "@confirmSignInWithTotpMfaCode": { + "description": "Title of the Confirm Sign In with Totp MFA Code step and form" + }, "resetPassword": "Send Code", "@resetPassword": { "description": "Title of the Reset Password step and form" 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 41d14de166..bbbf34925c 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/title_resolver.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/title_resolver.dart @@ -31,6 +31,24 @@ class TitleResolver extends Resolver { .confirmSignInNewPassword; } + /// The title for the continue sign in (mfa selection) Widget. + String continueSignInWithMfaSelection(BuildContext context) { + return AuthenticatorLocalizations.titlesOf(context) + .continueSignInWithMfaSelection; + } + + /// The title for the continue sign in (totp setup) Widget. + String continueSignInWithTotpSetup(BuildContext context) { + return AuthenticatorLocalizations.titlesOf(context) + .continueSignInWithTotpSetup; + } + + /// The title for the confirm sign in (totp MFA code) Widget. + String confirmSignInWithTotpMfaCode(BuildContext context) { + return AuthenticatorLocalizations.titlesOf(context) + .confirmSignInWithTotpMfaCode; + } + /// The title for the reset password Widget. String resetPassword(BuildContext context) { return AuthenticatorLocalizations.titlesOf(context).resetPassword; @@ -57,6 +75,12 @@ class TitleResolver extends Resolver { return confirmSignInMfa(context); case AuthenticatorStep.confirmSignInNewPassword: return confirmSignInNewPassword(context); + case AuthenticatorStep.continueSignInWithMfaSelection: + return continueSignInWithMfaSelection(context); + case AuthenticatorStep.continueSignInWithTotpSetup: + return continueSignInWithTotpSetup(context); + case AuthenticatorStep.confirmSignInWithTotpMfaCode: + return confirmSignInWithTotpMfaCode(context); case AuthenticatorStep.resetPassword: return resetPassword(context); case AuthenticatorStep.confirmResetPassword: diff --git a/packages/authenticator/amplify_authenticator/lib/src/mixins/authenticator_radio_field.dart b/packages/authenticator/amplify_authenticator/lib/src/mixins/authenticator_radio_field.dart index de6ddeace5..e6c04b7284 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/mixins/authenticator_radio_field.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/mixins/authenticator_radio_field.dart @@ -44,7 +44,6 @@ mixin AuthenticatorRadioField 'ConfirmSignInCustom'; } + +class ContinueSignInWithMfaSelection extends UnauthenticatedState { + const ContinueSignInWithMfaSelection({ + Set? allowedMfaTypes, + }) : allowedMfaTypes = allowedMfaTypes ?? const {}, + super(step: AuthenticatorStep.continueSignInWithMfaSelection); + + final Set allowedMfaTypes; + + @override + List get props => [step, allowedMfaTypes]; + + @override + String get runtimeTypeName => 'ContinueSignInWithMfaSelection'; +} + +class ContinueSignInTotpSetup extends UnauthenticatedState { + const ContinueSignInTotpSetup(this.totpSetupDetails, this.totpSetupUri) + : super(step: AuthenticatorStep.continueSignInWithTotpSetup); + + static Future setupURI( + TotpSetupDetails totpSetupDetails, + TotpOptions? totpOptions, + ) async { + final setupUri = totpSetupDetails.getSetupUri( + appName: totpOptions?.issuer ?? + // TODO(equartey): Update this once we have our own method of getting the app name + (await PackageInfo.fromPlatform()).appName, + ); + + return ContinueSignInTotpSetup( + totpSetupDetails, + setupUri, + ); + } + + final TotpSetupDetails totpSetupDetails; + final Uri totpSetupUri; + + @override + List get props => [step, totpSetupDetails]; + + @override + String get runtimeTypeName => 'ContinueSignInTotpSetup'; +} 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 13abac159a..ece3771cc9 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/state/authenticator_state.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/state/authenticator_state.dart @@ -1,11 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import 'package:amplify_auth_cognito/amplify_auth_cognito.dart'; import 'package:amplify_authenticator/amplify_authenticator.dart'; import 'package:amplify_authenticator/src/blocs/auth/auth_bloc.dart'; import 'package:amplify_authenticator/src/blocs/auth/auth_data.dart'; import 'package:amplify_authenticator/src/state/auth_state.dart'; +import 'package:amplify_core/amplify_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -140,6 +140,45 @@ class AuthenticatorState extends ChangeNotifier { String _confirmationCode = ''; + MfaType? get selectedMfaMethod => _selectedMfaMethod; + + /// The value for the MFA selection form field + /// + /// This value will be used during confirm sign up with MFA selection + set selectedMfaMethod(MfaType? value) { + _selectedMfaMethod = value; + notifyListeners(); + } + + MfaType? _selectedMfaMethod; + + TotpSetupDetails? get totpSetupDetails { + final state = _authBloc.currentState; + + if (state is ContinueSignInTotpSetup) { + return state.totpSetupDetails; + } + return null; + } + + Uri? get totpSetupUri { + final state = _authBloc.currentState; + + if (state is ContinueSignInTotpSetup) { + return state.totpSetupUri; + } + return null; + } + + Uri expectTotpUri() { + final totpUri = totpSetupUri; + assert( + totpUri != null, + 'Expected TOTP setup uri in state for current screen, instead got null', + ); + return totpUri!; + } + /// The publicChallengeParameters received from the CreateAuthChallenge lambda during custom auth /// /// This value will be used during the custom auth challenge flow @@ -329,6 +368,42 @@ class AuthenticatorState extends ChangeNotifier { _setIsBusy(false); } + /// Select MFA preference using the values for [selectedMfaMethod] + Future continueSignInWithMfaSelection() 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 { + if (!_formKey.currentState!.validate()) { + return; + } + + _setIsBusy(true); + + final confirm = AuthConfirmSignInData( + confirmationValue: _confirmationCode.trim(), + attributes: authAttributes, + ); + + _authBloc.add(AuthConfirmSignIn(confirm)); + await nextBlocEvent(); + _setIsBusy(false); + } + /// Complete the force password change with [newPassword] Future confirmSignInNewPassword() async { if (!_formKey.currentState!.validate()) { @@ -344,7 +419,7 @@ class AuthenticatorState extends ChangeNotifier { attributes: authAttributes, ); - _authBloc.add(AuthConfirmSignIn(confirm, rememberDevice: rememberDevice)); + _authBloc.add(AuthConfirmSignIn(confirm)); await nextBlocEvent(); _setIsBusy(false); } @@ -558,6 +633,7 @@ class AuthenticatorState extends ChangeNotifier { _newPassword = ''; authAttributes.clear(); _publicChallengeParams.clear(); + _selectedMfaMethod = null; } 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 f3af16439c..a2a0dfa5ae 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/state/inherited_forms.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/state/inherited_forms.dart @@ -15,6 +15,9 @@ class InheritedForms extends InheritedWidget { required this.resetPasswordForm, required this.confirmResetPasswordForm, required this.confirmSignInNewPasswordForm, + required this.continueSignInWithMfaSelectionForm, + required this.continueSignInWithTotpSetupForm, + required this.confirmSignInWithTotpMfaCodeForm, required this.verifyUserForm, required this.confirmVerifyUserForm, required super.child, @@ -26,6 +29,9 @@ class InheritedForms extends InheritedWidget { final ConfirmSignInCustomAuthForm confirmSignInCustomAuthForm; final ConfirmSignInMFAForm confirmSignInMFAForm; final ConfirmSignInNewPasswordForm confirmSignInNewPasswordForm; + final ContinueSignInWithMfaSelectionForm continueSignInWithMfaSelectionForm; + final ContinueSignInWithTotpSetupForm continueSignInWithTotpSetupForm; + final ConfirmSignInMFAForm confirmSignInWithTotpMfaCodeForm; final ResetPasswordForm resetPasswordForm; final ConfirmResetPasswordForm confirmResetPasswordForm; final VerifyUserForm verifyUserForm; @@ -47,6 +53,12 @@ class InheritedForms extends InheritedWidget { return confirmSignInMFAForm; case AuthenticatorStep.confirmSignInNewPassword: return confirmSignInNewPasswordForm; + case AuthenticatorStep.continueSignInWithMfaSelection: + return continueSignInWithMfaSelectionForm; + case AuthenticatorStep.continueSignInWithTotpSetup: + return continueSignInWithTotpSetupForm; + case AuthenticatorStep.confirmSignInWithTotpMfaCode: + return confirmSignInWithTotpMfaCodeForm; case AuthenticatorStep.resetPassword: return resetPasswordForm; case AuthenticatorStep.confirmResetPassword: @@ -86,7 +98,13 @@ class InheritedForms extends InheritedWidget { oldWidget.confirmSignInNewPasswordForm != confirmSignInNewPasswordForm || oldWidget.verifyUserForm != verifyUserForm || - oldWidget.confirmVerifyUserForm != confirmVerifyUserForm; + oldWidget.confirmVerifyUserForm != confirmVerifyUserForm || + oldWidget.continueSignInWithMfaSelectionForm != + continueSignInWithMfaSelectionForm || + oldWidget.continueSignInWithTotpSetupForm != + continueSignInWithTotpSetupForm || + oldWidget.confirmSignInWithTotpMfaCodeForm != + confirmSignInWithTotpMfaCodeForm; } } diff --git a/packages/authenticator/amplify_authenticator/lib/src/widgets/button.dart b/packages/authenticator/amplify_authenticator/lib/src/widgets/button.dart index 8764c728a5..04e7a1b425 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/widgets/button.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/widgets/button.dart @@ -230,6 +230,27 @@ class ConfirmSignInCustomButton extends ConfirmSignInMFAButton { state.confirmSignInCustomAuth(); } +/// {@category Prebuilt Widgets} +/// {@template amplify_authenticator.continue_sign_in_mfa_selection_button} +/// A prebuilt button for Sign In with MFA selection. +/// +/// Uses [ButtonResolverKey.confirm] for localization +/// {@endtemplate} +class ContinueSignInMFASelectionButton extends AuthenticatorElevatedButton { + /// {@macro amplify_authenticator.continue_sign_in_mfa_selection_button} + const ContinueSignInMFASelectionButton({Key? key}) + : super( + key: key ?? keyConfirmSignInMfaSelectionButton, + ); + + @override + ButtonResolverKey get labelKey => ButtonResolverKey.continueLabel; + + @override + void onPressed(BuildContext context, AuthenticatorState state) => + state.continueSignInWithMfaSelection(); +} + /// {@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 609725674d..c91a9033bd 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/widgets/form.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/widgets/form.dart @@ -9,6 +9,7 @@ import 'package:amplify_authenticator/src/mixins/authenticator_username_field.da import 'package:amplify_authenticator/src/state/inherited_authenticator_state.dart'; import 'package:amplify_authenticator/src/state/inherited_config.dart'; import 'package:amplify_authenticator/src/utils/list.dart'; +import 'package:amplify_authenticator/src/widgets/button.dart'; import 'package:amplify_authenticator/src/widgets/component.dart'; import 'package:amplify_authenticator/src/widgets/form_field.dart'; import 'package:amplify_authenticator/src/widgets/social/social_button.dart'; @@ -544,7 +545,8 @@ 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. +/// A prebuilt form for completing the sign in process with an MFA code, from +/// either SMS or TOTP. /// {@endtemplate} class ConfirmSignInMFAForm extends AuthenticatorForm { /// {@macro amplify_authenticator.confirm_sign_in_mfa_form} @@ -601,6 +603,51 @@ class ConfirmSignInNewPasswordForm extends AuthenticatorForm { AuthenticatorFormState(); } +/// {@category Prebuilt Widgets} +/// {@template amplify_authenticator.continue_sign_in_with_mfa_selection_form} +/// A prebuilt form for selecting MFA preference. +/// {@endtemplate} +class ContinueSignInWithMfaSelectionForm extends AuthenticatorForm { + /// {@macro amplify_authenticator.continue_sign_in_with_mfa_selection_form} + ContinueSignInWithMfaSelectionForm({super.key}) + : super._( + fields: [ + ConfirmSignInFormField.mfaSelection(), + ], + actions: const [ + ContinueSignInMFASelectionButton(), + 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. +/// {@endtemplate} +class ContinueSignInWithTotpSetupForm extends AuthenticatorForm { + /// {@macro amplify_authenticator.continue_sign_in_with_totp_setup_form} + ContinueSignInWithTotpSetupForm({ + super.key, + }) : super._( + fields: [ + TotpSetupFormField.totpSetup(), + ], + actions: const [ + ConfirmSignInMFAButton(), + 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 c2a5c7fc32..25f387bea3 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/widgets/form_field.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/widgets/form_field.dart @@ -3,11 +3,11 @@ library authenticator.form_field; -import 'package:amplify_auth_cognito/amplify_auth_cognito.dart'; import 'package:amplify_authenticator/amplify_authenticator.dart'; import 'package:amplify_authenticator/src/constants/authenticator_constants.dart'; import 'package:amplify_authenticator/src/enums/enums.dart'; import 'package:amplify_authenticator/src/keys.dart'; +import 'package:amplify_authenticator/src/l10n/instructions_resolver.dart'; import 'package:amplify_authenticator/src/mixins/authenticator_date_field.dart'; import 'package:amplify_authenticator/src/mixins/authenticator_phone_field.dart'; import 'package:amplify_authenticator/src/mixins/authenticator_radio_field.dart'; @@ -24,13 +24,17 @@ import 'package:amplify_authenticator/src/widgets/form.dart'; import 'package:amplify_flutter/amplify_flutter.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +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/mfa_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'; part 'form_fields/sign_up_form_field.dart'; +part 'form_fields/totp_setup_form_field.dart'; part 'form_fields/verify_user_form_field.dart'; /// {@template amplify_authenticator.authenticator_form_field} @@ -41,6 +45,7 @@ part 'form_fields/verify_user_form_field.dart'; /// - [SignUpFormField] /// - [ConfirmSignInFormField] /// - [ConfirmSignUpFormField] +/// - [TotpSetupFormField] /// - [VerifyUserFormField] /// {@endtemplate} abstract class AuthenticatorFormField autofillHints: autofillHints, ); + /// Creates a mfa preference selection component. + static ConfirmSignInFormField mfaSelection({ + Key? key, + }) => + _MfaMethodRadioField( + key: key ?? keyMfaMethodRadioConfirmSignInFormField, + field: ConfirmSignInField.mfaMethod, + ); + /// Creates a verification code component. static ConfirmSignInFormField verificationCode({ Key? key, @@ -314,6 +323,7 @@ abstract class ConfirmSignInFormField return 99; case ConfirmSignInField.code: case ConfirmSignInField.customChallenge: + case ConfirmSignInField.mfaMethod: return 10; case ConfirmSignInField.address: case ConfirmSignInField.birthdate: @@ -338,6 +348,7 @@ abstract class ConfirmSignInFormField case ConfirmSignInField.customChallenge: case ConfirmSignInField.newPassword: case ConfirmSignInField.confirmNewPassword: + case ConfirmSignInField.mfaMethod: return true; case ConfirmSignInField.address: case ConfirmSignInField.birthdate: @@ -359,6 +370,23 @@ abstract class ConfirmSignInFormField abstract class _ConfirmSignInFormFieldState extends AuthenticatorFormFieldState> { + @override + Widget? get surlabel { + switch (widget.field) { + case ConfirmSignInField.code + when state.currentStep == + AuthenticatorStep.confirmSignInWithTotpMfaCode: + return Text( + InputResolverKey.totpCodePrompt.resolve( + context, + stringResolver.inputs, + ), + ); + default: + return null; + } + } + @override bool get obscureText { switch (widget.field) { @@ -471,6 +499,7 @@ abstract class _ConfirmSignInFormFieldState ]; case ConfirmSignInField.custom: case ConfirmSignInField.customChallenge: + case ConfirmSignInField.mfaMethod: return null; } } 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 new file mode 100644 index 0000000000..c7c18fb5f7 --- /dev/null +++ b/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/mfa_selection_form_field.dart @@ -0,0 +1,61 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +part of authenticator.form_field; + +/// {@category Prebuilt Widgets} +/// {@template amplify_authenticator.mfa_selection_form_field} +/// A prebuilt form widget for use on the MFA selection step. +/// {@endtemplate} + +class _MfaMethodRadioField extends ConfirmSignInFormField { + const _MfaMethodRadioField({ + super.key, + required super.field, + }) : super._(); + + @override + _MfaSelectionFieldState createState() => _MfaSelectionFieldState(); +} + +class _MfaSelectionFieldState extends _ConfirmSignInFormFieldState + with AuthenticatorRadioField { + Set get _allowedMfaTypes { + final state = InheritedAuthBloc.of(context).currentState; + assert( + state is ContinueSignInWithMfaSelection, + 'Expected ContinueSignInWithMfaSelection for current screen', + ); + return (state as ContinueSignInWithMfaSelection).allowedMfaTypes; + } + + @override + List> get selections => [ + if (_allowedMfaTypes.contains(MfaType.totp)) + const InputSelection( + label: InputResolverKey.selectTotp, + value: MfaType.totp, + ), + if (_allowedMfaTypes.contains(MfaType.sms)) + const InputSelection( + label: InputResolverKey.selectSms, + value: MfaType.sms, + ), + ]; + + @override + MfaType get initialValue => selections.first.value; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + state.selectedMfaMethod = initialValue; + }); + } + + @override + ValueChanged get onChanged { + return (MfaType key) => state.selectedMfaMethod = key; + } +} diff --git a/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/totp_setup_form_field.dart b/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/totp_setup_form_field.dart new file mode 100644 index 0000000000..3faa90a16c --- /dev/null +++ b/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/totp_setup_form_field.dart @@ -0,0 +1,219 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// ignore_for_file: use_build_context_synchronously + +part of authenticator.form_field; + +/// {@category Prebuilt Widgets} +/// {@template amplify_authenticator.instruction_form_field} +/// Prebuilt form field widgets for setting up TOTP. +/// +/// +/// {@endtemplate} +abstract class TotpSetupFormField + extends AuthenticatorFormField { + /// {@macro amplify_authenticator.instruction_form_field} + /// + /// Either [titleKey] or [title] is required. + const TotpSetupFormField._({ + super.key, + required super.field, + }) : super._(); + + /// Creates a TOTP QR code component. + static TotpSetupFormField totpQrCode({ + Key? key, + }) => + _InstructionTotpQrCodeField( + key: key ?? keyQrCodeTotpSetupFormField, + field: TotpSetupField.totpQrCode, + ); + + /// Creates a TOTP Copy Key component. + static TotpSetupFormField totpCopyKey({ + Key? key, + }) => + _InstructionTotpCopyKeyField( + key: key ?? keyCopyKeyTotpSetupFormField, + field: TotpSetupField.totpCopyKey, + ); + + /// Creates a TOTP setup component. + static TotpSetupFormField totpSetup({ + Key? key, + }) => + _TotpSetupField( + key: key ?? keyTotpSetupFormField, + field: TotpSetupField.totpSetup, + ); +} + +abstract class _InstructionFormFieldState + extends AuthenticatorFormFieldState> {} + +class _TotpSetupField extends TotpSetupFormField { + const _TotpSetupField({ + super.key, + required super.field, + }) : super._(); + + @override + _InstructionTotpSetupFieldState createState() => + _InstructionTotpSetupFieldState(); +} + +class _InstructionTotpSetupFieldState + extends _InstructionFormFieldState { + static const _spacingBox = SizedBox(height: 10); + + String resolveInstruction(InstructionsKeyType type) => + stringResolver.instruction.resolve( + context, + type, + ); + + @override + Widget buildFormField(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + resolveInstruction(InstructionsKeyType.totpStep1Title), + style: Theme.of(context).textTheme.titleSmall, + ), + Text( + resolveInstruction(InstructionsKeyType.totpStep1Body), + ), + _spacingBox, + Text( + resolveInstruction(InstructionsKeyType.totpStep2Title), + style: Theme.of(context).textTheme.titleSmall, + ), + Text( + resolveInstruction(InstructionsKeyType.totpStep2Body), + ), + TotpSetupFormField.totpQrCode(), + TotpSetupFormField.totpCopyKey(), + Text( + resolveInstruction(InstructionsKeyType.totpStep3Title), + style: Theme.of(context).textTheme.titleSmall, + ), + Text( + resolveInstruction(InstructionsKeyType.totpStep3Body), + ), + ConfirmSignInFormField.verificationCode(), + ], + ); + } +} + +class _InstructionTotpCopyKeyField extends TotpSetupFormField { + const _InstructionTotpCopyKeyField({ + super.key, + required super.field, + }) : super._(); + + @override + _InstructionTotpCopyKeyFieldState createState() => + _InstructionTotpCopyKeyFieldState(); +} + +class _InstructionTotpCopyKeyFieldState + extends _InstructionFormFieldState { + /// Copies the TOTP key to the clipboard + Future _copyKey() async { + try { + await Clipboard.setData( + ClipboardData(text: state.totpSetupDetails!.sharedSecret), + ); + // There is a bug in the analysis that causes this line to fail linting + // This check resolves lint error in beta, thus the lint error can be + // ignored using use_build_context_synchronously + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: Theme.of(context).colorScheme.primary, + content: Text( + stringResolver.messages.copySucceeded( + context, + ), + textAlign: TextAlign.center, + ), + ), + ); + } on Exception { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: Theme.of(context).colorScheme.onError, + content: Text( + stringResolver.messages.copyFailed( + context, + ), + textAlign: TextAlign.center, + ), + ), + ); + } + } + + @override + Widget buildFormField(BuildContext context) { + return Center( + child: OutlinedButton( + onPressed: _copyKey, + child: Text( + stringResolver.buttons.resolve( + context, + ButtonResolverKey.copyKey, + ), + ), + ), + ); + } +} + +class _InstructionTotpQrCodeField extends TotpSetupFormField { + const _InstructionTotpQrCodeField({ + super.key, + required super.field, + }) : super._(); + + @override + _InstructionTotpQrCodeFieldState createState() => + _InstructionTotpQrCodeFieldState(); +} + +class _InstructionTotpQrCodeFieldState + extends _InstructionFormFieldState { + /// Retrieves the TOTP setup uri from the state + Uri get _totpUri => state.expectTotpUri(); + + @override + double? get marginBottom => 0; + + @override + Widget buildFormField(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: QrImageView( + size: 200, + padding: EdgeInsets.zero, + eyeStyle: QrEyeStyle( + color: Theme.of(context).textTheme.bodyMedium!.color, + ), + dataModuleStyle: QrDataModuleStyle( + color: Theme.of(context).textTheme.bodyMedium!.color, + ), + data: _totpUri.toString(), + version: QrVersions.auto, + ), + ), + ], + ); + } +} diff --git a/packages/authenticator/amplify_authenticator/pubspec.yaml b/packages/authenticator/amplify_authenticator/pubspec.yaml index 908ded3de7..9b90cbdefe 100644 --- a/packages/authenticator/amplify_authenticator/pubspec.yaml +++ b/packages/authenticator/amplify_authenticator/pubspec.yaml @@ -22,8 +22,12 @@ dependencies: sdk: flutter intl: ">=0.18.0 <1.0.0" meta: ^1.7.0 + # TODO(equartey): Remove this once we have our own method of getting the app name + package_info_plus: ^4.0.2 + qr_flutter: 4.1.0 smithy: ">=0.5.1 <0.6.0" stream_transform: ^2.0.0 + url_launcher: ^6.1.11 dev_dependencies: amplify_authenticator_test: diff --git a/packages/authenticator/amplify_authenticator/tool/generate_l10n.sh b/packages/authenticator/amplify_authenticator/tool/generate_l10n.sh index d8e3c180aa..a358fb0687 100755 --- a/packages/authenticator/amplify_authenticator/tool/generate_l10n.sh +++ b/packages/authenticator/amplify_authenticator/tool/generate_l10n.sh @@ -12,10 +12,10 @@ dart run ./tool/generate_country_localization.dart COUNTRY_OUTPUT_FILES=('lib/src/utils/country_code.dart' 'lib/src/l10n/country_resolver.dart') OUTPUT_DIR=lib/src/l10n/generated -TEMPLATES=('titles_en.arb' 'buttons_en.arb' 'inputs_en.arb' 'countries_en.arb' 'messages_en.arb') -ARB_DIRS=('lib/src/l10n/src/titles' 'lib/src/l10n/src/buttons' 'lib/src/l10n/src/inputs' 'lib/src/l10n/src/countries' 'lib/src/l10n/src/messages') -OUTPUT_CLASSES=('AuthenticatorTitleLocalizations' 'AuthenticatorButtonLocalizations' 'AuthenticatorInputLocalizations' 'AuthenticatorCountryLocalizations' 'AuthenticatorMessageLocalizations') -OUTPUT_FILES=('title_localizations.dart' 'button_localizations.dart' 'input_localizations.dart' 'country_localizations.dart' 'message_localizations.dart') +TEMPLATES=('titles_en.arb' 'buttons_en.arb' 'inputs_en.arb' 'countries_en.arb' 'messages_en.arb' 'instructions_en.arb') +ARB_DIRS=('lib/src/l10n/src/titles' 'lib/src/l10n/src/buttons' 'lib/src/l10n/src/inputs' 'lib/src/l10n/src/countries' 'lib/src/l10n/src/messages' 'lib/src/l10n/src/instructions') +OUTPUT_CLASSES=('AuthenticatorTitleLocalizations' 'AuthenticatorButtonLocalizations' 'AuthenticatorInputLocalizations' 'AuthenticatorCountryLocalizations' 'AuthenticatorMessageLocalizations' 'AuthenticatorInstructionsLocalizations') +OUTPUT_FILES=('title_localizations.dart' 'button_localizations.dart' 'input_localizations.dart' 'country_localizations.dart' 'message_localizations.dart' 'instructions_localizations.dart') for i in "${!TEMPLATES[@]}"; do ARB_DIR=${ARB_DIRS[i]} diff --git a/packages/authenticator/amplify_authenticator_test/lib/src/pages/confirm_sign_in_page.dart b/packages/authenticator/amplify_authenticator_test/lib/src/pages/confirm_sign_in_page.dart index 5492ff04c6..84c8f8cbfb 100644 --- a/packages/authenticator/amplify_authenticator_test/lib/src/pages/confirm_sign_in_page.dart +++ b/packages/authenticator/amplify_authenticator_test/lib/src/pages/confirm_sign_in_page.dart @@ -7,6 +7,7 @@ import 'package:amplify_authenticator/src/keys.dart'; // ignore: implementation_imports import 'package:amplify_authenticator/src/screens/authenticator_screen.dart'; import 'package:amplify_authenticator_test/src/pages/authenticator_page.dart'; +import 'package:amplify_flutter/amplify_flutter.dart'; import 'package:flutter_test/flutter_test.dart'; /// Confirm Sign In Page Object @@ -22,6 +23,10 @@ class ConfirmSignInPage extends AuthenticatorPage { find.byKey(keyConfirmNewPasswordConfirmSignInFormField); Finder get verificationField => find.byKey(keyCodeConfirmSignInFormField); Finder get confirmSignInButton => find.byKey(keyConfirmSignInButton); + Finder get confirmSignInMfaSelectionButton => + find.byKey(keyConfirmSignInMfaSelectionButton); + Finder get selectMfaRadio => + find.byKey(keyMfaMethodRadioConfirmSignInFormField); Finder get backToSignIn => find.byKey(keyBackToSignInButton); /// Then I see "Confirm Sign In - New Password" @@ -43,6 +48,39 @@ class ConfirmSignInPage extends AuthenticatorPage { expect(currentScreen.step, equals(AuthenticatorStep.confirmSignInMfa)); } + /// Then I see "Select your preferred MFA Method" + Future expectConfirmSignInMfaSelectionIsPresent() async { + final currentScreen = tester.widget( + find.byType(AuthenticatorScreen), + ); + expect( + currentScreen.step, + equals(AuthenticatorStep.continueSignInWithMfaSelection), + ); + } + + /// Then I see "Setup an Authentication App" + Future expectSignInTotpSetupIsPresent() async { + final currentScreen = tester.widget( + find.byType(AuthenticatorScreen), + ); + expect( + currentScreen.step, + equals(AuthenticatorStep.continueSignInWithTotpSetup), + ); + } + + /// Then I see "Enter your Authentication code" + Future expectConfirmSignInWithTotpMfaCodeIsPresent() async { + final currentScreen = tester.widget( + find.byType(AuthenticatorScreen), + ); + expect( + currentScreen.step, + equals(AuthenticatorStep.confirmSignInWithTotpMfaCode), + ); + } + /// Then I see "New Password" void expectNewPasswordIsPresent() { expect(newPasswordField, findsOneWidget); @@ -79,6 +117,28 @@ class ConfirmSignInPage extends AuthenticatorPage { await tester.pumpAndSettle(); } + /// When I select a MFA method + Future selectMfaMethod({ + required MfaType mfaMethod, + }) async { + expect(selectMfaRadio, findsOneWidget); + + final mfaMethodWidget = find.descendant( + of: selectMfaRadio, + matching: find.textContaining('(${mfaMethod.name.toUpperCase()})'), + ); + + await tester.tap(mfaMethodWidget); + await tester.pumpAndSettle(); + } + + /// When I click the "Confirm Sign In" button + Future submitConfirmSignInMfaSelection() async { + await tester.ensureVisible(confirmSignInMfaSelectionButton); + await tester.tap(confirmSignInMfaSelectionButton); + await tester.pumpAndSettle(); + } + /// When I navigate to the "Sign In" step. Future navigateToSignIn() async { await tester.tap(backToSignIn); diff --git a/packages/storage/amplify_storage_s3/example/linux/flutter/generated_plugin_registrant.cc b/packages/storage/amplify_storage_s3/example/linux/flutter/generated_plugin_registrant.cc index 92551e878e..a66c59350a 100644 --- a/packages/storage/amplify_storage_s3/example/linux/flutter/generated_plugin_registrant.cc +++ b/packages/storage/amplify_storage_s3/example/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) amplify_db_common_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "AmplifyDbCommonPlugin"); amplify_db_common_plugin_register_with_registrar(amplify_db_common_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/packages/storage/amplify_storage_s3/example/linux/flutter/generated_plugins.cmake b/packages/storage/amplify_storage_s3/example/linux/flutter/generated_plugins.cmake index 5fdff50d61..a7fd6afb7c 100644 --- a/packages/storage/amplify_storage_s3/example/linux/flutter/generated_plugins.cmake +++ b/packages/storage/amplify_storage_s3/example/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST amplify_db_common + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/packages/storage/amplify_storage_s3/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/storage/amplify_storage_s3/example/macos/Flutter/GeneratedPluginRegistrant.swift index 26276f0fec..32a9505309 100644 --- a/packages/storage/amplify_storage_s3/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/packages/storage/amplify_storage_s3/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,6 +10,7 @@ import amplify_secure_storage import device_info_plus import package_info_plus import path_provider_foundation +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AmplifyAuthCognitoPlugin.register(with: registry.registrar(forPlugin: "AmplifyAuthCognitoPlugin")) @@ -17,4 +18,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/packages/storage/amplify_storage_s3/example/windows/flutter/generated_plugin_registrant.cc b/packages/storage/amplify_storage_s3/example/windows/flutter/generated_plugin_registrant.cc index c95b7c976b..e24dd12e40 100644 --- a/packages/storage/amplify_storage_s3/example/windows/flutter/generated_plugin_registrant.cc +++ b/packages/storage/amplify_storage_s3/example/windows/flutter/generated_plugin_registrant.cc @@ -7,8 +7,11 @@ #include "generated_plugin_registrant.h" #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { AmplifyDbCommonPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("AmplifyDbCommonPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/packages/storage/amplify_storage_s3/example/windows/flutter/generated_plugins.cmake b/packages/storage/amplify_storage_s3/example/windows/flutter/generated_plugins.cmake index 20086cc391..92239a91a8 100644 --- a/packages/storage/amplify_storage_s3/example/windows/flutter/generated_plugins.cmake +++ b/packages/storage/amplify_storage_s3/example/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST amplify_db_common + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/packages/test/amplify_auth_integration_test/lib/amplify_auth_integration_test.dart b/packages/test/amplify_auth_integration_test/lib/amplify_auth_integration_test.dart index a16095a986..0023b6acaa 100644 --- a/packages/test/amplify_auth_integration_test/lib/amplify_auth_integration_test.dart +++ b/packages/test/amplify_auth_integration_test/lib/amplify_auth_integration_test.dart @@ -9,3 +9,4 @@ export 'src/async_test.dart'; export 'src/mfa_environments.dart'; export 'src/test_auth_plugin.dart'; export 'src/test_runner.dart'; +export 'src/totp_utils.dart'; diff --git a/packages/auth/amplify_auth_cognito/example/integration_test/utils/totp_utils.dart b/packages/test/amplify_auth_integration_test/lib/src/totp_utils.dart similarity index 100% rename from packages/auth/amplify_auth_cognito/example/integration_test/utils/totp_utils.dart rename to packages/test/amplify_auth_integration_test/lib/src/totp_utils.dart diff --git a/packages/test/amplify_auth_integration_test/pubspec.yaml b/packages/test/amplify_auth_integration_test/pubspec.yaml index bfa3cfde95..3453cc4350 100644 --- a/packages/test/amplify_auth_integration_test/pubspec.yaml +++ b/packages/test/amplify_auth_integration_test/pubspec.yaml @@ -12,15 +12,18 @@ environment: dependencies: amplify_api: any amplify_auth_cognito: any + amplify_auth_cognito_dart: any amplify_flutter: any amplify_integration_test: any async: ^2.10.0 + collection: any flutter: sdk: flutter flutter_test: sdk: flutter integration_test: sdk: flutter + otp: ^3.1.4 stack_trace: ^1.10.0 dev_dependencies: