diff --git a/client/lib/ui/app/account_selector_web.dart b/client/lib/ui/app/account_selector_web.dart index 0a8fe4dc..7e89d917 100644 --- a/client/lib/ui/app/account_selector_web.dart +++ b/client/lib/ui/app/account_selector_web.dart @@ -1,4 +1,6 @@ +import 'dart:async'; import 'dart:js_interop'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:offtheline/offtheline.dart'; @@ -6,6 +8,7 @@ import 'package:phylum/libphylum/phylum_account.dart'; import 'package:phylum/ui/app/dialog_scaffold.dart'; import 'package:phylum/ui/app/login_page.dart'; import 'package:phylum/ui/layout/app_layout.dart'; +import 'package:phylum/util/logging.dart'; import 'package:provider/provider.dart'; import 'package:web/web.dart' as web; @@ -23,12 +26,24 @@ class AccountSelector extends StatefulWidget { } class _AccountSelectorState extends State { - final channel = web.BroadcastChannel('takover'); + final channel = web.BroadcastChannel('handoff'); + late final AccountManager accountManager; + String? requestId; + Timer? retryRequestTimer; + int retriesLeft = 0; + bool requestAccepted = false; @override void initState() { super.initState(); channel.onmessage = processMessage.toJS; + accountManager = context.read(); + } + + @override + void dispose() { + channel.onmessage = null; + super.dispose(); } @override @@ -43,10 +58,8 @@ class _AccountSelectorState extends State { const Text('Phylum is already open in another tab or window'), const SizedBox(height: 12), ElevatedButton( + onPressed: requestId == null ? requestHandoff : null, child: const Text('Use Here'), - onPressed: () { - channel.postMessage('close'.toJS); - }, ), ], )); @@ -71,20 +84,74 @@ class _AccountSelectorState extends State { return AppLayout.create(account); } + void requestHandoff() { + if (requestId != null) return; + final r = Random(); + const chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; + + setState(() { + requestId = String.fromCharCodes(Iterable.generate(12, (_) => chars.codeUnitAt(r.nextInt(chars.length)))); + }); + + channel.postMessage('request|$requestId'.toJS); + retryRequestTimer = Timer.periodic(const Duration(milliseconds: 100), (t) { + if (retriesLeft-- > 0) { + if (!requestAccepted) { + channel.postMessage('request|$requestId'.toJS); + } + } else { + channel.postMessage('hijack|$requestId'.toJS); + retryRequestTimer?.cancel(); + retryRequestTimer = null; + requestId = null; + setPrimary(true.toJS); + accountManager.initialize(); + } + }); + retriesLeft = 5; + requestAccepted = false; + } + void processMessage(web.MessageEvent event) { - final msg = event.data.toString(); - final accountManager = context.read>(); - if (msg == 'close') { + final data = event.data.toString(); + final msg = data.split('|'); + if (msg.length < 2) { + logger.w('Unrecognized message: $data'); + } + + if (msg[0] == 'request') { + final id = msg[1]; if (accountManager.status == AccountManagerStatus.ready) { - channel.postMessage('closing'.toJS); + channel.postMessage('accept|$id'.toJS); accountManager.close().then((v) { - channel.postMessage('closed'.toJS); + channel.postMessage('success|$id'.toJS); setPrimary(false.toJS); - accountManager.close(); }); + } else { + channel.postMessage('reject|$id'.toJS); } } - if (msg == 'closed') { + if (msg[0] == 'hijack') { + setPrimary(false.toJS); + } + + // Process responses, but make sure the id matches + if (msg[1] != requestId) return; + + if (msg[0] == 'reject') { + retryRequestTimer?.cancel(); + retryRequestTimer = null; + requestId = null; + } + if (msg[0] == 'accept') { + // Maybe display a message if this is taking time + requestAccepted = true; + retriesLeft += 10; + } + if (msg[0] == 'success') { + retryRequestTimer?.cancel(); + retryRequestTimer = null; + requestId = null; setPrimary(true.toJS); accountManager.initialize(); }