Skip to content

Commit

Permalink
fix(amplify_authenticator): authenticator phone OR email confirmation (
Browse files Browse the repository at this point in the history
…#1785)

* fix(authenticator): move username selection state to authenticator state

* chore; update imports

* test: add integration tests for email or phone configs

* chore: move username input enum
  • Loading branch information
Jordan-Nelson authored Jun 22, 2022
1 parent 39674c3 commit 666349a
Show file tree
Hide file tree
Showing 12 changed files with 261 additions and 47 deletions.
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,167 @@
// 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 {
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();
},
);

testWidgets(
'Sign up & confirm account with phone number as username',
(tester) async {
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,
);
}
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/utils/country_code.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,39 @@ 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;
});
// Reset current username value to align with the current switch state.
String newUsername = useEmail
final newUsernameSelection = index == 0
? UsernameSelection.email
: UsernameSelection.phoneNumber;
// Return if username selection has not changed
if (newUsernameSelection == state.usernameSelection) {
return;
}
// Determine the new username value based off the new username selection
// and the current user attributes
final newUsername = newUsernameSelection ==
UsernameSelection.email
? state.getAttribute(CognitoUserAttributeKey.email) ?? ''
: state.getAttribute(
CognitoUserAttributeKey.phoneNumber) ??
'';
// Clear user attributes
state.authAttributes.clear();
// Reset country code if phone is not being used as a username
if (newUsernameSelection != UsernameSelection.phoneNumber) {
state.country = countryCodes.first;
}
// Update the username & username selection
state.username = newUsername;
state.usernameSelection = newUsernameSelection;
},
children: [
Text(emailTitle),
Text(phoneNumberTitle),
Text(emailTitle, key: keyEmailUsernameToggleButton),
Text(phoneNumberTitle, key: keyPhoneUsernameToggleButton),
],
);
}),
Expand Down Expand Up @@ -219,7 +237,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 +271,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 +298,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
16 changes: 13 additions & 3 deletions packages/amplify_authenticator/lib/src/models/username_input.dart
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,
}
Loading

0 comments on commit 666349a

Please sign in to comment.