Skip to content

Commit 23d0f31

Browse files
committed
feat: Add AuthenticatorTextFieldController and integrate with form fields
- Introduced AuthenticatorTextFieldController for programmatic control over text fields. - Updated mixins (AuthenticatorTextField, AuthenticatorUsernameField) to utilize the new controller. - Enhanced various form fields (ConfirmSignIn, ConfirmSignUp, EmailSetup, PhoneNumber, ResetPassword, SignIn, SignUp, VerifyUser) to accept and manage TextEditingController. - Added tests to ensure synchronization between form fields and their respective controllers.
1 parent 520d2ad commit 23d0f31

13 files changed

+457
-6
lines changed

packages/authenticator/amplify_authenticator/lib/amplify_authenticator.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export 'src/widgets/form_field.dart'
9494
TotpSetupFormField,
9595
VerifyUserFormField;
9696
export 'src/widgets/social/social_button.dart';
97+
export 'src/controllers/authenticator_text_field_controller.dart';
9798

9899
/// {@template amplify_authenticator.authenticator}
99100
/// # Amplify Authenticator
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import 'package:flutter/widgets.dart';
5+
6+
/// Controller for text driven Authenticator form fields.
7+
///
8+
/// Wraps Flutter's [TextEditingController] so developers can opt in to
9+
/// programmatic control over prebuilt Authenticator fields while keeping
10+
/// identical semantics to a regular controller.
11+
class AuthenticatorTextFieldController extends TextEditingController {
12+
AuthenticatorTextFieldController({super.text});
13+
14+
AuthenticatorTextFieldController.fromValue(TextEditingValue value)
15+
: super.fromValue(value);
16+
}

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

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,108 @@ mixin AuthenticatorTextField<
1111
T extends AuthenticatorFormField<FieldType, String>
1212
>
1313
on AuthenticatorFormFieldState<FieldType, String, T> {
14+
TextEditingController? _effectiveController;
15+
bool _isApplyingControllerText = false;
16+
String? _lastSyncedControllerValue;
17+
18+
@protected
19+
TextEditingController? get textController => null;
20+
21+
void _maybeUpdateEffectiveController() {
22+
final controller = textController;
23+
if (identical(controller, _effectiveController)) {
24+
return;
25+
}
26+
_effectiveController?.removeListener(_handleControllerChanged);
27+
_effectiveController = controller;
28+
_lastSyncedControllerValue = null;
29+
if (_effectiveController != null) {
30+
_effectiveController!.addListener(_handleControllerChanged);
31+
}
32+
}
33+
34+
void _handleControllerChanged() {
35+
final controller = _effectiveController;
36+
if (controller == null || _isApplyingControllerText) {
37+
return;
38+
}
39+
final text = controller.text;
40+
if (text == _lastSyncedControllerValue) {
41+
return;
42+
}
43+
_lastSyncedControllerValue = text;
44+
onChanged(text);
45+
}
46+
47+
void _syncControllerText({bool force = false}) {
48+
final controller = _effectiveController;
49+
if (controller == null) {
50+
return;
51+
}
52+
final target = initialValue ?? '';
53+
if (!force && controller.text == target) {
54+
_lastSyncedControllerValue = controller.text;
55+
return;
56+
}
57+
_isApplyingControllerText = true;
58+
controller.value = controller.value.copyWith(
59+
text: target,
60+
selection: TextSelection.collapsed(offset: target.length),
61+
composing: TextRange.empty,
62+
);
63+
_lastSyncedControllerValue = target;
64+
_isApplyingControllerText = false;
65+
}
66+
67+
@override
68+
void initState() {
69+
super.initState();
70+
_maybeUpdateEffectiveController();
71+
_syncControllerText(force: true);
72+
}
73+
74+
@override
75+
void didChangeDependencies() {
76+
super.didChangeDependencies();
77+
_maybeUpdateEffectiveController();
78+
_syncControllerText();
79+
}
80+
81+
@override
82+
void didUpdateWidget(covariant T oldWidget) {
83+
super.didUpdateWidget(oldWidget);
84+
_maybeUpdateEffectiveController();
85+
_syncControllerText(force: true);
86+
}
87+
88+
@override
89+
void dispose() {
90+
_effectiveController?.removeListener(_handleControllerChanged);
91+
_effectiveController = null;
92+
super.dispose();
93+
}
94+
1495
@override
1596
Widget buildFormField(BuildContext context) {
1697
final inputResolver = stringResolver.inputs;
1798
final hintText = widget.hintText == null
1899
? widget.hintTextKey?.resolve(context, inputResolver)
19100
: widget.hintText!;
101+
_maybeUpdateEffectiveController();
20102
return ValueListenableBuilder<bool>(
21103
valueListenable: AuthenticatorFormState.of(
22104
context,
23105
).obscureTextToggleValue,
24106
builder: (BuildContext context, bool toggleObscureText, Widget? _) {
25107
final obscureText = this.obscureText && toggleObscureText;
108+
_syncControllerText();
26109
return TextFormField(
27110
style: enabled
28111
? null
29112
: TextStyle(color: Theme.of(context).disabledColor),
30-
initialValue: initialValue,
113+
controller: _effectiveController,
114+
initialValue:
115+
_effectiveController == null ? initialValue : null,
31116
enabled: enabled,
32117
validator: widget.validatorOverride ?? validator,
33118
onChanged: onChanged,

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

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,108 @@ mixin AuthenticatorUsernameField<
1515
T extends AuthenticatorFormField<FieldType, UsernameInput>
1616
>
1717
on AuthenticatorFormFieldState<FieldType, UsernameInput, T> {
18+
TextEditingController? _controller;
19+
UsernameType? _controllerUsernameType;
20+
bool _applyingControllerText = false;
21+
String? _lastSyncedText;
22+
23+
@protected
24+
TextEditingController? get textController => null;
25+
26+
void _updateController() {
27+
final controller = textController;
28+
final type = selectedUsernameType;
29+
final shouldListen = type != UsernameType.phoneNumber;
30+
31+
if (identical(controller, _controller) && type == _controllerUsernameType) {
32+
if (!shouldListen && _controller != null) {
33+
_controller!.removeListener(_handleControllerChanged);
34+
}
35+
return;
36+
}
37+
38+
if (_controller != null) {
39+
_controller!.removeListener(_handleControllerChanged);
40+
}
41+
42+
_controller = controller;
43+
_controllerUsernameType = type;
44+
_lastSyncedText = null;
45+
46+
if (_controller != null && shouldListen) {
47+
_controller!.addListener(_handleControllerChanged);
48+
}
49+
}
50+
51+
void _handleControllerChanged() {
52+
final controller = _controller;
53+
if (controller == null || _applyingControllerText) {
54+
return;
55+
}
56+
57+
final text = controller.text;
58+
if (text == _lastSyncedText) {
59+
return;
60+
}
61+
62+
_lastSyncedText = text;
63+
_applyingControllerText = true;
64+
try {
65+
onChanged(UsernameInput(type: selectedUsernameType, username: text));
66+
} finally {
67+
_applyingControllerText = false;
68+
}
69+
}
70+
71+
void _syncControllerText({bool force = false}) {
72+
if (_controller == null || selectedUsernameType == UsernameType.phoneNumber) {
73+
return;
74+
}
75+
76+
final target = initialValue?.username ?? '';
77+
if (!force && _controller!.text == target) {
78+
_lastSyncedText = _controller!.text;
79+
return;
80+
}
81+
82+
_applyingControllerText = true;
83+
_controller!.value = _controller!.value.copyWith(
84+
text: target,
85+
selection: TextSelection.collapsed(offset: target.length),
86+
composing: TextRange.empty,
87+
);
88+
_lastSyncedText = target;
89+
_applyingControllerText = false;
90+
}
91+
92+
@override
93+
void initState() {
94+
super.initState();
95+
_updateController();
96+
_syncControllerText(force: true);
97+
}
98+
99+
@override
100+
void didChangeDependencies() {
101+
super.didChangeDependencies();
102+
_updateController();
103+
_syncControllerText();
104+
}
105+
106+
@override
107+
void didUpdateWidget(covariant T oldWidget) {
108+
super.didUpdateWidget(oldWidget);
109+
_updateController();
110+
_syncControllerText(force: true);
111+
}
112+
113+
@override
114+
void dispose() {
115+
_controller?.removeListener(_handleControllerChanged);
116+
_controller = null;
117+
super.dispose();
118+
}
119+
18120
@override
19121
UsernameInput? get initialValue {
20122
return UsernameInput(type: selectedUsernameType, username: state.username);
@@ -220,11 +322,17 @@ mixin AuthenticatorUsernameField<
220322
errorMaxLines: errorMaxLines,
221323
initialValue: state.username,
222324
autofillHints: autofillHints,
325+
controller: textController,
223326
);
224327
}
328+
329+
_updateController();
330+
_syncControllerText();
331+
225332
return TextFormField(
226333
style: enabled ? null : TextStyle(color: Theme.of(context).disabledColor),
227-
initialValue: initialValue?.username,
334+
controller: _controller,
335+
initialValue: _controller == null ? initialValue?.username : null,
228336
enabled: enabled,
229337
validator: validator,
230338
onChanged: onChanged,

0 commit comments

Comments
 (0)