Skip to content

Commit 054da31

Browse files
committed
login: Support logging out of an account
Fixes: #463
1 parent 3e11686 commit 054da31

File tree

4 files changed

+79
-2
lines changed

4 files changed

+79
-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: 52 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,63 @@ class ChooseAccountPage extends StatelessWidget {
221223
required Widget title,
222224
Widget? subtitle,
223225
}) {
226+
final designVariables = DesignVariables.of(context);
227+
final zulipLocalizations = ZulipLocalizations.of(context);
224228
return Card(
225229
clipBehavior: Clip.hardEdge,
226230
child: ListTile(
227231
title: title,
228232
subtitle: subtitle,
233+
trailing: PopupMenuButton<AccountItemOverflowMenuItem>(
234+
iconColor: designVariables.icon,
235+
itemBuilder: (context) => [
236+
PopupMenuItem(
237+
value: AccountItemOverflowMenuItem.logOut,
238+
child: Text(zulipLocalizations.chooseAccountPageLogOutButton)),
239+
],
240+
onSelected: (item) async {
241+
switch (item) {
242+
case AccountItemOverflowMenuItem.logOut: {
243+
unawaited(_logOutAccount(context, accountId));
244+
}
245+
}
246+
}),
247+
// The default trailing padding with M3 is 24px. Decrease by 12 because
248+
// PopupMenuButton adds 12px padding on all sides of the "…" icon.
249+
contentPadding: const EdgeInsetsDirectional.only(start: 16, end: 12),
229250
onTap: () => Navigator.push(context,
230251
HomePage.buildRoute(accountId: accountId))));
231252
}
232253

254+
Future<void> _logOutAccount(BuildContext context, int accountId) async {
255+
final globalStore = GlobalStoreWidget.of(context);
256+
257+
final account = globalStore.getAccount(accountId);
258+
if (account == null) return; // TODO(log)
259+
260+
// Async IIFE to not block removing the account on the unregister-token request.
261+
unawaited(() async {
262+
// TODO(#322) use actual acked push token; until #322, this is just null.
263+
final token = account.ackedPushToken
264+
// Try the current token as a fallback; maybe the server has registered
265+
// it and we just haven't recorded that fact in the client.
266+
?? NotificationService.instance.token.value;
267+
if (token == null) return;
268+
269+
final connection = globalStore.apiConnectionFromAccount(account);
270+
try {
271+
await NotificationService.unregisterToken(connection, token: token);
272+
} catch (e) {
273+
// TODO retry? handle failures?
274+
} finally {
275+
connection.close();
276+
}
277+
}());
278+
279+
// TODO error handling? disable logout button during this?
280+
await globalStore.removeAccount(accountId);
281+
}
282+
233283
@override
234284
Widget build(BuildContext context) {
235285
final zulipLocalizations = ZulipLocalizations.of(context);
@@ -286,6 +336,8 @@ class ChooseAccountPageOverflowButton extends StatelessWidget {
286336
}
287337
}
288338

339+
enum AccountItemOverflowMenuItem { logOut }
340+
289341
class HomePage extends StatelessWidget {
290342
const HomePage({super.key});
291343

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,15 +232,15 @@ 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
} catch (e) {
217237
// TODO(log)
218238
}
219239
}();
220240
}
221241
}
222242

223-
void _setStore(PerAccountStore store) {
243+
void _setStore(PerAccountStore? store) {
224244
if (store != this.store) {
225245
setState(() {
226246
this.store = store;

0 commit comments

Comments
 (0)