added support for multiple accounts, guardian login bugfixes, ui changes
This commit is contained in:
+118
-66
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -8,8 +9,8 @@ import '../../auth_link/guardian_link_listener.dart';
|
||||
import '../../auth_link/guardian_login_link.dart';
|
||||
import '../../background/widget_background_task.dart';
|
||||
import '../../session/session_lifecycle.dart';
|
||||
import '../../session/session_manager.dart';
|
||||
import '../../state/app/modules/account/bloc/account_bloc.dart';
|
||||
import '../../state/app/modules/account/bloc/account_state.dart';
|
||||
import '../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../storage/dev_tools_settings.dart';
|
||||
import '../../storage/settings.dart' as model;
|
||||
@@ -26,7 +27,10 @@ import 'widgets/login_branding.dart';
|
||||
import 'widgets/login_card.dart';
|
||||
|
||||
class Login extends StatefulWidget {
|
||||
const Login({super.key});
|
||||
/// Signs in another account while one is active; offers to go back.
|
||||
final bool addingAccount;
|
||||
|
||||
const Login({super.key, this.addingAccount = false});
|
||||
|
||||
@override
|
||||
State<Login> createState() => _LoginState();
|
||||
@@ -85,7 +89,7 @@ class _LoginState extends State<Login> with SingleTickerProviderStateMixin {
|
||||
return;
|
||||
}
|
||||
final signedIn = await _guardianController.submitLink(link);
|
||||
if (signedIn && mounted) _onLoginSuccess();
|
||||
if (signedIn && mounted) await _onLoginSuccess();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -104,95 +108,141 @@ class _LoginState extends State<Login> with SingleTickerProviderStateMixin {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onLoginSuccess() {
|
||||
Future<void> _onLoginSuccess() async {
|
||||
Haptics.heavyAccent();
|
||||
final accountBloc = context.read<AccountBloc>();
|
||||
// Fade the login content out before handing over to the post-login splash.
|
||||
// Both share the red backdrop, so this reads as one continuous transition
|
||||
// instead of an abrupt swap.
|
||||
final finished = SessionLifecycle.finishLogin();
|
||||
await _fade.reverse().orCancel.onError<TickerCanceled>((_, _) {});
|
||||
String? accountId;
|
||||
try {
|
||||
accountId = await finished;
|
||||
} on Object catch (e) {
|
||||
log('Login: finishing failed: $e');
|
||||
accountId = SessionManager().activeAccount?.id;
|
||||
}
|
||||
// Re-register the periodic refresh (cancelAll runs on logout) and kick
|
||||
// off an immediate one-off so the widget populates within seconds
|
||||
// instead of waiting up to 30 minutes for the next periodic slot.
|
||||
unawaited(WidgetBackgroundTask.initialize());
|
||||
unawaited(WidgetBackgroundTask.requestImmediateRefresh());
|
||||
// Fade the login content out before handing over to the post-login splash.
|
||||
// Both share the red backdrop, so this reads as one continuous transition
|
||||
// instead of an abrupt swap.
|
||||
_fade.reverse().whenComplete(() {
|
||||
if (!mounted) return;
|
||||
context.read<AccountBloc>().setStatus(AccountStatus.loggedIn);
|
||||
});
|
||||
accountBloc.activated(accountId, freshLogin: true);
|
||||
}
|
||||
|
||||
Future<void> _cancelAddAccount() async {
|
||||
final accountBloc = context.read<AccountBloc>();
|
||||
await SessionLifecycle.cancelAddAccount();
|
||||
accountBloc.activated(SessionManager().activeAccount?.id);
|
||||
}
|
||||
|
||||
Widget _buildCard() => switch (_audience) {
|
||||
null => LoginAudienceCard(
|
||||
addingAccount: widget.addingAccount,
|
||||
onSelected: (choice) => setState(() => _audience = choice),
|
||||
),
|
||||
LoginAudience.school => LoginCard(
|
||||
controller: _controller,
|
||||
onSuccess: _onLoginSuccess,
|
||||
addingAccount: widget.addingAccount,
|
||||
),
|
||||
LoginAudience.guardian => GuardianLoginCard(
|
||||
controller: _guardianController,
|
||||
onSuccess: _onLoginSuccess,
|
||||
addingAccount: widget.addingAccount,
|
||||
),
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
backgroundColor: _marianumRed,
|
||||
body: FadeTransition(
|
||||
opacity: _fade,
|
||||
child: SafeArea(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
maxWidth: 420,
|
||||
Widget build(BuildContext context) => PopScope(
|
||||
canPop: !widget.addingAccount,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (!didPop && !_busy) unawaited(_cancelAddAccount());
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: _marianumRed,
|
||||
appBar: widget.addingAccount
|
||||
? AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: ListenableBuilder(
|
||||
listenable: Listenable.merge([
|
||||
_controller,
|
||||
_guardianController,
|
||||
]),
|
||||
builder: (context, _) => IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
tooltip: 'Abbrechen',
|
||||
onPressed: _busy ? null : _cancelAddAccount,
|
||||
),
|
||||
// spaceBetween statt Spacer-in-IntrinsicHeight: Letzteres würde
|
||||
// die Column bei Inhaltsänderungen im unteren Block auf die
|
||||
// intrinsic-Höhe pinnen und ein paar Pixel Overflow erzeugen.
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
const LoginHeader(),
|
||||
const SizedBox(height: 28),
|
||||
_buildCard(),
|
||||
if (_audience != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
// Leaving mid-request would drop the form that
|
||||
// receives the result, so it is disabled then.
|
||||
child: ListenableBuilder(
|
||||
listenable: Listenable.merge([
|
||||
_controller,
|
||||
_guardianController,
|
||||
]),
|
||||
builder: (context, _) => TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
title: const Text('Konto hinzufügen'),
|
||||
)
|
||||
: null,
|
||||
body: FadeTransition(
|
||||
opacity: _fade,
|
||||
child: SafeArea(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
maxWidth: 420,
|
||||
),
|
||||
// spaceBetween statt Spacer-in-IntrinsicHeight: Letzteres würde
|
||||
// die Column bei Inhaltsänderungen im unteren Block auf die
|
||||
// intrinsic-Höhe pinnen und ein paar Pixel Overflow erzeugen.
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
LoginHeader(addingAccount: widget.addingAccount),
|
||||
const SizedBox(height: 28),
|
||||
_buildCard(),
|
||||
if (_audience != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
// Leaving mid-request would drop the form that
|
||||
// receives the result, so it is disabled then.
|
||||
child: ListenableBuilder(
|
||||
listenable: Listenable.merge([
|
||||
_controller,
|
||||
_guardianController,
|
||||
]),
|
||||
builder: (context, _) => TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
icon: const Icon(Icons.arrow_back, size: 18),
|
||||
label: const Text('Zurück zur Auswahl'),
|
||||
onPressed:
|
||||
_controller.loading ||
|
||||
_guardianController.loading
|
||||
? null
|
||||
: () => setState(() => _audience = null),
|
||||
),
|
||||
icon: const Icon(Icons.arrow_back, size: 18),
|
||||
label: const Text('Zurück zur Auswahl'),
|
||||
onPressed:
|
||||
_controller.loading ||
|
||||
_guardianController.loading
|
||||
? null
|
||||
: () => setState(() => _audience = null),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [_EndpointLink(), LoginFooter()],
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// The new account must live on the server the app
|
||||
// already talks to.
|
||||
if (!widget.addingAccount) const _EndpointLink(),
|
||||
const LoginFooter(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -201,6 +251,8 @@ class _LoginState extends State<Login> with SingleTickerProviderStateMixin {
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
bool get _busy => _controller.loading || _guardianController.loading;
|
||||
}
|
||||
|
||||
/// Subtle text link above the footer that surfaces the currently selected
|
||||
|
||||
Reference in New Issue
Block a user