diff --git a/client/lib/ui/login/login_page.dart b/client/lib/ui/login/login_page.dart index 7e971144..6ec66f68 100644 --- a/client/lib/ui/login/login_page.dart +++ b/client/lib/ui/login/login_page.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:phylum/ui/common/logo_row.dart'; import 'package:phylum/ui/login/instance_url_fragment.dart'; import 'package:phylum/ui/login/password_login_fragment.dart'; +import 'package:phylum/util/dialogs.dart'; +import 'package:uri/uri.dart'; import 'login_request.dart'; @@ -20,7 +22,9 @@ class _LoginPageState extends State { @override void initState() { super.initState(); - _instanceUrl = widget.fixedInstanceUrl; + if (widget.fixedInstanceUrl != null) { + _instanceUrl = _cleanUrl(widget.fixedInstanceUrl!); + } } @override @@ -53,8 +57,42 @@ class _LoginPageState extends State { Widget _buildBody(BuildContext context) { final instanceUrl = _instanceUrl; if (instanceUrl == null) { - return InstanceUrlFragment(onSubmitted: (url) => setState(() => _instanceUrl = url)); + return InstanceUrlFragment(onSubmitted: (url) { + setState(() => _instanceUrl = _cleanUrl(url)); + }); } - return PasswordLoginFragment(onSubmitted: (email, password) => performLogin(context, instanceUrl, email, password)); + return PasswordLoginFragment( + onSubmitted: (email, password) => performLogin(context, instanceUrl, email, password), + onBackPressed: widget.fixedInstanceUrl == null ? () => setState(() => _instanceUrl = null) : null, + onForgotPassword: (email) async { + if (email.isEmpty) { + showAlertDialog( + context, + message: 'Please enter email address to reset password', + barrierDismissible: true, + ); + return; + } + final confirm = await showAlertDialog( + context, + title: 'Reset Password', + message: 'This will request a password reset for $email. Do you want to continue?', + negativeText: 'No', + positiveText: 'Yes', + barrierDismissible: true, + ) ?? + false; + if (!confirm || !context.mounted) return; + }, + ); + } + + Uri _cleanUrl(Uri url) { + if (url.path.endsWith('/')) { + final builder = UriBuilder.fromUri(url); + builder.path = url.pathSegments.where((s) => s.isNotEmpty).join('/'); + return builder.build(); + } + return url; } } diff --git a/client/lib/ui/login/login_request.dart b/client/lib/ui/login/login_request.dart index f2076765..64002468 100644 --- a/client/lib/ui/login/login_request.dart +++ b/client/lib/ui/login/login_request.dart @@ -11,8 +11,6 @@ import 'package:uri/uri.dart'; void performLogin(BuildContext context, Uri instanceUri, String email, String password) async { final builder = UriBuilder.fromUri(instanceUri); - builder.path = builder.path.split('/').where((s) => s.isNotEmpty).join('/'); - final cleanServerUrl = builder.build(); showProgressDialog( context, @@ -40,7 +38,7 @@ void performLogin(BuildContext context, Uri instanceUri, String email, String pa ); } else { final accountManager = context.read>(); - final account = await PhylumAccount.createFromLoginResponse(cleanServerUrl, json.cast()); + final account = await PhylumAccount.createFromLoginResponse(instanceUri, json.cast()); accountManager.addAccount(account); } } diff --git a/client/lib/ui/login/password_login_fragment.dart b/client/lib/ui/login/password_login_fragment.dart index bcfbed72..b49c4a16 100644 --- a/client/lib/ui/login/password_login_fragment.dart +++ b/client/lib/ui/login/password_login_fragment.dart @@ -2,8 +2,15 @@ import 'package:flutter/material.dart'; class PasswordLoginFragment extends StatefulWidget { final Function(String, String) onSubmitted; + final Function()? onBackPressed; + final Function(String) onForgotPassword; - const PasswordLoginFragment({super.key, required this.onSubmitted}); + const PasswordLoginFragment({ + super.key, + required this.onSubmitted, + required this.onBackPressed, + required this.onForgotPassword, + }); @override State createState() => _PasswordLoginFragmentState(); @@ -16,6 +23,7 @@ class _PasswordLoginFragmentState extends State { @override Widget build(BuildContext context) { return Column( + spacing: 12.0, children: [ TextField( decoration: const InputDecoration( @@ -31,13 +39,33 @@ class _PasswordLoginFragmentState extends State { obscureText: true, keyboardType: TextInputType.visiblePassword, onChanged: (value) => setState(() => _password = value), - onSubmitted: _email.isNotEmpty ? (value) => widget.onSubmitted(_email, _password) : null, + onSubmitted: _email.isEmpty ? null : (value) => widget.onSubmitted(_email, _password), + ), + TextButton( + onPressed: () => widget.onForgotPassword(_email), + child: Text( + 'Forgot Password', + style: TextStyle(color: Theme.of(context).colorScheme.primary), + )), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.end, + spacing: 12.0, + children: [ + if (widget.onBackPressed != null) + TextButton( + onPressed: widget.onBackPressed, + child: Text( + 'Back', + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + ), + ElevatedButton( + onPressed: _email.isEmpty || _password.isEmpty ? null : () => widget.onSubmitted(_email, _password), + child: Text('Login'), + ), + ], ), - TextButton(onPressed: () {}, child: Text('Forgot Password')), - ElevatedButton( - onPressed: _email.isEmpty || _password.isEmpty ? null : () => widget.onSubmitted(_email, _password), - child: Text('Login'), - ) ], ); }