Skip to content

Commit

Permalink
login: Support logging out of an account
Browse files Browse the repository at this point in the history
Fixes: zulip#463
  • Loading branch information
chrisbobbe committed Oct 17, 2024
1 parent b4b9569 commit 6bc8bcc
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 2 deletions.
4 changes: 4 additions & 0 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
"@chooseAccountPageTitle": {
"description": "Title for ChooseAccountPage"
},
"chooseAccountPageLogOutButton": "Log out",
"@chooseAccountPageLogOutButton": {
"description": "Label for the 'Log out' button for an account on the choose-account page"
},
"chooseAccountButtonAddAnAccount": "Add an account",
"@chooseAccountButtonAddAnAccount": {
"description": "Label for ChooseAccountPage button to add an account"
Expand Down
53 changes: 53 additions & 0 deletions lib/widgets/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
import '../log.dart';
import '../model/localizations.dart';
import '../model/narrow.dart';
import '../model/store.dart';
import '../notifications/receive.dart';
import 'about_zulip.dart';
import 'app_bar.dart';
import 'dialog.dart';
Expand Down Expand Up @@ -221,15 +223,64 @@ class ChooseAccountPage extends StatelessWidget {
required Widget title,
Widget? subtitle,
}) {
final designVariables = DesignVariables.of(context);
final zulipLocalizations = ZulipLocalizations.of(context);
return Card(
clipBehavior: Clip.hardEdge,
child: ListTile(
title: title,
subtitle: subtitle,
trailing: PopupMenuButton<AccountItemOverflowMenuItem>(
iconColor: designVariables.icon,
itemBuilder: (context) => [
PopupMenuItem(
value: AccountItemOverflowMenuItem.logOut,
child: Text(zulipLocalizations.chooseAccountPageLogOutButton)),
],
onSelected: (item) async {
switch (item) {
case AccountItemOverflowMenuItem.logOut: {
unawaited(_logOutAccount(context, accountId));
}
}
}),
// The default trailing padding with M3 is 24px. Decrease by 12 because
// PopupMenuButton adds 12px padding on all sides of the "…" icon.
contentPadding: const EdgeInsetsDirectional.only(start: 16, end: 12),
onTap: () => Navigator.push(context,
HomePage.buildRoute(accountId: accountId))));
}

Future<void> _logOutAccount(BuildContext context, int accountId) async {
final globalStore = GlobalStoreWidget.of(context);

final account = globalStore.getAccount(accountId);
if (account == null) return; // TODO(log)

// Unawaited, to not block removing the account on this request.
unawaited(_unregisterToken(globalStore, account));

await globalStore.removeAccount(accountId);
}

Future<void> _unregisterToken(GlobalStore globalStore, Account account) async {
// TODO(#322) use actual acked push token; until #322, this is just null.
final token = account.ackedPushToken
// Try the current token as a fallback; maybe the server has registered
// it and we just haven't recorded that fact in the client.
?? NotificationService.instance.token.value;
if (token == null) return;

final connection = globalStore.apiConnectionFromAccount(account);
try {
await NotificationService.unregisterToken(connection, token: token);
} catch (e) {
// TODO retry? handle failures?
} finally {
connection.close();
}
}

@override
Widget build(BuildContext context) {
final zulipLocalizations = ZulipLocalizations.of(context);
Expand Down Expand Up @@ -286,6 +337,8 @@ class ChooseAccountPageOverflowButton extends StatelessWidget {
}
}

enum AccountItemOverflowMenuItem { logOut }

class HomePage extends StatelessWidget {
const HomePage({super.key});

Expand Down
1 change: 1 addition & 0 deletions lib/widgets/page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ mixin AccountPageRouteMixin<T extends Object?> on PageRoute<T> {
return PerAccountStoreWidget(
accountId: accountId,
placeholder: const LoadingPlaceholderPage(),
routeToRemoveOnLogout: this,
child: super.buildPage(context, animation, secondaryAnimation));
}
}
Expand Down
24 changes: 22 additions & 2 deletions lib/widgets/store.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

import '../model/binding.dart';
import '../model/store.dart';
import 'page.dart';

/// Provides access to the app's data.
///
Expand Down Expand Up @@ -112,11 +114,19 @@ class PerAccountStoreWidget extends StatefulWidget {
super.key,
required this.accountId,
this.placeholder = const LoadingPlaceholder(),
this.routeToRemoveOnLogout,
required this.child,
});

final int accountId;
final Widget placeholder;

/// A per-account [Route] that should be removed on logout.
///
/// Use this when the widget is a page on a route that should go away
/// when the account is logged out, instead of lingering with [placeholder].
final AccountPageRouteMixin? routeToRemoveOnLogout;

final Widget child;

/// The user's data for the relevant Zulip account for this widget.
Expand Down Expand Up @@ -195,6 +205,16 @@ class _PerAccountStoreWidgetState extends State<PerAccountStoreWidget> {
void didChangeDependencies() {
super.didChangeDependencies();
final globalStore = GlobalStoreWidget.of(context);
final accountExists = globalStore.getAccount(widget.accountId) != null;
if (!accountExists) {
// logged out
_setStore(null);
if (widget.routeToRemoveOnLogout != null) {
SchedulerBinding.instance.addPostFrameCallback(
(_) => Navigator.of(context).removeRoute(widget.routeToRemoveOnLogout!));
}
return;
}
// If we already have data, get it immediately. This avoids showing one
// frame of loading indicator each time we have a new PerAccountStoreWidget.
final store = globalStore.perAccountSync(widget.accountId);
Expand All @@ -212,13 +232,13 @@ class _PerAccountStoreWidgetState extends State<PerAccountStoreWidget> {
// The account was logged out while its store was loading.
// This widget will be showing [placeholder] perpetually,
// but that's OK as long as other code will be removing it from the UI
// (for example by removing a per-account route from the nav).
// (usually by using [routeToRemoveOnLogout]).
}
}();
}
}

void _setStore(PerAccountStore store) {
void _setStore(PerAccountStore? store) {
if (store != this.store) {
setState(() {
this.store = store;
Expand Down

0 comments on commit 6bc8bcc

Please sign in to comment.