Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(amplify_authenticator): authenticator phone OR email confirmation #1785

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import 'package:amplify_flutter/amplify_flutter.dart';
import 'envs/auth_with_email.dart' as auth_with_email;
import 'envs/auth_with_email_lambda_signup_trigger.dart'
as auth_with_email_lambda_signup_trigger;
import 'envs/auth_with_email_or_phone.dart' as auth_with_email_or_phone;
import 'envs/auth_with_phone.dart' as auth_with_phone;
import 'envs/auth_with_username.dart' as auth_with_username;
import 'pages/test_utils.dart';
Expand All @@ -38,6 +39,7 @@ const environmentsByConfiguration = {
'auth-with-email-lambda-signup-trigger',
'ui/components/authenticator/reset-password': 'auth-with-username',
'ui/components/authenticator/verify-user': 'auth-with-email',
'email-or-phone': 'auth-with-email-or-phone'
};

const environments = {
Expand All @@ -46,6 +48,7 @@ const environments = {
'auth-with-username': auth_with_username.amplifyconfig,
'auth-with-email-lambda-signup-trigger':
auth_with_email_lambda_signup_trigger.amplifyconfig,
'auth-with-email-or-phone': auth_with_email_or_phone.amplifyconfig,
};

Future<void> loadConfiguration(String configurationName,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import 'dart:io';

import 'package:amplify_api/amplify_api.dart';
import 'package:amplify_authenticator/amplify_authenticator.dart';
import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:amplify_test/amplify_test.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

import 'config.dart';
import 'pages/confirm_sign_up_page.dart';
import 'pages/sign_in_page.dart';
import 'pages/sign_up_page.dart';
import 'pages/test_utils.dart';
import 'utils/mock_data.dart';

void main() {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// resolves issue on iOS. See: https://github.com/flutter/flutter/issues/89651
binding.deferFirstFrame();

final isMobile = !kIsWeb && (Platform.isIOS || Platform.isAndroid);

final authenticator = Authenticator(
child: MaterialApp(
builder: Authenticator.builder(),
home: const Scaffold(
body: Center(
child: SignOutButton(),
),
),
),
);

group('confirm-sign-up', () {
// Given I'm running the example with an "Email or Phone" config
setUpAll(() async {
await loadConfiguration(
'email-or-phone',
additionalConfigs: isMobile ? [AmplifyAPI()] : null,
);
});

setUp(signOut);

tearDown(Amplify.Auth.deleteUser);

// Scenario: Sign up & confirm account with email as username
testWidgets(
'Sign up & confirm account with email as username',
(tester) async {
SignUpPage signUpPage = SignUpPage(tester: tester);
ConfirmSignUpPage confirmSignUpPage = ConfirmSignUpPage(tester: tester);
SignInPage signInPage = SignInPage(tester: tester);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
SignUpPage signUpPage = SignUpPage(tester: tester);
ConfirmSignUpPage confirmSignUpPage = ConfirmSignUpPage(tester: tester);
SignInPage signInPage = SignInPage(tester: tester);
final signUpPage = SignUpPage(tester: tester);
final confirmSignUpPage = ConfirmSignUpPage(tester: tester);
final signInPage = SignInPage(tester: tester);


await loadAuthenticator(tester: tester, authenticator: authenticator);

final email = generateEmail();
final phoneNumber = generateUSPhoneNumber();
final password = generatePassword();

final code = getOtpCode(email);

await signInPage.navigateToSignUp();

// When I select email as a username
await signUpPage.selectEmail();

// And I type my email address as a username
await signUpPage.enterUsername(email);

// And I type my password
await signUpPage.enterPassword(password);

// And I confirm my password
await signUpPage.enterPasswordConfirmation(password);

// And I enter my phone number
await signUpPage.enterPhoneNumber(phoneNumber.withOutCountryCode());

// And I click the "Create Account" button
await signUpPage.submitSignUp();

// And I see "Confirmation Code"
confirmSignUpPage.expectConfirmationCodeIsPresent();

// And I type a valid confirmation code
await confirmSignUpPage.enterCode(await code);

// And I click the "Confirm" button
await confirmSignUpPage.submitConfirmSignUp();

// Then I see "Sign out"
await signInPage.expectAuthenticated();
},
skip: !isMobile,
);

testWidgets(
'Sign up & confirm account with phone number as username',
(tester) async {
SignUpPage signUpPage = SignUpPage(tester: tester);
ConfirmSignUpPage confirmSignUpPage = ConfirmSignUpPage(tester: tester);
SignInPage signInPage = SignInPage(tester: tester);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
SignUpPage signUpPage = SignUpPage(tester: tester);
ConfirmSignUpPage confirmSignUpPage = ConfirmSignUpPage(tester: tester);
SignInPage signInPage = SignInPage(tester: tester);
final signUpPage = SignUpPage(tester: tester);
final confirmSignUpPage = ConfirmSignUpPage(tester: tester);
final signInPage = SignInPage(tester: tester);


await loadAuthenticator(tester: tester, authenticator: authenticator);

final email = generateEmail();
final phoneNumber = generateUSPhoneNumber();
final password = generatePassword();

final code = getOtpCode(email);

await signInPage.navigateToSignUp();

// When I select phone number as a username
await signUpPage.selectPhone();

// And I type my phone number as a username
await signUpPage.enterUsername(phoneNumber.withOutCountryCode());

// And I type my password
await signUpPage.enterPassword(password);

// And I confirm my password
await signUpPage.enterPasswordConfirmation(password);

// And I enter my email address
await signUpPage.enterEmail(email);

// And I click the "Create Account" button
await signUpPage.submitSignUp();

// And I see "Confirmation Code"
confirmSignUpPage.expectConfirmationCodeIsPresent();

// And I type a valid confirmation code
await confirmSignUpPage.enterCode(await code);

// And I click the "Confirm" button
await confirmSignUpPage.submitConfirmSignUp();

// Then I see "Sign out"
await signInPage.expectAuthenticated();
},
skip: !isMobile,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can skip on the group

);
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const amplifyconfig = ''' {
"UserAgent": "aws-amplify-cli/2.0",
"Version": "1.0"
}''';
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ class SignUpPage extends AuthenticatorPage {
find.byKey(keyPreferredUsernameSignUpFormField);
Finder get signUpButton => find.byKey(keySignUpButton);

Finder get selectEmailButton => find.byKey(keyEmailUsernameToggleButton);
Finder get selectPhoneButton => find.byKey(keyPhoneUsernameToggleButton);

/// When I type a new "username"
Future<void> enterUsername(String username) async {
await tester.enterText(usernameField, username);
Expand All @@ -52,6 +55,11 @@ class SignUpPage extends AuthenticatorPage {
await tester.enterText(emailField, email);
}

/// When I type my "PhoneNumber"
Future<void> enterPhoneNumber(String value) async {
await tester.enterText(phoneField, value);
}

/// When I type a new "preferred username"
Future<void> enterPreferredUsername(String username) async {
await tester.enterText(preferredUsernameField, username);
Expand All @@ -64,6 +72,20 @@ class SignUpPage extends AuthenticatorPage {
await tester.pumpAndSettle();
}

/// When I select "email" as a username
Future<void> selectEmail() async {
await tester.ensureVisible(selectEmailButton);
await tester.tap(selectEmailButton);
await tester.pumpAndSettle();
}

/// When I select "phone" as a username
Future<void> selectPhone() async {
await tester.ensureVisible(selectPhoneButton);
await tester.tap(selectPhoneButton);
await tester.pumpAndSettle();
}

/// Then I see "Username" as an input field
void expectUserNameIsPresent({String usernameLabel = 'Username'}) {
// username field is present
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ 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/username_input.dart' show UsernameType, UsernameInput;
export 'src/models/username_input.dart'
show UsernameType, UsernameInput, UsernameSelection;
export 'src/state/authenticator_state.dart';
export 'src/widgets/button.dart'
show
Expand Down
2 changes: 2 additions & 0 deletions packages/amplify_authenticator/lib/src/keys.dart
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ const keyForgotPasswordButton = Key('forgotPasswordButton');
const keySkipVerifyUserButton = Key('skipVerifyUserButton');
const keySubmitVerifyUserButton = Key('submitVerifyUserButton');
const keySubmitConfirmVerifyUserButton = Key('submitConfirmVerifyUserButton');
const keyEmailUsernameToggleButton = Key('emailUsernameToggleButton');
const keyPhoneUsernameToggleButton = Key('phoneUsernameToggleButton');

// Checkboxes keys

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
*/

import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_authenticator/src/keys.dart';
import 'package:amplify_authenticator/src/l10n/auth_strings_resolver.dart';
import 'package:amplify_authenticator/src/models/username_input.dart';
import 'package:amplify_authenticator/src/state/authenticator_state.dart';
import 'package:amplify_authenticator/src/utils/validators.dart';
import 'package:amplify_authenticator/src/widgets/component.dart';
import 'package:amplify_authenticator/src/widgets/form.dart';
Expand Down Expand Up @@ -129,23 +131,33 @@ mixin AuthenticatorUsernameField<FieldType,
return ToggleButtons(
borderWidth: buttonBorderWidth,
constraints: buttonConstraints,
isSelected: [useEmail.value, !useEmail.value],
isSelected: [
state.usernameSelection == UsernameSelection.email,
state.usernameSelection == UsernameSelection.phoneNumber,
],
onPressed: (int index) {
bool useEmail = index == 0;
setState(() {
this.useEmail.value = useEmail;
});
final newUsernameSelection = index == 0
? UsernameSelection.email
: UsernameSelection.phoneNumber;
final oldUsernameSelection = state.usernameSelection;
if (oldUsernameSelection == newUsernameSelection) {
return;
}
state.usernameSelection = newUsernameSelection;
// Reset current username value to align with the current switch state.
String newUsername = useEmail
String newUsername = newUsernameSelection ==
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
String newUsername = newUsernameSelection ==
final newUsername = newUsernameSelection ==

UsernameSelection.email
? state.getAttribute(CognitoUserAttributeKey.email) ?? ''
: state.getAttribute(
CognitoUserAttributeKey.phoneNumber) ??
'';
state.username = newUsername;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since .username and .usernameSelection both trigger rebuilds, I think setting the username first might make more sense?

// Clear attributes on switch
state.authAttributes.clear();
Jordan-Nelson marked this conversation as resolved.
Show resolved Hide resolved
},
children: [
Text(emailTitle),
Text(phoneNumberTitle),
Text(emailTitle, key: keyEmailUsernameToggleButton),
Text(phoneNumberTitle, key: keyPhoneUsernameToggleButton),
],
);
}),
Expand Down Expand Up @@ -219,7 +231,7 @@ mixin AuthenticatorUsernameField<FieldType,
validator: _validator,
enabled: enabled,
errorMaxLines: errorMaxLines,
initialValue: state.getAttribute(CognitoUserAttributeKey.phoneNumber),
initialValue: state.username,
);
}
return TextFormField(
Expand Down Expand Up @@ -253,9 +265,6 @@ mixin UsernameAttributes<T extends AuthenticatorForm>
return <CognitoUserAttributeKey>{...?authConfig?.usernameAttributes};
}();

/// Toggle value for the email or phone number case.
final ValueNotifier<bool> useEmail = ValueNotifier(true);

UsernameConfigType get usernameType {
if (usernameAttributes.isEmpty) {
return UsernameConfigType.username;
Expand Down Expand Up @@ -283,7 +292,7 @@ mixin UsernameAttributes<T extends AuthenticatorForm>
case UsernameConfigType.phoneNumber:
return UsernameType.phoneNumber;
case UsernameConfigType.emailOrPhoneNumber:
if (useEmail.value) {
if (state.usernameSelection == UsernameSelection.email) {
return UsernameType.email;
}
return UsernameType.phoneNumber;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ enum UsernameConfigType {
}

/// {@template amplify_authenticator.username_type}
/// The type of username input field presented to the user. Depending on your
/// Cognito configuration, users may choose to create their own username, use
/// their email, or use their phone number as their login.
/// The type of username input field presented to the user.
///
/// Depending on your Cognito configuration, users will be required to either
/// create a unique username, or sign up with an email or phone number.
/// {@endtemplate}
enum UsernameType {
/// The user's chosen username.
Expand Down Expand Up @@ -57,3 +58,12 @@ class UsernameInput {
required this.username,
});
}

/// {@template amplify_authenticator.username_input.username_selection}
/// The username type to use during sign up and sign in for configurations
/// that allow email OR phone number.
/// {@endtemplate}
enum UsernameSelection {
email,
phoneNumber,
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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/models/username_input.dart';
import 'package:amplify_authenticator/src/state/auth_state.dart';
import 'package:amplify_authenticator/src/utils/country_code.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -98,6 +99,21 @@ class AuthenticatorState extends ChangeNotifier {

String _username = '';

/// {@macro amplify_authenticator.username_input.username_selection}
///
/// Defaults to [UsernameSelection.email].
///
/// The value has no meaning for Auth configurations that do not allow
/// for email OR phone number to be used as username.
UsernameSelection get usernameSelection => _usernameSelection;

set usernameSelection(UsernameSelection value) {
_usernameSelection = value;
notifyListeners();
}

UsernameSelection _usernameSelection = UsernameSelection.email;

/// The value for the password form field
///
/// This value will be used during sign up, sign in, or other actions
Expand Down
Loading