Skip to content

Commit 7757fa1

Browse files
committed
Refactor form field controllers to use AuthenticatorTextFieldController
- Updated all form fields to replace TextEditingController with AuthenticatorTextFieldController. - Modified constructors and properties across various form fields including ConfirmSignUpFormField, EmailSetupFormField, PhoneNumberField, ResetPasswordFormField, SignInFormField, SignUpFormField, and VerifyUserFormField. - Adjusted tests to accommodate the new controller type, ensuring compatibility and functionality remain intact.
1 parent f17198a commit 7757fa1

17 files changed

+428
-309
lines changed

packages/authenticator/amplify_authenticator/CHANGELOG.md

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,3 @@
1-
## 2.4.0
2-
3-
### Features
4-
5-
- feat(authenticator): Add TextEditingController support to form fields
6-
- Added `AuthenticatorTextFieldController` class for programmatic control of form fields
7-
- All text-based form fields now accept an optional `controller` parameter
8-
- Enables pre-populating fields (e.g., from GPS/API data) and auto-filling verification codes
9-
- Bidirectional sync between controller and internal state
10-
- Compatible with standard `TextEditingController` for flexibility
11-
- feat(authenticator): Allow SignUpFormField inputs to be disabled or hidden so apps can prefill values programmatically or keep legacy attributes off-screen
12-
131
## 2.3.8
142

153
### Chores

packages/authenticator/amplify_authenticator/example/lib/authenticator_with_controllers.dart

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,19 +97,28 @@ class _AuthenticatorWithControllersState
9797
signUpForm: SignUpForm.custom(
9898
fields: [
9999
// Username field with controller - can be pre-populated or modified
100-
SignUpFormField.username(controller: _usernameController),
100+
SignUpFormField.username(
101+
authenticatorTextFieldController: _usernameController,
102+
),
101103

102104
// Email field with controller
103-
SignUpFormField.email(controller: _emailController, required: true),
105+
SignUpFormField.email(
106+
authenticatorTextFieldController: _emailController,
107+
required: true,
108+
),
104109

105110
SignUpFormField.password(),
106111
SignUpFormField.passwordConfirmation(),
107112

108113
// Address field with controller - can be populated from GPS/API
109-
SignUpFormField.address(controller: _addressController),
114+
SignUpFormField.address(
115+
authenticatorTextFieldController: _addressController,
116+
),
110117

111118
// Phone number field with controller
112-
SignUpFormField.phoneNumber(controller: _phoneController),
119+
SignUpFormField.phoneNumber(
120+
authenticatorTextFieldController: _phoneController,
121+
),
113122
],
114123
),
115124

packages/authenticator/amplify_authenticator/lib/amplify_authenticator.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ export 'package:amplify_authenticator/src/utils/dial_code_options.dart'
4343
show DialCodeOptions;
4444

4545
export 'src/controllers/authenticator_text_field_controller.dart';
46-
export 'src/enums/enums.dart' show AuthenticatorStep, Gender;
46+
export 'src/enums/enums.dart'
47+
show AuthenticatorStep, AuthenticatorTextEnabledOverride, Gender;
4748
export 'src/l10n/auth_strings_resolver.dart' hide ButtonResolverKeyType;
4849
export 'src/models/authenticator_exception.dart';
4950
export 'src/models/totp_options.dart';
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
/// Enum for overriding the enabled state of a form field.
5+
enum AuthenticatorTextEnabledOverride {
6+
/// Use the default enabled state.
7+
defaultSetting,
8+
9+
/// Force the field to be enabled.
10+
enabled,
11+
12+
/// Force the field to be disabled.
13+
disabled,
14+
}

packages/authenticator/amplify_authenticator/lib/src/enums/enums.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
export 'authenticator_step.dart';
5+
export 'authenticator_text_enabled_override.dart';
56
export 'confirm_signin_types.dart';
67
export 'confirm_signup_types.dart';
78
export 'email_setup_types.dart';

packages/authenticator/amplify_authenticator/lib/src/mixins/authenticator_text_field.dart

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ mixin AuthenticatorTextField<
1919
bool _controllerUpdateScheduled = false;
2020

2121
@protected
22-
TextEditingController? get textController => null;
22+
TextEditingController? get textController =>
23+
widget.authenticatorTextFieldController;
2324

2425
void _maybeUpdateEffectiveController() {
2526
final controller = textController;
@@ -69,13 +70,30 @@ mixin AuthenticatorTextField<
6970
if (controller == null) {
7071
return;
7172
}
73+
74+
// If there is a pending controller update, do not overwrite the controller
75+
// with the state value, as the state value may be stale.
76+
if (_pendingControllerText != null && !force) {
77+
return;
78+
}
79+
7280
final target = initialValue ?? '';
7381
final controllerText = controller.text;
82+
7483
if (!force && controllerText == target) {
7584
_lastSyncedControllerValue = controllerText;
7685
return;
7786
}
7887

88+
// If the controller has changed locally (user input) but the state
89+
// has not changed from what we last synced, ignore the state value
90+
// as it is likely stale.
91+
if (!force &&
92+
controllerText != _lastSyncedControllerValue &&
93+
target == _lastSyncedControllerValue) {
94+
return;
95+
}
96+
7997
final normalizedController = controllerText.trimRight();
8098
final normalizedTarget = target.trimRight();
8199
if (normalizedController == normalizedTarget) {
@@ -104,18 +122,31 @@ mixin AuthenticatorTextField<
104122
void didChangeDependencies() {
105123
super.didChangeDependencies();
106124
_maybeUpdateEffectiveController();
107-
// Schedule both syncs after build completes
108-
WidgetsBinding.instance.addPostFrameCallback((_) {
109-
if (mounted) {
110-
// First sync controller -> state if controller has initial text
111-
if (_effectiveController != null &&
112-
_lastSyncedControllerValue == null) {
113-
_handleControllerChanged();
114-
}
125+
if (mounted) {
126+
// First sync controller -> state if controller has initial text
127+
if (_effectiveController != null &&
128+
_lastSyncedControllerValue == null &&
129+
_effectiveController!.text.isNotEmpty) {
130+
final text = _effectiveController!.text;
131+
WidgetsBinding.instance.addPostFrameCallback((_) {
132+
if (!mounted) return;
133+
_isApplyingControllerText = true;
134+
try {
135+
onChanged(text);
136+
_lastSyncedControllerValue = text;
137+
} finally {
138+
_isApplyingControllerText = false;
139+
}
140+
});
141+
} else {
115142
// Then sync state -> controller to ensure they're in sync
116-
_syncControllerText();
143+
WidgetsBinding.instance.addPostFrameCallback((_) {
144+
if (mounted) {
145+
_syncControllerText();
146+
}
147+
});
117148
}
118-
});
149+
}
119150
}
120151

121152
@override
@@ -124,7 +155,7 @@ mixin AuthenticatorTextField<
124155
_maybeUpdateEffectiveController();
125156
WidgetsBinding.instance.addPostFrameCallback((_) {
126157
if (mounted) {
127-
_syncControllerText(force: true);
158+
_syncControllerText();
128159
}
129160
});
130161
}
@@ -144,7 +175,6 @@ mixin AuthenticatorTextField<
144175
final hintText = widget.hintText == null
145176
? widget.hintTextKey?.resolve(context, inputResolver)
146177
: widget.hintText!;
147-
_maybeUpdateEffectiveController();
148178
return ValueListenableBuilder<bool>(
149179
valueListenable: AuthenticatorFormState.of(
150180
context,

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

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ mixin AuthenticatorUsernameField<
2424
bool _controllerUpdateScheduled = false;
2525

2626
@protected
27-
TextEditingController? get textController => null;
27+
TextEditingController? get textController =>
28+
widget.authenticatorTextFieldController;
2829

2930
void _updateController() {
3031
final controller = textController;
@@ -96,13 +97,28 @@ mixin AuthenticatorUsernameField<
9697
return;
9798
}
9899

100+
// If there is a pending controller update, do not overwrite the controller
101+
// with the state value, as the state value may be stale.
102+
if (_pendingControllerText != null && !force) {
103+
return;
104+
}
105+
99106
final target = initialValue?.username ?? '';
100107
final controllerText = _controller!.text;
101108
if (!force && controllerText == target) {
102109
_lastSyncedText = controllerText;
103110
return;
104111
}
105112

113+
// If the controller has changed locally (user input) but the state
114+
// has not changed from what we last synced, ignore the state value
115+
// as it is likely stale.
116+
if (!force &&
117+
controllerText != _lastSyncedText &&
118+
target == _lastSyncedText) {
119+
return;
120+
}
121+
106122
final normalizedController = controllerText.trimRight();
107123
final normalizedTarget = target.trimRight();
108124
if (normalizedController == normalizedTarget) {
@@ -132,17 +148,33 @@ mixin AuthenticatorUsernameField<
132148
void didChangeDependencies() {
133149
super.didChangeDependencies();
134150
_updateController();
135-
// Schedule both syncs after build completes
136-
WidgetsBinding.instance.addPostFrameCallback((_) {
137-
if (mounted) {
138-
// First sync controller -> state if controller has initial text
139-
if (_controller != null && _lastSyncedText == null) {
140-
_handleControllerChanged();
141-
}
151+
if (mounted) {
152+
// First sync controller -> state if controller has initial text
153+
if (_controller != null &&
154+
_lastSyncedText == null &&
155+
_controller!.text.isNotEmpty) {
156+
final text = _controller!.text;
157+
WidgetsBinding.instance.addPostFrameCallback((_) {
158+
if (!mounted) return;
159+
_applyingControllerText = true;
160+
try {
161+
onChanged(
162+
UsernameInput(type: selectedUsernameType, username: text),
163+
);
164+
_lastSyncedText = text;
165+
} finally {
166+
_applyingControllerText = false;
167+
}
168+
});
169+
} else {
142170
// Then sync state -> controller to ensure they're in sync
143-
_syncControllerText();
171+
WidgetsBinding.instance.addPostFrameCallback((_) {
172+
if (mounted) {
173+
_syncControllerText();
174+
}
175+
});
144176
}
145-
});
177+
}
146178
}
147179

148180
@override
@@ -151,7 +183,7 @@ mixin AuthenticatorUsernameField<
151183
_updateController();
152184
WidgetsBinding.instance.addPostFrameCallback((_) {
153185
if (mounted) {
154-
_syncControllerText(force: true);
186+
_syncControllerText();
155187
}
156188
});
157189
}
@@ -366,11 +398,14 @@ mixin AuthenticatorUsernameField<
366398
requiredOverride: true,
367399
onChanged: handleChanged,
368400
validator: validator,
369-
enabled: enabled,
401+
enabled: enabled
402+
? AuthenticatorTextEnabledOverride.enabled
403+
: AuthenticatorTextEnabledOverride.disabled,
370404
errorMaxLines: errorMaxLines,
371405
initialValue: state.username,
372406
autofillHints: autofillHints,
373-
controller: textController,
407+
authenticatorTextFieldController:
408+
textController as AuthenticatorTextFieldController?,
374409
);
375410
}
376411

packages/authenticator/amplify_authenticator/lib/src/widgets/form_field.dart

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,13 @@ abstract class AuthenticatorFormField<
9999
final Iterable<String>? autofillHints;
100100

101101
/// Optional text controller exposed by text-driven form fields.
102-
TextEditingController? get controller => null;
102+
AuthenticatorTextFieldController? get authenticatorTextFieldController =>
103+
null;
103104

104105
/// Whether the field can receive manual input.
105106
///
106107
/// When `null`, the widget decides its enabled state.
107-
final bool? enabledOverride;
108+
final AuthenticatorTextEnabledOverride? enabledOverride;
108109

109110
/// Whether the field should be rendered.
110111
///
@@ -143,9 +144,17 @@ abstract class AuthenticatorFormField<
143144
..add(EnumProperty<UsernameType?>('usernameType', usernameType))
144145
..add(IterableProperty<String>('autofillHints', autofillHints))
145146
..add(
146-
DiagnosticsProperty<TextEditingController?>('controller', controller),
147+
DiagnosticsProperty<AuthenticatorTextFieldController?>(
148+
'authenticatorTextFieldController',
149+
authenticatorTextFieldController,
150+
),
151+
)
152+
..add(
153+
EnumProperty<AuthenticatorTextEnabledOverride?>(
154+
'enabledOverride',
155+
enabledOverride,
156+
),
147157
)
148-
..add(DiagnosticsProperty<bool?>('enabledOverride', enabledOverride))
149158
..add(DiagnosticsProperty<bool>('visible', visible));
150159
}
151160
}
@@ -187,7 +196,17 @@ abstract class AuthenticatorFormFieldState<
187196
FieldValue? get initialValue => null;
188197

189198
/// Whether the form field accepts input.
190-
bool get enabled => widget.enabledOverride ?? true;
199+
bool get enabled {
200+
switch (widget.enabledOverride) {
201+
case AuthenticatorTextEnabledOverride.enabled:
202+
return true;
203+
case AuthenticatorTextEnabledOverride.disabled:
204+
return false;
205+
case AuthenticatorTextEnabledOverride.defaultSetting:
206+
case null:
207+
return true;
208+
}
209+
}
191210

192211
/// Widget to show at leading end, typically an [Icon].
193212
Widget? get prefix => null;

0 commit comments

Comments
 (0)