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
|
||||
|
||||
@@ -61,6 +61,7 @@ class LoginController extends ChangeNotifier {
|
||||
return LoginResult.success;
|
||||
}
|
||||
|
||||
var signedIn = false;
|
||||
try {
|
||||
await _discardPreviousAccount();
|
||||
// AuthLogin = Credential-Probe + Token-Create in einem Call.
|
||||
@@ -73,6 +74,7 @@ class LoginController extends ChangeNotifier {
|
||||
await SessionManager().signIn(
|
||||
CredentialSession(username: user, password: password),
|
||||
);
|
||||
signedIn = true;
|
||||
// Mint the Nextcloud app password now — it doubles as the Nextcloud
|
||||
// credential probe: a rejection means 2FA is active (or the NC password
|
||||
// diverges) and the login must finish interactively in the browser.
|
||||
@@ -82,7 +84,9 @@ class LoginController extends ChangeNotifier {
|
||||
return ncReady ? LoginResult.success : LoginResult.nextcloudLoginRequired;
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
await SessionManager().signOut();
|
||||
// Only the account signed in by this attempt; while adding an account
|
||||
// the active one is parked and restored on cancel.
|
||||
if (signedIn) await SessionManager().signOut();
|
||||
await const MarianumConnectTokenStorage().clear();
|
||||
final isWrongCredentials = e is AuthException && e.statusCode == 401;
|
||||
_errorMessage = isWrongCredentials
|
||||
@@ -98,7 +102,8 @@ class LoginController extends ChangeNotifier {
|
||||
/// Vorherigen Token verwerfen, bevor ein neuer angefordert wird, und den
|
||||
/// Widget-Snapshot löschen — sonst blitzt nach einem Account-Wechsel kurz
|
||||
/// der Stundenplan des vorigen Users auf dem Home-Bildschirm. Die Session
|
||||
/// selbst überschreibt signIn vollständig.
|
||||
/// selbst überschreibt signIn vollständig; beim Hinzufügen eines Kontos
|
||||
/// liegt der Token des aktiven Kontos schon in dessen Tresor.
|
||||
Future<void> _discardPreviousAccount() async {
|
||||
await const MarianumConnectTokenStorage().clear();
|
||||
await WidgetSync.clear();
|
||||
|
||||
@@ -14,9 +14,13 @@ class GuardianLoginCard extends StatefulWidget {
|
||||
final GuardianLoginController controller;
|
||||
final VoidCallback onSuccess;
|
||||
|
||||
/// Signs in an additional account instead of the first one.
|
||||
final bool addingAccount;
|
||||
|
||||
const GuardianLoginCard({
|
||||
required this.controller,
|
||||
required this.onSuccess,
|
||||
this.addingAccount = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -118,7 +122,9 @@ class _GuardianLoginCardState extends State<GuardianLoginCard> {
|
||||
return Form(
|
||||
key: _emailFormKey,
|
||||
child: LoginCardFrame(
|
||||
title: 'Anmeldung für Eltern',
|
||||
title: widget.addingAccount
|
||||
? 'Elternkonto hinzufügen'
|
||||
: 'Anmeldung für Eltern',
|
||||
hint:
|
||||
'Gib die E-Mail-Adresse ein, die bei der Schule hinterlegt ist. '
|
||||
'Du erhältst einen Anmeldecode per E-Mail.',
|
||||
@@ -194,7 +200,7 @@ class _GuardianLoginCardState extends State<GuardianLoginCard> {
|
||||
const SizedBox(height: 20),
|
||||
LoginSubmitButton(
|
||||
key: const Key('guardian-verify-button'),
|
||||
label: 'Anmelden',
|
||||
label: widget.addingAccount ? 'Hinzufügen' : 'Anmelden',
|
||||
loading: _controller.loading,
|
||||
onPressed: _submitCode,
|
||||
),
|
||||
|
||||
@@ -9,24 +9,35 @@ enum LoginAudience { school, guardian }
|
||||
class LoginAudienceCard extends StatelessWidget {
|
||||
final ValueChanged<LoginAudience> onSelected;
|
||||
|
||||
const LoginAudienceCard({required this.onSelected, super.key});
|
||||
/// Signs in an additional account instead of the first one.
|
||||
final bool addingAccount;
|
||||
|
||||
const LoginAudienceCard({
|
||||
required this.onSelected,
|
||||
this.addingAccount = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => LoginCardFrame(
|
||||
title: 'Anmelden',
|
||||
hint: 'Bitte wähle deine Anmeldemethode',
|
||||
title: addingAccount ? 'Konto hinzufügen' : 'Anmelden',
|
||||
hint: addingAccount
|
||||
? 'Welches Konto möchtest du hinzufügen?'
|
||||
: 'Bitte wähle deine Anmeldemethode',
|
||||
children: [
|
||||
_AudienceButton(
|
||||
key: const Key('login-audience-school'),
|
||||
icon: Icons.school_outlined,
|
||||
label: 'Login für Schülerschaft & Lehrkräfte',
|
||||
label: addingAccount
|
||||
? 'Schulkonto (Schülerschaft & Lehrkräfte)'
|
||||
: 'Login für Schülerschaft & Lehrkräfte',
|
||||
onPressed: () => onSelected(LoginAudience.school),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_AudienceButton(
|
||||
key: const Key('login-audience-guardian'),
|
||||
icon: Icons.family_restroom_outlined,
|
||||
label: 'Login für Eltern',
|
||||
label: addingAccount ? 'Elternkonto' : 'Login für Eltern',
|
||||
onPressed: () => onSelected(LoginAudience.guardian),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LoginHeader extends StatelessWidget {
|
||||
const LoginHeader({super.key});
|
||||
/// Signs in an additional account instead of the first one.
|
||||
final bool addingAccount;
|
||||
|
||||
const LoginHeader({this.addingAccount = false, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
@@ -29,7 +32,10 @@ class LoginHeader extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Stundenplan, Talk, Dateien und mehr - alles an einem Ort für deinen Schulalltag am Marianum Fulda.',
|
||||
addingAccount
|
||||
? 'Melde ein weiteres Konto an. In den Einstellungen kannst du '
|
||||
'danach jederzeit zwischen deinen Konten wechseln.'
|
||||
: 'Stundenplan, Talk, Dateien und mehr - alles an einem Ort für deinen Schulalltag am Marianum Fulda.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.85),
|
||||
|
||||
@@ -12,9 +12,13 @@ class LoginCard extends StatefulWidget {
|
||||
final LoginController controller;
|
||||
final VoidCallback onSuccess;
|
||||
|
||||
/// Signs in an additional account instead of the first one.
|
||||
final bool addingAccount;
|
||||
|
||||
const LoginCard({
|
||||
required this.controller,
|
||||
required this.onSuccess,
|
||||
this.addingAccount = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -83,8 +87,11 @@ class _LoginCardState extends State<LoginCard> {
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: LoginCardFrame(
|
||||
title: 'Anmelden',
|
||||
hint: 'Melde dich mit deinen Marianum-Zugangsdaten an.',
|
||||
title: widget.addingAccount ? 'Schulkonto hinzufügen' : 'Anmelden',
|
||||
hint: widget.addingAccount
|
||||
? 'Melde dich mit den Marianum-Zugangsdaten des Kontos an, das '
|
||||
'du hinzufügen möchtest.'
|
||||
: 'Melde dich mit deinen Marianum-Zugangsdaten an.',
|
||||
children: [
|
||||
TextFormField(
|
||||
key: const Key('login-username-field'),
|
||||
@@ -127,7 +134,7 @@ class _LoginCardState extends State<LoginCard> {
|
||||
const SizedBox(height: 20),
|
||||
LoginSubmitButton(
|
||||
key: const Key('login-submit-button'),
|
||||
label: 'Anmelden',
|
||||
label: widget.addingAccount ? 'Hinzufügen' : 'Anmelden',
|
||||
loading: loading,
|
||||
onPressed: _submit,
|
||||
),
|
||||
|
||||
@@ -6,24 +6,25 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../api/marianumcloud/cloud_users/cloud_users_actions.dart';
|
||||
import '../../../../push/push_registration.dart';
|
||||
import '../../../../routing/app_routes.dart';
|
||||
import '../../../../session/account_codec.dart';
|
||||
import '../../../../session/session.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/capabilities/bloc/capabilities_cubit.dart';
|
||||
import '../../../../widget/account_switcher_sheet.dart';
|
||||
import '../../../../widget/app_progress_indicator.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/avatar_actions_sheet.dart';
|
||||
import '../../../../widget/centered_leading.dart';
|
||||
import '../../../../widget/child_switcher.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
import '../../../../widget/demo_restricted.dart';
|
||||
import '../../../../widget/user_avatar.dart';
|
||||
|
||||
// Display-name is process-wide stable until the user logs out; cache it so
|
||||
// every Settings rebuild doesn't re-issue the OCS request.
|
||||
// Display-name is stable per account; cache it so every Settings rebuild
|
||||
// doesn't re-issue the OCS request.
|
||||
String? _cachedDisplayName;
|
||||
String? _cachedDisplayNameFor;
|
||||
|
||||
class AccountSection extends StatelessWidget {
|
||||
const AccountSection({super.key});
|
||||
@@ -44,20 +45,45 @@ class _GuardianAccount extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final children = context.watch<CapabilitiesCubit>().state.children;
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
leading: const CenteredLeading(Icon(Icons.family_restroom_outlined)),
|
||||
title: const Text('Elternkonto'),
|
||||
subtitle: Text(email),
|
||||
trailing: TextButton.icon(
|
||||
icon: const Icon(Icons.logout_outlined, size: 18),
|
||||
label: const Text('Abmelden'),
|
||||
onPressed: () => _confirmLogout(context),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 8, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 36,
|
||||
backgroundColor: colors.secondaryContainer,
|
||||
foregroundColor: colors.onSecondaryContainer,
|
||||
child: const Icon(Icons.family_restroom_outlined, size: 34),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<AccountIndex>(
|
||||
valueListenable: SessionManager().accounts,
|
||||
builder: (context, index, _) => _AccountName(
|
||||
name: index.active?.displayName ?? 'Elternkonto',
|
||||
identity: email,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const _AccountActions(),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (children.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: Text(
|
||||
children.length == 1 ? 'Dein Kind' : 'Deine Kinder',
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final child in children) ChildTile(child: child),
|
||||
],
|
||||
);
|
||||
@@ -74,17 +100,20 @@ class _SchoolAccount extends StatefulWidget {
|
||||
class _SchoolAccountState extends State<_SchoolAccount> {
|
||||
int _avatarVersion = 0;
|
||||
bool _avatarBusy = false;
|
||||
String? _displayName = _cachedDisplayName;
|
||||
String? _displayName;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (_displayName == null) _loadDisplayName();
|
||||
final username = SessionManager().requireNextcloud().username;
|
||||
if (_cachedDisplayNameFor == username) _displayName = _cachedDisplayName;
|
||||
if (_displayName == null) _loadDisplayName(username);
|
||||
}
|
||||
|
||||
Future<void> _loadDisplayName() async {
|
||||
Future<void> _loadDisplayName(String username) async {
|
||||
try {
|
||||
final info = await GetUserInfo().run();
|
||||
_cachedDisplayNameFor = username;
|
||||
_cachedDisplayName = info.displayName.isEmpty ? null : info.displayName;
|
||||
if (!mounted) return;
|
||||
setState(() => _displayName = _cachedDisplayName);
|
||||
@@ -143,7 +172,7 @@ class _SchoolAccountState extends State<_SchoolAccount> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 8, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
@@ -177,7 +206,7 @@ class _SchoolAccountState extends State<_SchoolAccount> {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -207,12 +236,8 @@ class _SchoolAccountState extends State<_SchoolAccount> {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.logout_outlined, size: 18),
|
||||
label: const Text('Abmelden'),
|
||||
onPressed: () => _confirmLogout(context),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const _AccountActions(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -245,26 +270,77 @@ class _SchoolAccountState extends State<_SchoolAccount> {
|
||||
}
|
||||
|
||||
Future<void> _confirmLogout(BuildContext context) async {
|
||||
final accountBloc = context.read<AccountBloc>();
|
||||
final others = SessionManager().accounts.value.accounts.length - 1;
|
||||
String? nextAccountId;
|
||||
// Flip AccountBloc state only after the dialog fully closes: doing it from
|
||||
// inside the sign-out (the previous approach) raced AsyncDialogAction's
|
||||
// pop(true) against the listener's popUntil(isFirst) and could leave the
|
||||
// navigator in an inconsistent state.
|
||||
// pop(true) against the navigator teardown of the account switch.
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => ConfirmDialog(
|
||||
title: 'Abmelden?',
|
||||
content: 'Möchtest du dich wirklich abmelden?',
|
||||
content: others > 0
|
||||
? 'Möchtest du dich wirklich abmelden? Die App wechselt danach zu '
|
||||
'einem deiner anderen Konten.'
|
||||
: 'Möchtest du dich wirklich abmelden?',
|
||||
confirmButton: 'Abmelden',
|
||||
onConfirmAsync: _performLogout,
|
||||
onConfirmAsync: () async =>
|
||||
nextAccountId = await SessionLifecycle.signOut(),
|
||||
),
|
||||
);
|
||||
if (confirmed != true || !context.mounted) return;
|
||||
context.read<AccountBloc>().setStatus(AccountStatus.loggedOut);
|
||||
if (confirmed != true) return;
|
||||
accountBloc.activated(nextAccountId);
|
||||
}
|
||||
|
||||
Future<void> _performLogout() async {
|
||||
await SessionLifecycle.signOut();
|
||||
_cachedDisplayName = null;
|
||||
class _AccountName extends StatelessWidget {
|
||||
final String name;
|
||||
final String identity;
|
||||
|
||||
const _AccountName({required this.name, required this.identity});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
identity,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _AccountActions extends StatelessWidget {
|
||||
const _AccountActions();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
style: compactAccountButtonStyle,
|
||||
icon: const Icon(Icons.logout_outlined, size: 18),
|
||||
label: const Text('Abmelden'),
|
||||
onPressed: () => _confirmLogout(context),
|
||||
),
|
||||
const AccountSwitchButton(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _AvatarEditBadge extends StatelessWidget {
|
||||
|
||||
Reference in New Issue
Block a user