Reintroduce legacy sign in using username and password

This commit is contained in:
Ming Ming 2024-10-19 00:52:38 +08:00
parent bfcf0c4586
commit 69dada413b
9 changed files with 419 additions and 31 deletions

View file

@ -1501,6 +1501,10 @@
"homeTabMapBrowser": "Map", "homeTabMapBrowser": "Map",
"mapBrowserSetDefaultDateRangeButton": "Set as default", "mapBrowserSetDefaultDateRangeButton": "Set as default",
"todayText": "Today", "todayText": "Today",
"alternativeSignIn": "Alternative sign in with username and password",
"@alternativeSignIn": {
"description": "Sign in using username and password instead of the recommended Nextcloud login flow"
},
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues", "errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
"@errorUnauthenticated": { "@errorUnauthenticated": {

View file

@ -260,6 +260,7 @@
"homeTabMapBrowser", "homeTabMapBrowser",
"mapBrowserSetDefaultDateRangeButton", "mapBrowserSetDefaultDateRangeButton",
"todayText", "todayText",
"alternativeSignIn",
"errorUnauthenticated", "errorUnauthenticated",
"errorDisconnected", "errorDisconnected",
"errorLocked", "errorLocked",
@ -272,7 +273,8 @@
"cs": [ "cs": [
"mapBrowserSetDefaultDateRangeButton", "mapBrowserSetDefaultDateRangeButton",
"todayText" "todayText",
"alternativeSignIn"
], ],
"de": [ "de": [
@ -292,7 +294,8 @@
"searchLandingPeopleListEmptyText2", "searchLandingPeopleListEmptyText2",
"mapBrowserDateRangeLabel", "mapBrowserDateRangeLabel",
"mapBrowserSetDefaultDateRangeButton", "mapBrowserSetDefaultDateRangeButton",
"todayText" "todayText",
"alternativeSignIn"
], ],
"el": [ "el": [
@ -440,12 +443,14 @@
"mapBrowserDateRangeCustom", "mapBrowserDateRangeCustom",
"homeTabMapBrowser", "homeTabMapBrowser",
"mapBrowserSetDefaultDateRangeButton", "mapBrowserSetDefaultDateRangeButton",
"todayText" "todayText",
"alternativeSignIn"
], ],
"es": [ "es": [
"mapBrowserSetDefaultDateRangeButton", "mapBrowserSetDefaultDateRangeButton",
"todayText" "todayText",
"alternativeSignIn"
], ],
"fi": [ "fi": [
@ -484,7 +489,8 @@
"mapBrowserDateRangeCustom", "mapBrowserDateRangeCustom",
"homeTabMapBrowser", "homeTabMapBrowser",
"mapBrowserSetDefaultDateRangeButton", "mapBrowserSetDefaultDateRangeButton",
"todayText" "todayText",
"alternativeSignIn"
], ],
"fr": [ "fr": [
@ -523,7 +529,8 @@
"mapBrowserDateRangeCustom", "mapBrowserDateRangeCustom",
"homeTabMapBrowser", "homeTabMapBrowser",
"mapBrowserSetDefaultDateRangeButton", "mapBrowserSetDefaultDateRangeButton",
"todayText" "todayText",
"alternativeSignIn"
], ],
"it": [ "it": [
@ -567,7 +574,8 @@
"mapBrowserDateRangeCustom", "mapBrowserDateRangeCustom",
"homeTabMapBrowser", "homeTabMapBrowser",
"mapBrowserSetDefaultDateRangeButton", "mapBrowserSetDefaultDateRangeButton",
"todayText" "todayText",
"alternativeSignIn"
], ],
"nl": [ "nl": [
@ -948,6 +956,7 @@
"homeTabMapBrowser", "homeTabMapBrowser",
"mapBrowserSetDefaultDateRangeButton", "mapBrowserSetDefaultDateRangeButton",
"todayText", "todayText",
"alternativeSignIn",
"errorUnauthenticated", "errorUnauthenticated",
"errorDisconnected", "errorDisconnected",
"errorLocked", "errorLocked",
@ -998,7 +1007,8 @@
"mapBrowserDateRangeCustom", "mapBrowserDateRangeCustom",
"homeTabMapBrowser", "homeTabMapBrowser",
"mapBrowserSetDefaultDateRangeButton", "mapBrowserSetDefaultDateRangeButton",
"todayText" "todayText",
"alternativeSignIn"
], ],
"pt": [ "pt": [
@ -1057,7 +1067,8 @@
"mapBrowserDateRangeCustom", "mapBrowserDateRangeCustom",
"homeTabMapBrowser", "homeTabMapBrowser",
"mapBrowserSetDefaultDateRangeButton", "mapBrowserSetDefaultDateRangeButton",
"todayText" "todayText",
"alternativeSignIn"
], ],
"ru": [ "ru": [
@ -1096,7 +1107,12 @@
"mapBrowserDateRangeCustom", "mapBrowserDateRangeCustom",
"homeTabMapBrowser", "homeTabMapBrowser",
"mapBrowserSetDefaultDateRangeButton", "mapBrowserSetDefaultDateRangeButton",
"todayText" "todayText",
"alternativeSignIn"
],
"tr": [
"alternativeSignIn"
], ],
"zh": [ "zh": [
@ -1166,7 +1182,8 @@
"mapBrowserDateRangeCustom", "mapBrowserDateRangeCustom",
"homeTabMapBrowser", "homeTabMapBrowser",
"mapBrowserSetDefaultDateRangeButton", "mapBrowserSetDefaultDateRangeButton",
"todayText" "todayText",
"alternativeSignIn"
], ],
"zh_Hant": [ "zh_Hant": [
@ -1330,6 +1347,7 @@
"mapBrowserDateRangeCustom", "mapBrowserDateRangeCustom",
"homeTabMapBrowser", "homeTabMapBrowser",
"mapBrowserSetDefaultDateRangeButton", "mapBrowserSetDefaultDateRangeButton",
"todayText" "todayText",
"alternativeSignIn"
] ]
} }

View file

@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/widget/measure.dart';
class ExpandableContainer extends StatefulWidget {
const ExpandableContainer({
super.key,
required this.isShow,
required this.child,
});
@override
State<StatefulWidget> createState() => ExpandableContainerState();
final bool isShow;
final Widget child;
}
class ExpandableContainerState extends State<ExpandableContainer>
with TickerProviderStateMixin {
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: k.animationDurationNormal,
vsync: this,
value: 0,
);
_animation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
void didUpdateWidget(covariant ExpandableContainer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.isShow != widget.isShow) {
if (widget.isShow) {
_animationController.animateTo(1);
} else {
_animationController.animateBack(0);
}
}
}
@override
Widget build(BuildContext context) {
return MatrixTransition(
animation: _animation,
onTransform: (animationValue) => Matrix4.identity()
..translate(0.0, -(_size.height / 2) * (1 - animationValue), 0.0)
..scale(1.0, animationValue, 1.0),
child: MeasureSize(
onChange: (size) => setState(() {
_size = size;
}),
child: widget.child,
),
);
}
late AnimationController _animationController;
late Animation<double> _animation;
var _size = Size.zero;
}

View file

@ -12,16 +12,17 @@ import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/entity/pref.dart'; import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/entity/pref_util.dart' as pref_util; import 'package:nc_photos/entity/pref_util.dart' as pref_util;
import 'package:nc_photos/exception_event.dart'; import 'package:nc_photos/exception_event.dart';
import 'package:nc_photos/legacy/connect.dart' as legacy;
import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme.dart';
import 'package:nc_photos/widget/app_intermediate_circular_progress_indicator.dart'; import 'package:nc_photos/widget/app_intermediate_circular_progress_indicator.dart';
import 'package:nc_photos/widget/connect.dart'; import 'package:nc_photos/widget/connect.dart';
import 'package:nc_photos/widget/expandable_container.dart';
import 'package:nc_photos/widget/home.dart'; import 'package:nc_photos/widget/home.dart';
import 'package:nc_photos/widget/page_visibility_mixin.dart'; import 'package:nc_photos/widget/page_visibility_mixin.dart';
import 'package:nc_photos/widget/root_picker.dart'; import 'package:nc_photos/widget/root_picker.dart';
import 'package:np_codegen/np_codegen.dart'; import 'package:np_codegen/np_codegen.dart';
import 'package:np_collection/np_collection.dart'; import 'package:np_collection/np_collection.dart';
import 'package:np_common/unique.dart';
import 'package:np_db/np_db.dart'; import 'package:np_db/np_db.dart';
import 'package:np_string/np_string.dart'; import 'package:np_string/np_string.dart';
import 'package:to_string/to_string.dart'; import 'package:to_string/to_string.dart';
@ -49,7 +50,6 @@ class SignIn extends StatelessWidget {
} }
} }
@npLog
class _WrappedSignIn extends StatefulWidget { class _WrappedSignIn extends StatefulWidget {
const _WrappedSignIn(); const _WrappedSignIn();
@ -57,6 +57,7 @@ class _WrappedSignIn extends StatefulWidget {
State<StatefulWidget> createState() => _WrappedSignInState(); State<StatefulWidget> createState() => _WrappedSignInState();
} }
@npLog
class _WrappedSignInState extends State<_WrappedSignIn> class _WrappedSignInState extends State<_WrappedSignIn>
with RouteAware, PageVisibilityMixin { with RouteAware, PageVisibilityMixin {
@override @override
@ -81,10 +82,18 @@ class _WrappedSignInState extends State<_WrappedSignIn>
body: MultiBlocListener( body: MultiBlocListener(
listeners: [ listeners: [
_BlocListenerT( _BlocListenerT(
selector: (state) => state.connectUri, selector: (state) => state.connectArg,
listener: (context, connectUri) { listener: (context, connectArg) {
if (connectUri != null) { if (connectArg == null) {
_onConnect(context, connectUri.value); return;
}
if (connectArg.username != null &&
connectArg.password != null) {
_onLegacyConnect(context, connectArg);
} else {
final uri = Uri.parse(
"${connectArg.scheme}://${connectArg.address}");
_onConnect(context, uri);
} }
}, },
), ),
@ -145,6 +154,39 @@ class _WrappedSignInState extends State<_WrappedSignIn>
// we've got a good account // we've got a good account
context.addEvent(_SetConnectedAccount(account)); context.addEvent(_SetConnectedAccount(account));
} }
Future<void> _onLegacyConnect(BuildContext context, _ConnectArg arg) async {
Account? account = Account(
id: Account.newId(),
scheme: arg.scheme,
address: arg.address,
userId: arg.username!.toCi(),
username2: arg.username!,
password: arg.password!,
roots: [""],
);
_log.info("[_onLegacyConnect] Try connecting with account: $account");
account = await Navigator.pushNamed<Account>(
context,
legacy.Connect.routeName,
arguments: legacy.ConnectArguments(account),
);
if (account == null) {
// connection failed
return;
}
account = await Navigator.pushNamed<Account>(
context,
RootPicker.routeName,
arguments: RootPickerArguments(account),
);
if (account == null) {
// ???
return;
}
// we've got a good account
context.addEvent(_SetConnectedAccount(account));
}
} }
// typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; // typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;

View file

@ -16,10 +16,14 @@ abstract class $_StateCopyWithWorker {
_State call( _State call(
{_Scheme? scheme, {_Scheme? scheme,
String? serverUrl, String? serverUrl,
Unique<Uri>? connectUri, String? username,
String? password,
bool? shouldObscurePassword,
_ConnectArg? connectArg,
Account? connectedAccount, Account? connectedAccount,
bool? isConnecting, bool? isConnecting,
bool? isCompleted, bool? isCompleted,
bool? isAltMode,
ExceptionEvent? error}); ExceptionEvent? error});
} }
@ -30,22 +34,31 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
_State call( _State call(
{dynamic scheme, {dynamic scheme,
dynamic serverUrl, dynamic serverUrl,
dynamic connectUri = copyWithNull, dynamic username,
dynamic password,
dynamic shouldObscurePassword,
dynamic connectArg = copyWithNull,
dynamic connectedAccount = copyWithNull, dynamic connectedAccount = copyWithNull,
dynamic isConnecting, dynamic isConnecting,
dynamic isCompleted, dynamic isCompleted,
dynamic isAltMode,
dynamic error = copyWithNull}) { dynamic error = copyWithNull}) {
return _State( return _State(
scheme: scheme as _Scheme? ?? that.scheme, scheme: scheme as _Scheme? ?? that.scheme,
serverUrl: serverUrl as String? ?? that.serverUrl, serverUrl: serverUrl as String? ?? that.serverUrl,
connectUri: connectUri == copyWithNull username: username as String? ?? that.username,
? that.connectUri password: password as String? ?? that.password,
: connectUri as Unique<Uri>?, shouldObscurePassword:
shouldObscurePassword as bool? ?? that.shouldObscurePassword,
connectArg: connectArg == copyWithNull
? that.connectArg
: connectArg as _ConnectArg?,
connectedAccount: connectedAccount == copyWithNull connectedAccount: connectedAccount == copyWithNull
? that.connectedAccount ? that.connectedAccount
: connectedAccount as Account?, : connectedAccount as Account?,
isConnecting: isConnecting as bool? ?? that.isConnecting, isConnecting: isConnecting as bool? ?? that.isConnecting,
isCompleted: isCompleted as bool? ?? that.isCompleted, isCompleted: isCompleted as bool? ?? that.isCompleted,
isAltMode: isAltMode as bool? ?? that.isAltMode,
error: error == copyWithNull ? that.error : error as ExceptionEvent?); error: error == copyWithNull ? that.error : error as ExceptionEvent?);
} }
@ -61,11 +74,11 @@ extension $_StateCopyWith on _State {
// NpLogGenerator // NpLogGenerator
// ************************************************************************** // **************************************************************************
extension _$_WrappedSignInNpLog on _WrappedSignIn { extension _$_WrappedSignInStateNpLog on _WrappedSignInState {
// ignore: unused_element // ignore: unused_element
Logger get _log => log; Logger get _log => log;
static final log = Logger("widget.sign_in._WrappedSignIn"); static final log = Logger("widget.sign_in._WrappedSignInState");
} }
extension _$_BlocNpLog on _Bloc { extension _$_BlocNpLog on _Bloc {
@ -82,7 +95,7 @@ extension _$_BlocNpLog on _Bloc {
extension _$_StateToString on _State { extension _$_StateToString on _State {
String _$toString() { String _$toString() {
// ignore: unnecessary_string_interpolations // ignore: unnecessary_string_interpolations
return "_State {scheme: ${scheme.name}, serverUrl: $serverUrl, connectUri: $connectUri, connectedAccount: $connectedAccount, isConnecting: $isConnecting, isCompleted: $isCompleted, error: $error}"; return "_State {scheme: ${scheme.name}, serverUrl: $serverUrl, username: $username, password: $password, shouldObscurePassword: $shouldObscurePassword, connectArg: $connectArg, connectedAccount: $connectedAccount, isConnecting: $isConnecting, isCompleted: $isCompleted, isAltMode: $isAltMode, error: $error}";
} }
} }
@ -114,9 +127,44 @@ extension _$_SetConnectedAccountToString on _SetConnectedAccount {
} }
} }
extension _$_SetAltModeToString on _SetAltMode {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetAltMode {value: $value}";
}
}
extension _$_SetUsernameToString on _SetUsername {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetUsername {value: $value}";
}
}
extension _$_SetPasswordToString on _SetPassword {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetPassword {value: $value}";
}
}
extension _$_SetObscurePasswordToString on _SetObscurePassword {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetObscurePassword {value: $value}";
}
}
extension _$_SetErrorToString on _SetError { extension _$_SetErrorToString on _SetError {
String _$toString() { String _$toString() {
// ignore: unnecessary_string_interpolations // ignore: unnecessary_string_interpolations
return "_SetError {error: $error, stackTrace: $stackTrace}"; return "_SetError {error: $error, stackTrace: $stackTrace}";
} }
} }
extension _$_ConnectArgToString on _ConnectArg {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_ConnectArg {scheme: $scheme, address: $address, username: $username, password: $password}";
}
}

View file

@ -10,6 +10,10 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
on<_SetServerUrl>(_onSetServerUrl); on<_SetServerUrl>(_onSetServerUrl);
on<_Connect>(_onConnect); on<_Connect>(_onConnect);
on<_SetConnectedAccount>(_onSetConnectedAccount); on<_SetConnectedAccount>(_onSetConnectedAccount);
on<_SetAltMode>(_onSetAltMode);
on<_SetUsername>(_onSetUsername);
on<_SetPassword>(_onSetPassword);
on<_SetObscurePassword>(_onSetObscurePassword);
on<_SetError>(_onSetError); on<_SetError>(_onSetError);
} }
@ -44,9 +48,22 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
_log.info(ev); _log.info(ev);
final scheme = state.scheme.toValueString(); final scheme = state.scheme.toValueString();
final serverUrl = state.serverUrl.trim().trimRightAny("/"); final serverUrl = state.serverUrl.trim().trimRightAny("/");
final uri = Uri.parse("$scheme://$serverUrl"); final _ConnectArg arg;
_log.info("[_onConnect] Try connecting with url: $uri"); if (!state.isAltMode) {
emit(state.copyWith(connectUri: Unique(uri))); arg = _ConnectArg(
scheme: scheme,
address: serverUrl,
);
} else {
arg = _ConnectArg(
scheme: scheme,
address: serverUrl,
username: state.username,
password: state.password,
);
}
_log.info("[_onConnect] Try connecting: $arg");
emit(state.copyWith(connectArg: arg));
} }
Future<void> _onSetConnectedAccount( Future<void> _onSetConnectedAccount(
@ -65,6 +82,26 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
} }
} }
void _onSetAltMode(_SetAltMode ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(isAltMode: ev.value));
}
void _onSetUsername(_SetUsername ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(username: ev.value));
}
void _onSetPassword(_SetPassword ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(password: ev.value));
}
void _onSetObscurePassword(_SetObscurePassword ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(shouldObscurePassword: ev.value));
}
void _onSetError(_SetError ev, Emitter<_State> emit) { void _onSetError(_SetError ev, Emitter<_State> emit) {
_log.info(ev); _log.info(ev);
emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace))); emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace)));

View file

@ -6,18 +6,26 @@ class _State {
const _State({ const _State({
required this.scheme, required this.scheme,
required this.serverUrl, required this.serverUrl,
this.connectUri, required this.username,
required this.password,
required this.shouldObscurePassword,
this.connectArg,
this.connectedAccount, this.connectedAccount,
required this.isConnecting, required this.isConnecting,
required this.isCompleted, required this.isCompleted,
required this.isAltMode,
this.error, this.error,
}); });
factory _State.init() => const _State( factory _State.init() => const _State(
scheme: _Scheme.https, scheme: _Scheme.https,
serverUrl: "", serverUrl: "",
username: "",
password: "",
shouldObscurePassword: true,
isConnecting: false, isConnecting: false,
isCompleted: false, isCompleted: false,
isAltMode: false,
); );
@override @override
@ -25,11 +33,16 @@ class _State {
final _Scheme scheme; final _Scheme scheme;
final String serverUrl; final String serverUrl;
final Unique<Uri>? connectUri; final String username;
final String password;
final bool shouldObscurePassword;
final _ConnectArg? connectArg;
final Account? connectedAccount; final Account? connectedAccount;
final bool isConnecting; final bool isConnecting;
final bool isCompleted; final bool isCompleted;
final bool isAltMode;
final ExceptionEvent? error; final ExceptionEvent? error;
} }
@ -73,6 +86,46 @@ class _SetConnectedAccount implements _Event {
final Account value; final Account value;
} }
@toString
class _SetAltMode implements _Event {
const _SetAltMode(this.value);
@override
String toString() => _$toString();
final bool value;
}
@toString
class _SetUsername implements _Event {
const _SetUsername(this.value);
@override
String toString() => _$toString();
final String value;
}
@toString
class _SetPassword implements _Event {
const _SetPassword(this.value);
@override
String toString() => _$toString();
final String value;
}
@toString
class _SetObscurePassword implements _Event {
const _SetObscurePassword(this.value);
@override
String toString() => _$toString();
final bool value;
}
@toString @toString
class _SetError implements _Event { class _SetError implements _Event {
const _SetError(this.error, [this.stackTrace]); const _SetError(this.error, [this.stackTrace]);

View file

@ -14,3 +14,21 @@ enum _Scheme {
} }
} }
} }
@toString
class _ConnectArg {
const _ConnectArg({
required this.scheme,
required this.address,
this.username,
this.password,
});
@override
String toString() => _$toString();
final String scheme;
final String address;
final String? username;
final String? password;
}

View file

@ -173,6 +173,35 @@ class _SignInBody extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(height: 8),
Stack(
children: [
_BlocSelector(
selector: (state) => state.isAltMode,
builder: (context, isAltMode) => ExpandableContainer(
isShow: isAltMode,
child: const _LegacySignInForm(),
),
),
_BlocSelector(
selector: (state) => state.isAltMode,
builder: (context, isAltMode) => Visibility(
visible: !isAltMode,
child: InkWell(
onTap: () {
context.addEvent(const _SetAltMode(true));
},
child: Text(
L10n.global().alternativeSignIn,
style: const TextStyle(
decoration: TextDecoration.underline,
),
),
),
),
),
],
),
], ],
), ),
); );
@ -228,3 +257,70 @@ class _ServerUrlInput extends StatelessWidget {
); );
} }
} }
class _LegacySignInForm extends StatelessWidget {
const _LegacySignInForm();
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
decoration: InputDecoration(
hintText: L10n.global().usernameInputHint,
),
keyboardType: TextInputType.text,
validator: (value) {
if (!context.state.isAltMode) {
return null;
}
if (value!.trim().isEmpty) {
return L10n.global().usernameInputInvalidEmpty;
}
return null;
},
onChanged: (value) {
context.addEvent(_SetUsername(value));
},
),
const SizedBox(height: 8),
_BlocSelector(
selector: (state) => state.shouldObscurePassword,
builder: (context, shouldObscurePassword) => TextFormField(
decoration: InputDecoration(
hintText: L10n.global().passwordInputHint,
suffixIcon: shouldObscurePassword
? IconButton(
icon: const Icon(Icons.visibility_off_outlined),
onPressed: () {
context.addEvent(const _SetObscurePassword(false));
},
)
: IconButton(
icon: const Icon(Icons.visibility_outlined),
onPressed: () {
context.addEvent(const _SetObscurePassword(true));
},
),
),
keyboardType: TextInputType.text,
obscureText: shouldObscurePassword,
validator: (value) {
if (!context.state.isAltMode) {
return null;
}
if (value!.trim().isEmpty) {
return L10n.global().passwordInputInvalidEmpty;
}
return null;
},
onChanged: (value) {
context.addEvent(_SetPassword(value));
},
),
),
],
);
}
}