Skip to content

Commit e9fccdf

Browse files
chrisbobbegnprice
authored andcommitted
login [nfc]: Pull username-password form into separate widget
To make room for web-auth buttons on the same page. For now, we'll plan to put the username/password form and all the web-auth buttons together on the same page. We don't have to keep it this way forever, but it has some advantages: - My feeling is that most web apps do this, and I don't see an obvious reason to deviate from that pattern here, probably because: - In the common case of username/password authentication, users won't need to tap a "Sign in with password" button and then context-switch to a new page. The form is just there, right after your realm URL has been accepted.
1 parent 49fcd76 commit e9fccdf

File tree

1 file changed

+91
-57
lines changed

1 file changed

+91
-57
lines changed

lib/widgets/login.dart

Lines changed: 91 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -245,17 +245,34 @@ class PasswordLoginPage extends StatefulWidget {
245245
}
246246

247247
class _PasswordLoginPageState extends State<PasswordLoginPage> {
248-
final GlobalKey<FormFieldState<String>> _usernameKey = GlobalKey();
249-
final GlobalKey<FormFieldState<String>> _passwordKey = GlobalKey();
248+
bool _inProgress = false;
250249

251-
bool _obscurePassword = true;
252-
void _handlePasswordVisibilityPress() {
253-
setState(() {
254-
_obscurePassword = !_obscurePassword;
255-
});
256-
}
250+
Future<void> _tryInsertAccountAndNavigate({
251+
required String email,
252+
required String apiKey,
253+
required int userId,
254+
}) async {
255+
final globalStore = GlobalStoreWidget.of(context);
256+
// TODO(#108): give feedback to user on SQL exception, like dupe realm+user
257+
final accountId = await globalStore.insertAccount(AccountsCompanion.insert(
258+
realmUrl: widget.serverSettings.realmUrl,
259+
email: email,
260+
apiKey: apiKey,
261+
userId: userId,
262+
zulipFeatureLevel: widget.serverSettings.zulipFeatureLevel,
263+
zulipVersion: widget.serverSettings.zulipVersion,
264+
zulipMergeBase: Value(widget.serverSettings.zulipMergeBase),
265+
));
257266

258-
bool _inProgress = false;
267+
if (!mounted) {
268+
return;
269+
}
270+
271+
Navigator.of(context).pushAndRemoveUntil(
272+
HomePage.buildRoute(accountId: accountId),
273+
(route) => (route is! _LoginSequenceRoute),
274+
);
275+
}
259276

260277
Future<int> _getUserId(String email, apiKey) async {
261278
final connection = ApiConnection.live( // TODO make this widget testable
@@ -265,9 +282,51 @@ class _PasswordLoginPageState extends State<PasswordLoginPage> {
265282
return (await getOwnUser(connection)).userId;
266283
}
267284

285+
@override
286+
Widget build(BuildContext context) {
287+
assert(!PerAccountStoreWidget.debugExistsOf(context));
288+
final zulipLocalizations = ZulipLocalizations.of(context);
289+
290+
return Scaffold(
291+
appBar: AppBar(title: Text(zulipLocalizations.loginPageTitle),
292+
bottom: _inProgress
293+
? const PreferredSize(preferredSize: Size.fromHeight(4),
294+
child: LinearProgressIndicator(minHeight: 4)) // 4 restates default
295+
: null),
296+
body: SafeArea(
297+
minimum: const EdgeInsets.all(8),
298+
child: Center(
299+
child: ConstrainedBox(
300+
constraints: const BoxConstraints(maxWidth: 400),
301+
child: _UsernamePasswordForm(loginPageState: this)))));
302+
}
303+
}
304+
305+
class _UsernamePasswordForm extends StatefulWidget {
306+
const _UsernamePasswordForm({required this.loginPageState});
307+
308+
final _PasswordLoginPageState loginPageState;
309+
310+
@override
311+
State<_UsernamePasswordForm> createState() => _UsernamePasswordFormState();
312+
}
313+
314+
class _UsernamePasswordFormState extends State<_UsernamePasswordForm> {
315+
final GlobalKey<FormFieldState<String>> _usernameKey = GlobalKey();
316+
final GlobalKey<FormFieldState<String>> _passwordKey = GlobalKey();
317+
318+
bool _obscurePassword = true;
319+
void _handlePasswordVisibilityPress() {
320+
setState(() {
321+
_obscurePassword = !_obscurePassword;
322+
});
323+
}
324+
268325
void _submit() async {
326+
final serverSettings = widget.loginPageState.widget.serverSettings;
327+
269328
final context = _usernameKey.currentContext!;
270-
final realmUrl = widget.serverSettings.realmUrl;
329+
final realmUrl = serverSettings.realmUrl;
271330
final usernameFieldState = _usernameKey.currentState!;
272331
final passwordFieldState = _passwordKey.currentState!;
273332
final usernameValid = usernameFieldState.validate(); // Side effect: on-field error text
@@ -278,15 +337,15 @@ class _PasswordLoginPageState extends State<PasswordLoginPage> {
278337
final String username = usernameFieldState.value!;
279338
final String password = passwordFieldState.value!;
280339

281-
setState(() {
282-
_inProgress = true;
340+
widget.loginPageState.setState(() {
341+
widget.loginPageState._inProgress = true;
283342
});
284343
try {
285344
final FetchApiKeyResult result;
286345
try {
287346
result = await fetchApiKey(
288347
realmUrl: realmUrl,
289-
zulipFeatureLevel: widget.serverSettings.zulipFeatureLevel,
348+
zulipFeatureLevel: serverSettings.zulipFeatureLevel,
290349
username: username, password: password);
291350
} on ApiRequestException catch (e) {
292351
if (!context.mounted) return;
@@ -304,46 +363,32 @@ class _PasswordLoginPageState extends State<PasswordLoginPage> {
304363
}
305364

306365
// TODO(server-7): Rely on user_id from fetchApiKey.
307-
final int userId = result.userId ?? await _getUserId(result.email, result.apiKey);
366+
final int userId = result.userId
367+
?? await widget.loginPageState._getUserId(result.email, result.apiKey);
308368
// https://github.com/dart-lang/linter/issues/4007
309369
// ignore: use_build_context_synchronously
310370
if (!context.mounted) {
311371
return;
312372
}
313373

314-
final globalStore = GlobalStoreWidget.of(context);
315-
// TODO(#108): give feedback to user on SQL exception, like dupe realm+user
316-
final accountId = await globalStore.insertAccount(AccountsCompanion.insert(
317-
realmUrl: realmUrl,
374+
await widget.loginPageState._tryInsertAccountAndNavigate(
318375
email: result.email,
319376
apiKey: result.apiKey,
320377
userId: userId,
321-
zulipFeatureLevel: widget.serverSettings.zulipFeatureLevel,
322-
zulipVersion: widget.serverSettings.zulipVersion,
323-
zulipMergeBase: Value(widget.serverSettings.zulipMergeBase),
324-
));
325-
// https://github.com/dart-lang/linter/issues/4007
326-
// ignore: use_build_context_synchronously
327-
if (!context.mounted) {
328-
return;
329-
}
330-
331-
Navigator.of(context).pushAndRemoveUntil(
332-
HomePage.buildRoute(accountId: accountId),
333-
(route) => (route is! _LoginSequenceRoute),
334378
);
335379
} finally {
336-
setState(() {
337-
_inProgress = false;
380+
widget.loginPageState.setState(() {
381+
widget.loginPageState._inProgress = false;
338382
});
339383
}
340384
}
341385

342386
@override
343387
Widget build(BuildContext context) {
344388
assert(!PerAccountStoreWidget.debugExistsOf(context));
389+
final serverSettings = widget.loginPageState.widget.serverSettings;
345390
final zulipLocalizations = ZulipLocalizations.of(context);
346-
final requireEmailFormatUsernames = widget.serverSettings.requireEmailFormatUsernames;
391+
final requireEmailFormatUsernames = serverSettings.requireEmailFormatUsernames;
347392

348393
final usernameField = TextFormField(
349394
key: _usernameKey,
@@ -399,28 +444,17 @@ class _PasswordLoginPageState extends State<PasswordLoginPage> {
399444
selectedIcon: const Icon(Icons.visibility_off),
400445
)));
401446

402-
return Scaffold(
403-
appBar: AppBar(title: Text(zulipLocalizations.loginPageTitle),
404-
bottom: _inProgress
405-
? const PreferredSize(preferredSize: Size.fromHeight(4),
406-
child: LinearProgressIndicator(minHeight: 4)) // 4 restates default
407-
: null),
408-
body: SafeArea(
409-
minimum: const EdgeInsets.all(8),
410-
child: Center(
411-
child: ConstrainedBox(
412-
constraints: const BoxConstraints(maxWidth: 400),
413-
child: Form(
414-
// TODO(#110) Try to highlight CZO / Zulip Cloud realms in autofill
415-
child: AutofillGroup(
416-
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
417-
usernameField,
418-
const SizedBox(height: 8),
419-
passwordField,
420-
const SizedBox(height: 8),
421-
ElevatedButton(
422-
onPressed: _inProgress ? null : _submit,
423-
child: Text(zulipLocalizations.loginFormSubmitLabel)),
424-
])))))));
447+
return Form(
448+
// TODO(#110) Try to highlight CZO / Zulip Cloud realms in autofill
449+
child: AutofillGroup(
450+
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
451+
usernameField,
452+
const SizedBox(height: 8),
453+
passwordField,
454+
const SizedBox(height: 8),
455+
ElevatedButton(
456+
onPressed: widget.loginPageState._inProgress ? null : _submit,
457+
child: Text(zulipLocalizations.loginFormSubmitLabel)),
458+
])));
425459
}
426460
}

0 commit comments

Comments
 (0)