Skip to content

Commit 3fe188e

Browse files
committed
login: Support logging out of an account
Fixes: #463
1 parent 64a8ebf commit 3fe188e

File tree

4 files changed

+83
-2
lines changed

4 files changed

+83
-2
lines changed

assets/l10n/app_en.arb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
"@chooseAccountPageTitle": {
2020
"description": "Title for ChooseAccountPage"
2121
},
22+
"chooseAccountPageLogOutButton": "Log out",
23+
"@chooseAccountPageLogOutButton": {
24+
"description": "Label for the 'Log out' button for an account on the choose-account page"
25+
},
2226
"chooseAccountButtonAddAnAccount": "Add an account",
2327
"@chooseAccountButtonAddAnAccount": {
2428
"description": "Label for ChooseAccountPage button to add an account"

lib/widgets/app.dart

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
88
import '../log.dart';
99
import '../model/localizations.dart';
1010
import '../model/narrow.dart';
11+
import '../model/store.dart';
12+
import '../notifications/receive.dart';
1113
import 'about_zulip.dart';
1214
import 'app_bar.dart';
1315
import 'dialog.dart';
@@ -221,15 +223,69 @@ class ChooseAccountPage extends StatelessWidget {
221223
required Widget title,
222224
Widget? subtitle,
223225
}) {
226+
final designVariables = DesignVariables.of(context);
227+
final zulipLocalizations = ZulipLocalizations.of(context);
228+
final materialLocalizations = MaterialLocalizations.of(context);
224229
return Card(
225230
clipBehavior: Clip.hardEdge,
226231
child: ListTile(
227232
title: title,
228233
subtitle: subtitle,
234+
trailing: MenuAnchor(
235+
menuChildren: [
236+
MenuItemButton(
237+
onPressed: () => unawaited(_logOutAccount(context, accountId)),
238+
child: Text(zulipLocalizations.chooseAccountPageLogOutButton)),
239+
],
240+
builder: (BuildContext context, MenuController controller, Widget? child) {
241+
return IconButton(
242+
tooltip: materialLocalizations.showMenuTooltip, // "Show menu"
243+
onPressed: () {
244+
if (controller.isOpen) {
245+
controller.close();
246+
} else {
247+
controller.open();
248+
}
249+
},
250+
icon: Icon(Icons.adaptive.more, color: designVariables.icon));
251+
}),
252+
// The default trailing padding with M3 is 24px. Decrease by 12 because
253+
// IconButton (the "…" button) comes with 12px padding on all sides.
254+
contentPadding: const EdgeInsetsDirectional.only(start: 16, end: 12),
229255
onTap: () => Navigator.push(context,
230256
HomePage.buildRoute(accountId: accountId))));
231257
}
232258

259+
Future<void> _logOutAccount(BuildContext context, int accountId) async {
260+
final globalStore = GlobalStoreWidget.of(context);
261+
262+
final account = globalStore.getAccount(accountId);
263+
if (account == null) return; // TODO(log)
264+
265+
// Unawaited, to not block removing the account on this request.
266+
unawaited(_unregisterToken(globalStore, account));
267+
268+
await globalStore.removeAccount(accountId);
269+
}
270+
271+
Future<void> _unregisterToken(GlobalStore globalStore, Account account) async {
272+
// TODO(#322) use actual acked push token; until #322, this is just null.
273+
final token = account.ackedPushToken
274+
// Try the current token as a fallback; maybe the server has registered
275+
// it and we just haven't recorded that fact in the client.
276+
?? NotificationService.instance.token.value;
277+
if (token == null) return;
278+
279+
final connection = globalStore.apiConnectionFromAccount(account);
280+
try {
281+
await NotificationService.unregisterToken(connection, token: token);
282+
} catch (e) {
283+
// TODO retry? handle failures?
284+
} finally {
285+
connection.close();
286+
}
287+
}
288+
233289
@override
234290
Widget build(BuildContext context) {
235291
final zulipLocalizations = ZulipLocalizations.of(context);

lib/widgets/page.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ mixin AccountPageRouteMixin<T extends Object?> on PageRoute<T> {
4040
return PerAccountStoreWidget(
4141
accountId: accountId,
4242
placeholder: const LoadingPlaceholderPage(),
43+
routeToRemoveOnLogout: this,
4344
child: super.buildPage(context, animation, secondaryAnimation));
4445
}
4546
}

lib/widgets/store.dart

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter/scheduler.dart';
23

34
import '../model/binding.dart';
45
import '../model/store.dart';
6+
import 'page.dart';
57

68
/// Provides access to the app's data.
79
///
@@ -112,11 +114,19 @@ class PerAccountStoreWidget extends StatefulWidget {
112114
super.key,
113115
required this.accountId,
114116
this.placeholder = const LoadingPlaceholder(),
117+
this.routeToRemoveOnLogout,
115118
required this.child,
116119
});
117120

118121
final int accountId;
119122
final Widget placeholder;
123+
124+
/// A per-account [Route] that should be removed on logout.
125+
///
126+
/// Use this when the widget is a page on a route that should go away
127+
/// when the account is logged out, instead of lingering with [placeholder].
128+
final AccountPageRouteMixin? routeToRemoveOnLogout;
129+
120130
final Widget child;
121131

122132
/// The user's data for the relevant Zulip account for this widget.
@@ -195,6 +205,16 @@ class _PerAccountStoreWidgetState extends State<PerAccountStoreWidget> {
195205
void didChangeDependencies() {
196206
super.didChangeDependencies();
197207
final globalStore = GlobalStoreWidget.of(context);
208+
final accountExists = globalStore.getAccount(widget.accountId) != null;
209+
if (!accountExists) {
210+
// logged out
211+
_setStore(null);
212+
if (widget.routeToRemoveOnLogout != null) {
213+
SchedulerBinding.instance.addPostFrameCallback(
214+
(_) => Navigator.of(context).removeRoute(widget.routeToRemoveOnLogout!));
215+
}
216+
return;
217+
}
198218
// If we already have data, get it immediately. This avoids showing one
199219
// frame of loading indicator each time we have a new PerAccountStoreWidget.
200220
final store = globalStore.perAccountSync(widget.accountId);
@@ -212,13 +232,13 @@ class _PerAccountStoreWidgetState extends State<PerAccountStoreWidget> {
212232
// The account was logged out while its store was loading.
213233
// This widget will be showing [placeholder] perpetually,
214234
// but that's OK as long as other code will be removing it from the UI
215-
// (for example by removing a per-account route from the nav).
235+
// (usually by using [routeToRemoveOnLogout]).
216236
}
217237
}();
218238
}
219239
}
220240

221-
void _setStore(PerAccountStore store) {
241+
void _setStore(PerAccountStore? store) {
222242
if (store != this.store) {
223243
setState(() {
224244
this.store = store;

0 commit comments

Comments
 (0)