Skip to content

Commit 40d8e43

Browse files
committed
fix(amplify_authenticator): authenticator phone OR email confirmation (#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
1 parent 8c274d4 commit 40d8e43

File tree

12 files changed

+261
-47
lines changed

12 files changed

+261
-47
lines changed

packages/amplify_authenticator/example/integration_test/config.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import 'package:amplify_flutter/amplify_flutter.dart';
1919
import 'envs/auth_with_email.dart' as auth_with_email;
2020
import 'envs/auth_with_email_lambda_signup_trigger.dart'
2121
as auth_with_email_lambda_signup_trigger;
22+
import 'envs/auth_with_email_or_phone.dart' as auth_with_email_or_phone;
2223
import 'envs/auth_with_phone.dart' as auth_with_phone;
2324
import 'envs/auth_with_username.dart' as auth_with_username;
2425
import 'pages/test_utils.dart';
@@ -38,6 +39,7 @@ const environmentsByConfiguration = {
3839
'auth-with-email-lambda-signup-trigger',
3940
'ui/components/authenticator/reset-password': 'auth-with-username',
4041
'ui/components/authenticator/verify-user': 'auth-with-email',
42+
'email-or-phone': 'auth-with-email-or-phone'
4143
};
4244

4345
const environments = {
@@ -46,6 +48,7 @@ const environments = {
4648
'auth-with-username': auth_with_username.amplifyconfig,
4749
'auth-with-email-lambda-signup-trigger':
4850
auth_with_email_lambda_signup_trigger.amplifyconfig,
51+
'auth-with-email-or-phone': auth_with_email_or_phone.amplifyconfig,
4952
};
5053

5154
Future<void> loadConfiguration(String configurationName,
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import 'dart:io';
16+
17+
import 'package:amplify_api/amplify_api.dart';
18+
import 'package:amplify_authenticator/amplify_authenticator.dart';
19+
import 'package:amplify_flutter/amplify_flutter.dart';
20+
import 'package:amplify_test/amplify_test.dart';
21+
import 'package:flutter/foundation.dart';
22+
import 'package:flutter/material.dart';
23+
import 'package:flutter_test/flutter_test.dart';
24+
import 'package:integration_test/integration_test.dart';
25+
26+
import 'config.dart';
27+
import 'pages/confirm_sign_up_page.dart';
28+
import 'pages/sign_in_page.dart';
29+
import 'pages/sign_up_page.dart';
30+
import 'pages/test_utils.dart';
31+
import 'utils/mock_data.dart';
32+
33+
void main() {
34+
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
35+
// resolves issue on iOS. See: https://github.com/flutter/flutter/issues/89651
36+
binding.deferFirstFrame();
37+
38+
final isMobile = !kIsWeb && (Platform.isIOS || Platform.isAndroid);
39+
40+
final authenticator = Authenticator(
41+
child: MaterialApp(
42+
builder: Authenticator.builder(),
43+
home: const Scaffold(
44+
body: Center(
45+
child: SignOutButton(),
46+
),
47+
),
48+
),
49+
);
50+
51+
group(
52+
'confirm-sign-up',
53+
() {
54+
// Given I'm running the example with an "Email or Phone" config
55+
setUpAll(() async {
56+
await loadConfiguration(
57+
'email-or-phone',
58+
additionalConfigs: isMobile ? [AmplifyAPI()] : null,
59+
);
60+
});
61+
62+
setUp(signOut);
63+
64+
tearDown(Amplify.Auth.deleteUser);
65+
66+
// Scenario: Sign up & confirm account with email as username
67+
testWidgets(
68+
'Sign up & confirm account with email as username',
69+
(tester) async {
70+
final signUpPage = SignUpPage(tester: tester);
71+
final confirmSignUpPage = ConfirmSignUpPage(tester: tester);
72+
final signInPage = SignInPage(tester: tester);
73+
74+
await loadAuthenticator(tester: tester, authenticator: authenticator);
75+
76+
final email = generateEmail();
77+
final phoneNumber = generateUSPhoneNumber();
78+
final password = generatePassword();
79+
80+
final code = getOtpCode(email);
81+
82+
await signInPage.navigateToSignUp();
83+
84+
// When I select email as a username
85+
await signUpPage.selectEmail();
86+
87+
// And I type my email address as a username
88+
await signUpPage.enterUsername(email);
89+
90+
// And I type my password
91+
await signUpPage.enterPassword(password);
92+
93+
// And I confirm my password
94+
await signUpPage.enterPasswordConfirmation(password);
95+
96+
// And I enter my phone number
97+
await signUpPage.enterPhoneNumber(phoneNumber.withOutCountryCode());
98+
99+
// And I click the "Create Account" button
100+
await signUpPage.submitSignUp();
101+
102+
// And I see "Confirmation Code"
103+
confirmSignUpPage.expectConfirmationCodeIsPresent();
104+
105+
// And I type a valid confirmation code
106+
await confirmSignUpPage.enterCode(await code);
107+
108+
// And I click the "Confirm" button
109+
await confirmSignUpPage.submitConfirmSignUp();
110+
111+
// Then I see "Sign out"
112+
await signInPage.expectAuthenticated();
113+
},
114+
);
115+
116+
testWidgets(
117+
'Sign up & confirm account with phone number as username',
118+
(tester) async {
119+
final signUpPage = SignUpPage(tester: tester);
120+
final confirmSignUpPage = ConfirmSignUpPage(tester: tester);
121+
final signInPage = SignInPage(tester: tester);
122+
123+
await loadAuthenticator(tester: tester, authenticator: authenticator);
124+
125+
final email = generateEmail();
126+
final phoneNumber = generateUSPhoneNumber();
127+
final password = generatePassword();
128+
129+
final code = getOtpCode(email);
130+
131+
await signInPage.navigateToSignUp();
132+
133+
// When I select phone number as a username
134+
await signUpPage.selectPhone();
135+
136+
// And I type my phone number as a username
137+
await signUpPage.enterUsername(phoneNumber.withOutCountryCode());
138+
139+
// And I type my password
140+
await signUpPage.enterPassword(password);
141+
142+
// And I confirm my password
143+
await signUpPage.enterPasswordConfirmation(password);
144+
145+
// And I enter my email address
146+
await signUpPage.enterEmail(email);
147+
148+
// And I click the "Create Account" button
149+
await signUpPage.submitSignUp();
150+
151+
// And I see "Confirmation Code"
152+
confirmSignUpPage.expectConfirmationCodeIsPresent();
153+
154+
// And I type a valid confirmation code
155+
await confirmSignUpPage.enterCode(await code);
156+
157+
// And I click the "Confirm" button
158+
await confirmSignUpPage.submitConfirmSignUp();
159+
160+
// Then I see "Sign out"
161+
await signInPage.expectAuthenticated();
162+
},
163+
);
164+
},
165+
skip: !isMobile,
166+
);
167+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
const amplifyconfig = ''' {
2+
"UserAgent": "aws-amplify-cli/2.0",
3+
"Version": "1.0"
4+
}''';

packages/amplify_authenticator/example/integration_test/pages/sign_up_page.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ class SignUpPage extends AuthenticatorPage {
3232
find.byKey(keyPreferredUsernameSignUpFormField);
3333
Finder get signUpButton => find.byKey(keySignUpButton);
3434

35+
Finder get selectEmailButton => find.byKey(keyEmailUsernameToggleButton);
36+
Finder get selectPhoneButton => find.byKey(keyPhoneUsernameToggleButton);
37+
3538
/// When I type a new "username"
3639
Future<void> enterUsername(String username) async {
3740
await tester.enterText(usernameField, username);
@@ -52,6 +55,11 @@ class SignUpPage extends AuthenticatorPage {
5255
await tester.enterText(emailField, email);
5356
}
5457

58+
/// When I type my "PhoneNumber"
59+
Future<void> enterPhoneNumber(String value) async {
60+
await tester.enterText(phoneField, value);
61+
}
62+
5563
/// When I type a new "preferred username"
5664
Future<void> enterPreferredUsername(String username) async {
5765
await tester.enterText(preferredUsernameField, username);
@@ -64,6 +72,20 @@ class SignUpPage extends AuthenticatorPage {
6472
await tester.pumpAndSettle();
6573
}
6674

75+
/// When I select "email" as a username
76+
Future<void> selectEmail() async {
77+
await tester.ensureVisible(selectEmailButton);
78+
await tester.tap(selectEmailButton);
79+
await tester.pumpAndSettle();
80+
}
81+
82+
/// When I select "phone" as a username
83+
Future<void> selectPhone() async {
84+
await tester.ensureVisible(selectPhoneButton);
85+
await tester.tap(selectPhoneButton);
86+
await tester.pumpAndSettle();
87+
}
88+
6789
/// Then I see "Username" as an input field
6890
void expectUserNameIsPresent({String usernameLabel = 'Username'}) {
6991
// username field is present

packages/amplify_authenticator/lib/amplify_authenticator.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ export 'package:amplify_flutter/amplify_flutter.dart'
5555
export 'src/enums/enums.dart' show AuthenticatorStep, Gender;
5656
export 'src/l10n/auth_strings_resolver.dart' hide ButtonResolverKeyType;
5757
export 'src/models/authenticator_exception.dart';
58-
export 'src/models/username_input.dart' show UsernameType, UsernameInput;
58+
export 'src/models/username_input.dart'
59+
show UsernameType, UsernameInput, UsernameSelection;
5960
export 'src/state/authenticator_state.dart';
6061
export 'src/widgets/button.dart'
6162
show

packages/amplify_authenticator/lib/src/keys.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ const keyForgotPasswordButton = Key('forgotPasswordButton');
122122
const keySkipVerifyUserButton = Key('skipVerifyUserButton');
123123
const keySubmitVerifyUserButton = Key('submitVerifyUserButton');
124124
const keySubmitConfirmVerifyUserButton = Key('submitConfirmVerifyUserButton');
125+
const keyEmailUsernameToggleButton = Key('emailUsernameToggleButton');
126+
const keyPhoneUsernameToggleButton = Key('phoneUsernameToggleButton');
125127

126128
// Checkboxes keys
127129

packages/amplify_authenticator/lib/src/mixins/authenticator_username_field.dart

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
*/
1515

1616
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
17+
import 'package:amplify_authenticator/src/keys.dart';
1718
import 'package:amplify_authenticator/src/l10n/auth_strings_resolver.dart';
1819
import 'package:amplify_authenticator/src/models/username_input.dart';
20+
import 'package:amplify_authenticator/src/utils/country_code.dart';
1921
import 'package:amplify_authenticator/src/utils/validators.dart';
2022
import 'package:amplify_authenticator/src/widgets/component.dart';
2123
import 'package:amplify_authenticator/src/widgets/form.dart';
@@ -129,23 +131,39 @@ mixin AuthenticatorUsernameField<FieldType,
129131
return ToggleButtons(
130132
borderWidth: buttonBorderWidth,
131133
constraints: buttonConstraints,
132-
isSelected: [useEmail.value, !useEmail.value],
134+
isSelected: [
135+
state.usernameSelection == UsernameSelection.email,
136+
state.usernameSelection == UsernameSelection.phoneNumber,
137+
],
133138
onPressed: (int index) {
134-
bool useEmail = index == 0;
135-
setState(() {
136-
this.useEmail.value = useEmail;
137-
});
138-
// Reset current username value to align with the current switch state.
139-
String newUsername = useEmail
139+
final newUsernameSelection = index == 0
140+
? UsernameSelection.email
141+
: UsernameSelection.phoneNumber;
142+
// Return if username selection has not changed
143+
if (newUsernameSelection == state.usernameSelection) {
144+
return;
145+
}
146+
// Determine the new username value based off the new username selection
147+
// and the current user attributes
148+
final newUsername = newUsernameSelection ==
149+
UsernameSelection.email
140150
? state.getAttribute(CognitoUserAttributeKey.email) ?? ''
141151
: state.getAttribute(
142152
CognitoUserAttributeKey.phoneNumber) ??
143153
'';
154+
// Clear user attributes
155+
state.authAttributes.clear();
156+
// Reset country code if phone is not being used as a username
157+
if (newUsernameSelection != UsernameSelection.phoneNumber) {
158+
state.country = countryCodes.first;
159+
}
160+
// Update the username & username selection
144161
state.username = newUsername;
162+
state.usernameSelection = newUsernameSelection;
145163
},
146164
children: [
147-
Text(emailTitle),
148-
Text(phoneNumberTitle),
165+
Text(emailTitle, key: keyEmailUsernameToggleButton),
166+
Text(phoneNumberTitle, key: keyPhoneUsernameToggleButton),
149167
],
150168
);
151169
}),
@@ -219,7 +237,7 @@ mixin AuthenticatorUsernameField<FieldType,
219237
validator: _validator,
220238
enabled: enabled,
221239
errorMaxLines: errorMaxLines,
222-
initialValue: state.getAttribute(CognitoUserAttributeKey.phoneNumber),
240+
initialValue: state.username,
223241
);
224242
}
225243
return TextFormField(
@@ -253,9 +271,6 @@ mixin UsernameAttributes<T extends AuthenticatorForm>
253271
return <CognitoUserAttributeKey>{...?authConfig?.usernameAttributes};
254272
}();
255273

256-
/// Toggle value for the email or phone number case.
257-
final ValueNotifier<bool> useEmail = ValueNotifier(true);
258-
259274
UsernameConfigType get usernameType {
260275
if (usernameAttributes.isEmpty) {
261276
return UsernameConfigType.username;
@@ -283,7 +298,7 @@ mixin UsernameAttributes<T extends AuthenticatorForm>
283298
case UsernameConfigType.phoneNumber:
284299
return UsernameType.phoneNumber;
285300
case UsernameConfigType.emailOrPhoneNumber:
286-
if (useEmail.value) {
301+
if (state.usernameSelection == UsernameSelection.email) {
287302
return UsernameType.email;
288303
}
289304
return UsernameType.phoneNumber;

packages/amplify_authenticator/lib/src/models/username_input.dart

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ enum UsernameConfigType {
2323
}
2424

2525
/// {@template amplify_authenticator.username_type}
26-
/// The type of username input field presented to the user. Depending on your
27-
/// Cognito configuration, users may choose to create their own username, use
28-
/// their email, or use their phone number as their login.
26+
/// The type of username input field presented to the user.
27+
///
28+
/// Depending on your Cognito configuration, users will be required to either
29+
/// create a unique username, or sign up with an email or phone number.
2930
/// {@endtemplate}
3031
enum UsernameType {
3132
/// The user's chosen username.
@@ -57,3 +58,12 @@ class UsernameInput {
5758
required this.username,
5859
});
5960
}
61+
62+
/// {@template amplify_authenticator.username_input.username_selection}
63+
/// The username type to use during sign up and sign in for configurations
64+
/// that allow email OR phone number.
65+
/// {@endtemplate}
66+
enum UsernameSelection {
67+
email,
68+
phoneNumber,
69+
}

0 commit comments

Comments
 (0)