-
Notifications
You must be signed in to change notification settings - Fork 262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
store: Handle invalid API key on register-queue #1183
base: main
Are you sure you want to change the base?
Conversation
Thanks! Would you post a screenshot of what the error looks like? Then we can also wordsmith the error message. |
Cool, thanks. I think "Invalid API key" is probably too technical for this context. @alya do we have an appropriate message handy from elsewhere in the product? |
a43e320
to
4df9d15
Compare
Updated the PR to implement Chris' suggestion here. We now handle invalid API key by calling |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Here's a review of the first five commits:
06c5543 l10n [nfc]: Use a generalize name for errorCouldNotConnectTitle
d27b46c log [nfc]: Rename ReportErrorCallback to ReportErrorCancellablyCallback
c5fc2d9 example_data: Start generating account id
652c332 home: Stop assuming account existence from loading page
ab91480 log: Add reportErrorModally
which leaves the last three that I haven't read yet:
58fd4b1 store: Handle invalid API key on register-queue
7a632b2 display test [nfc]: Make openNotification public
2e0bcb9 actions test: Use a realistic example
For the last commit, it's the one with the Fixes:
line but it doesn't actually change any app code, it only touches test code. Is that intentional? I'd like to understand if/how these test changes are related to your app-code changes for #737.
"errorLoginCouldNotConnectTitle": "Could not connect", | ||
"@errorLoginCouldNotConnectTitle": { | ||
"errorCouldNotConnectTitle": "Could not connect", | ||
"@errorCouldNotConnectTitle": { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
l10n [nfc]: Use a generalize name for errorCouldNotConnectTitle
commit-message nit: "generalized"
lib/widgets/home.dart
Outdated
if (account == null) { | ||
// We should only reach this state very briefly. | ||
// See [_LoadingPlaceholderPage.accountId]. | ||
return Scaffold( | ||
appBar: AppBar(), | ||
body: const SizedBox.shrink()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels like it duplicates more details than it needs to, like the appBar
param and the whole set of Scaffold
params that aren't passed. How about applying the condition more precisely to the things that are supposed to be different?
lib/log.dart
Outdated
// This gets set in [ZulipApp]. We need this indirection to keep `lib/log.dart` | ||
// from importing widget code, because the file is a dependency for the rest of | ||
// the app. | ||
ReportErrorCallback reportErrorToUserModally = defaultReportErrorToUserBriefly; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setting reportErrorToUserModally
to defaultReportErrorToUserBriefly
looks odd. How about renaming defaultReportErrorToUserBriefly
to something that seems like an appropriate value for both reportErrorToUserBriefly
and reportErrorToUserModally
?
Maybe dumpErrorToConsole
? That name makes it sound like a potential problem if it actually gets called (errors should be reported, not dropped on the floor 🙂)—but that's actually appropriate in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea! Renaming to reportErrorToConsole
because we don't usually dump the error object, which I feel is what "dump" is for.
test/widgets/app_test.dart
Outdated
reportErrorToUserModally(message, details: details); | ||
check(ZulipApp.ready).value.isFalse(); | ||
await tester.pump(); | ||
check(find.byType(AlertDialog)).findsNothing(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a commit
test [nfc]: Factor out checkNoErrorDialog helper
in my revision for #1239; how about cherry-picking that as a prep commit and using it here? (See reasoning in the commit)
The last commit "fixes" the issue because it resolves a TODO comment for the issue. With #737 we can set up the test in a scenario where the API key is invalidated. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Comments below.
assets/l10n/app_en.arb
Outdated
@@ -458,6 +458,13 @@ | |||
"@topicValidationErrorMandatoryButEmpty": { | |||
"description": "Topic validation error when topic is required but was empty." | |||
}, | |||
"errorInvalidApiKeyMessage": "Your account at {url} cannot be authenticated. Please try again or use another account.", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think my comment in CZO still applies:
[…] the message looks like it can't be literally true. If an action really "cannot" be done, then trying the same action again won't work.
Or, from another angle: the server hasn't said that the account can't be authenticated, so we shouldn't tell the user that. That would be a pretty pathological case: I guess in theory it would be accurate if the database were corrupted in a way that made it impossible to know if any API key was valid.
What we know from the INVALID_API_KEY
error is that the current attempt to authenticate failed, because we used the wrong API key. Here's a proposal:
Your account at {url} could not be authenticated. Please try logging in again or use another account.
cc @alya for thoughts on this message.
lib/widgets/app.dart
Outdated
/// Navigate to [ChooseAccountPage], ensuring that its route is at the root level. | ||
static void navigate(BuildContext context) { | ||
final navigator = Navigator.of(context); | ||
navigator.popUntil((route) => route.isFirst); | ||
unawaited(navigator.pushReplacement( | ||
MaterialWidgetRoute(page: const ChooseAccountPage()))); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this isn't used anywhere.
lib/widgets/store.dart
Outdated
if (!navigator.canPop()) { | ||
// This ensures that the navigator stack is non-empty after the | ||
// removal of the route. | ||
unawaited(navigator.push( | ||
MaterialWidgetRoute(page: const ChooseAccountPage()))); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my manual testing, I'm getting an extra ChooseAccountPage
in the nav stack. Do you reproduce? Possibly it's only happening when I have more than one PerAccountStoreWidget
mounted for the logged-out account. I think probably what's happening is you expected NavigatorState.canPop()
to return false only when the nav stack is empty, but it's returning false when the nav stack consists of one route.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, a check for both isFirst
and isCurrent
would be more appropriate. I think this also means that the current test does not cover this case correctly, will fix that too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It turned out that
Yeah, a check for both
isFirst
andisCurrent
would be more appropriate.
is not accurate.
There can be routes that are not removed on logout but are not helpful either for the user to navigate to a choose-account page. DialogRoute
is an example of that. So checks based on "if the navigator stack is empty" will likely not work. We need a solution that guarantees a choose-account page route at the root level, and this is probably not the right place to do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We now implement a NavigatorObserver
based solution. It implements didPop
and didRemove
to push a choose-account page route proactively if it observes that the navigator stack is becoming empty.
lib/widgets/store.dart
Outdated
// The API key is invalid and the store can never be loaded | ||
// unless the user retries manually. | ||
if (!mounted) return; | ||
final zulipLocalizations = ZulipLocalizations.of(context); | ||
reportErrorToUserModally( | ||
zulipLocalizations.errorCouldNotConnectTitle, | ||
details: zulipLocalizations.errorInvalidApiKeyMessage( | ||
globalStore.getAccount(widget.accountId)!.realmUrl.toString())); | ||
unawaited(logOutAccount(context, widget.accountId)); | ||
return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of this gets skipped if there's been a previously successful /register
, because in that case the store != null
path is taken (some lines above this).
So in this revision, in the refresh-expired-event-queue case (GlobalStore._reloadPerAccount
), I think the uncaught error means the user gets stuck with a stale PerAccountStore
with no active event queue, and they can't replace that store (because we take the store != null
path) except by closing/opening the app, or I guess logging out and back in. We should handle the refresh-expired-event-queue case, with test coverage.
Can we put this reportErrorToUserModally()
, the logOutAccount()
, and the reset-navigation-state action (previous comment) somewhere that clearly gets invoked once (just once) on every /register
/ INVALID_API_KEY
, including the one in _reloadPerAccount
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also need tests to check that the account is in fact logged out / removed, and that some subtle related things are dealt with, like _perAccountStores
and _perAccountStoresLoading
, calling PerAccountStore.dispose
, and notifying listeners. How about using TestGlobalStore.takeDoRemoveAccountCalls
for this, because GlobalStore.removeAccount
already has tests for those things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For example (just for the first-/register
case):
test('GlobalStore.perAccount on INVALID_API_KEY', () => awaitFakeAsync((async) async {
addTearDown(testBinding.reset);
await testBinding.globalStore.insertAccount(eg.selfAccount.toCompanion(false));
testBinding.globalStore.loadPerAccountException = ZulipApiException(
routeName: '/register', code: 'INVALID_API_KEY', httpStatus: 400,
data: {}, message: '');
await check(testBinding.globalStore.perAccount(eg.selfAccount.id))
.throws<ZulipApiException>();
// fails
check(testBinding.globalStore.takeDoRemoveAccountCalls())
.single.equals(eg.selfAccount.id);
}));
(to go near the other 'GlobalStore.perAccount' tests)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the right place for calling logOutAccount
and reportErrorModally
should be loadPerAccount
. We throw an AccountNotFoundException
after processing the error, and make sure that all call-sites handles that correctly. _PerAccountStoreWidgetState
already expects the exception.
For the other call-site, we need to handle it at least for one of _reloadPerAccount
, UpdateMachine._handlePollError
, or UpdateMachine.poll
.
It doesn't seem right to catch it from _reloadPerAccount
without rethrowing or at least indicating that the reload was a failure; _handlePollError
's caller assume that the store is disposed after calling it (because of a reloaded PerAccountStore
), so we should still either rethrow the error or indicate the failure reloading the store. So we probably should just catch it from poll
, within another catch
block.
Because logOutAccount
is no longer purely a user action, we might want to revisit #1010 (comment) and figure out a new home to this. While GlobalStore
is logically a good fit, I agree that lib/model/store.dart
has become too crowded for us to add more to it. Maybe using mixins for store.dart
would be a helpful prep/follow-up change.
test/widgets/store_test.dart
Outdated
await tester.tap(find.text('Try another account')); | ||
await tester.pump(); // tap the button | ||
await tester.pump(const Duration(milliseconds: 250)); // wait for animation | ||
check(find.byType(CircularProgressIndicator)).findsOne(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't yet understand this CircularProgressIndicator
check.
- Does the test expect it to be on a specific page? (meaning the check stops being as useful if another page adds a
CircularProgressIndicator
) - If we're expecting it on a non-active/foregrounded page, do we need to use
skipOffstage: false
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's looking for the route that is not on the top of the stack. I'm switching to a rewrite of this using NavigatorObserver
's, so that we can check the navigator stack more directly.
// TODO(#737): switch to a realistic setup: | ||
// https://github.com/zulip/zulip-flutter/pull/1076#discussion_r1874124363 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reading that linked discussion and links from there, I'm not finding an explanation for why the TODO involves #737 at all. What Greg meant by not "realistic setup" is just that "These pages don't exist in real life", meaning two MaterialAccountWidgetRoute
s with page InboxPageBody
. Routes like that don't exist in the app because, since the bottom-tabs update, the Inbox page is never alone on its own route, it's always part of the route returned by HomePage.buildRoute
.
The direct fix is to use realistic routes for account1Route
and account2Route
, e.g. by using MessageListPage.buildRoute
instead, making follow-on changes to findAccount{1,2}PageContent
, and renaming the makeUnreadTopicInInbox
helper. Let's have a commit that just does that and removes the TODO, explaining in the commit message that it's not related to 737 after all.
If you'd like to go further in a follow-up commit (which I think is not a high priority), I'd have some feedback on that direction:
- Take care to include
skipOffstage: false
where necessary when checking for something on a route that's not the top of the stack. - I intentionally put
account1Route
(the one to be removed) underaccount2Route
in the stack, to guard against buggy implementations that might use something likeNavigatorState.popUntil
(which starts from the top and stops at the first route that shouldn't be removed, even if there are ones that should be removed under it). This would be a nice feature to keep, but I guess not essential.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This rewrite ended being a bit more substantial than just switching both accounts initial routes to the result of MessageListPage.buildRoute(). We do not use popUtil because the navigator stack normally should not become empty, so the HomePage route for account1 stays. Additionally, because we are pushing a different page route, we no longer need to set up different messages as the discriminator, further simplifying the test. See also: zulip#1183 (comment) Signed-off-by: Zixuan James Li <[email protected]>
The PR has been updated to use a different approach maintaining the navigator stack. We are reusing a code path that throws |
lib/model/store.dart
Outdated
@@ -19,6 +19,7 @@ import '../api/backoff.dart'; | |||
import '../api/route/realm.dart'; | |||
import '../log.dart'; | |||
import '../notifications/receive.dart'; | |||
import '../widgets/actions.dart'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prompted to look at this by #1257 (comment) . As I said there just now, let's avoid importing widgets/ from models/.
As the lib/widgets/actions.dart file's dartdoc says:
/// Methods that act through the Zulip API and show feedback in the UI.
///
/// The methods in this file can be thought of as higher-level wrappers for
/// some of the Zulip API endpoint binding methods in `lib/api/route/`.
/// But they don't belong in `lib/api/`, because they also interact with widgets
/// in order to present success or error feedback to the user through the UI.
the point of that file being in widgets/ is that its functions use widgets to give the user feedback. Since logOutAccount
and its callee unregisterToken
don't do that, they can move elsewhere.
An easy solution is to move those two functions to a file lib/model/actions.dart
, i.e. under model/
.
I think ultimately the right organization is probably that these belong as methods on GlobalStore, after all the more local-storage-focused methods that also act on accounts.
(Then in the future, as a matter of code organization, all those accounts-oriented methods' implementations will live on an AccountStore or AccountsStore, but the way one calls them will still be via GlobalStore — much like the existing ChannelStore and MessageStore etc. help keep PerAccountStore organized.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Echoing the last paragraph of #1183 (comment), I assume the reason that we don't move it to GlobalStore
right away is to prevent cluttering the class before we carry out the code reorganization you mentioned. The plan of moving it to lib/model/actions.dart
sounds good to me (along with the BuildContext
parameter to GlobalStore
refactor).
This rewrite ended being a bit more substantial than just switching both accounts initial routes to the result of MessageListPage.buildRoute(). We do not use popUtil because the navigator stack normally should not become empty, so the HomePage route for account1 stays. Additionally, because we are pushing a different page route, we no longer need to set up different messages as the discriminator, further simplifying the test. See also: zulip#1183 (comment) Signed-off-by: Zixuan James Li <[email protected]>
Updated the PR to resolve conflicts and move |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Comments below, including one where I got something unexpected in manual testing.
lib/log.dart
Outdated
void defaultReportErrorToUserBriefly(String? message, {String? details}) { | ||
/// Show the user a dismissable error message in a modal popup. | ||
/// | ||
/// Typically this shows a [AlertDialog] containing the message. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: "an [AlertDialog]"
lib/model/store.dart
Outdated
// The API key is invalid and the store can never be loaded | ||
// unless the user retries manually. | ||
final zulipLocalizations = GlobalLocalizations.zulipLocalizations; | ||
final account = getAccount(accountId)!; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will throw if there is no account, which can legitimately happen if the account is logged out during await doLoadPerAccount(accountId)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about: get the realm URL before the await doLoadPerAccount(accountId)
, after the assert(_accounts.containsKey(accountId))
, and store it in a variable?
Then also either skip the logOutAccount
if getAccount(accountId)
is null, or else investigate logOutAccount
's TODO(log)
for removal. (We won't want noise in Sentry from things that are expected to happen.)
lib/model/store.dart
Outdated
if (!_accounts.containsKey(accountId)) { | ||
// [removeAccount] was called during [doLoadPerAccount]. | ||
store.dispose(); | ||
// [removeAccount] was called during or after [doLoadPerAccount]. | ||
store?.dispose(); | ||
throw AccountNotFoundException(); | ||
} | ||
return store; | ||
return store!; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code gets harder to understand, since now it has to be read for both the success case and the INVALID_API_KEY
case. How about leaving this code untouched and keep the INVALID_API_KEY
case self-contained in its case
in the catch
? For that, I think we'd just need to throw AccountNotFoundException()
at the end of that case; there's no store
that we have to clean up.
lib/model/store.dart
Outdated
} on AccountNotFoundException { | ||
// Cannot recover by replacing the store because the account | ||
// was logged out. | ||
return; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we assert(_disposed)
here too?
Also curious whether this is better to do here or in _handlePollError
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cannot assert(_disposed)
here because the reload might not have happened if there was an error (likely during _reloadPerAccount
) that prevents the store from being disposed.
I chose to not handle it in _handlePollError
because of the assert(_disposed)
that follows the _handlePollError
call. This way we also maintain that _handlePollError
has the store replaced. An alternative would be returning a bool
from _handlePollError
, indicating if the store gets disposed.
It turned out that my claims aren't accurate. The store does get disposed: the account is logged out when loadPerAccount
handles the error, and the store subsequently gets disposed. We just need to be clear that _handlePollError
doesn't replace the store in this case.
Looking at _handlePollError
, I think can indeed handle the error inside, by just having the try
block contain the _reloadPerAccount
call, since that's where the AccountNotFoundException
might come from (the underlying endpoint is /register
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having tested this further, there are two cases _handlePollError
should be aware of:
- The
_reloadPerAccount
call (POST /register
) can fail due to an invalid API key. Handling this is necessary because otherwise it is a runtime error. - The
Object error
parameter, i.e. the error from polling (GET /events
) can be aZulipApiException
with HTTP status code 401, indicating the invalid API key error.
Without handling it, it is marked as unexpected in thedefault
branch under theswitch
, but we are still able to recover as long as we have error handling for the_reloadPerAccount
call.
To handle the second case, we just need to acknowledge that such an error is expected and will be dealt with later when we try to replace the event queue.
Another way is to log out the account before trying to replace the event queue. Not sure which way is better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's defer the handling of GET /events
failure to #1054.
test/model/store_test.dart
Outdated
@@ -843,6 +858,25 @@ void main() { | |||
check(store.debugMessageListViews).isEmpty(); | |||
})); | |||
|
|||
test('log out if fail to reload on unexpected errors', () => awaitFakeAsync((async) async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this test description should mention INVALID_API_KEY
; how about:
unexpected poll error, but reload fails with
INVALID_API_KEY
; log out
lib/model/store.dart
Outdated
assert(debugLog('Backing off, then will retry…')); | ||
// TODO tell user if initial-fetch errors persist, or look non-transient | ||
// TODO(#890): tell user if initial-fetch errors persist, or look non-transient | ||
switch (e) { | ||
case ZulipApiException(code: 'INVALID_API_KEY'): | ||
// We cannot recover from this error through retrying. | ||
// Leave it to [GlobalStore.loadPerAccount]. | ||
rethrow; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The log line 'Backing off, then will retry…' becomes inaccurate in some cases; can you adjust so that doesn't happen?
lib/model/store.dart
Outdated
// TODO tell user if initial-fetch errors persist, or look non-transient | ||
// TODO(#890): tell user if initial-fetch errors persist, or look non-transient | ||
switch (e) { | ||
case ZulipApiException(code: 'INVALID_API_KEY'): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In manual testing on a dev server, where I changed an account's API key, the code I got was actually 'UNAUTHORIZED', not 'INVALID_API_KEY'. Have you seen 'INVALID_API_KEY' in manual testing? If you haven't done manual testing of the new functionality, please do that. 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see! Didn't realize that my prior testing was limited to throwing ZulipApiException
from certain places, but not with a dev server. Will do that. Thanks for pointing it out! 🥲
ee10619
to
2859759
Compare
The PR has been updated. Thanks for the review!
Do you mean the case when the event queue expires but the auth keys aren't valid? It works for me when I leave the app in the background after invalidating the API key. |
Yeah; (1) in your commit message:
Which I guess, thinking more about it, doesn't actually require the event queue expiring, right, because polling could fail for other reasons—including an invalid-API-key error :)
Great! |
Yeah, a |
Yeah, that makes sense. If default:
assert(debugLog('BUG: Unexpected error in event polling: $error\n' // TODO(log)
'Replacing event queue…'));
_reportToUserErrorConnectingToServer(error);
// Similar story to the _EventHandlingException case;
// separate only so that that other case can print more context.
// The bug here could be in the server if it's an ApiRequestException,
// but our remedy is the same either way.
isUnexpected = true; If so, that seems wrong; this is something we expect to happen, though rarely, and the |
Yes. I was thinking of deferring that to #1054 (#1183 (comment)). For now, there is a TODO comment for this: 7334cca. We can also handle this by assuming that this is expected, and leave to |
This allows us to call it from model code when GlobalStore is available. Signed-off-by: Zixuan James Li <[email protected]>
This is more about testing the implementation of PerAccountStoreWidget to handle routeToRemoveOnLogout, instead of logOutAccount. Signed-off-by: Zixuan James Li <[email protected]>
plus moving and refactoring the tests to match Signed-off-by: Zixuan James Li <[email protected]>
Ah I see—thanks for reminding me of where you discussed this. :) I think deferring it to #1054 is fine. |
So, LGTM; marking for Greg's review. |
f72a133
to
6bd13ee
Compare
`PerAccountStore` shouldn't be an owner of the `MessageListView` objects. Its relationship to `MessageListView` is similar to that of `AutocompleteViewManager` to `MentionAutocompleteView` (zulip#645). With two owners, `MessageListView` can be disposed twice when `removeAccount` is called (when the user logs out, for example): 1. Before the frame is rendered, after removing the `PerAccountStore` from `GlobalStore`, `removeAccount` disposes the `PerAccountStore` , which disposes the `MessageListView` (via `MessageStoreImpl`). `removeAccount` also notifies the listeners of `GlobalStore`. At this point `_MessageListState` is not yet disposed. 2. Being dependent on `GlobalStore`, `PerAccountStoreWidget` is rebuilt. This time, the StatefulElement corresponding to the `MessageList` widget, is no longer in the element tree because `PerAccountStoreWidget` cannot find the account and builds a placeholder instead. 3. During finalization, `_MessageListState` tries to dispose the `MessageListView`, and fails to do so. We couldn't've kept `MessageStoreImpl.dispose` with an assertion `_messageListView.isEmpty`, because `PerAccountStore` is disposed before `_MessageListState.dispose` (and similarily `_MessageListState.onNewStore`) is called. Fixing that will be a future follow-up to this, as noted in the TODO comment. See discussion: https://chat.zulip.org/#narrow/channel/243-mobile-team/topic/MessageListView.20lifecycle/near/2083074 Signed-off-by: Zixuan James Li <[email protected]>
Signed-off-by: Zixuan James Li <[email protected]>
This highlights the API choice that the callback signature allows the caller to clear/cancel the reported errors, by passing `null` for the `message` parameter, drawing distinction from a later added variant that does not allow this. Signed-off-by: Zixuan James Li <[email protected]>
Signed-off-by: Zixuan James Li <[email protected]>
Signed-off-by: Zixuan James Li <[email protected]>
Coming up with a realistic test case doesn't actually require invalidating API key. Because the goal is to use routes that exist in the app (`InboxPageBody` has become a part of `HomePage` and doesn't exist on its own), we can set up HomePage and MessageListPage instead. Signed-off-by: Zixuan James Li <[email protected]>
Signed-off-by: Zixuan James Li <[email protected]>
Signed-off-by: Zixuan James Li <[email protected]>
Signed-off-by: Zixuan James Li <[email protected]>
The method loadPerAccount has two call sites, i.e. places where we send register-queue requests: 1. _reloadPerAccount through [UpdateMachine._handlePollError] 2. perAccount through [PerAccountStoreWidget] (the common case) We utilize the existing [AccountNotFoundException] because invalidating invalid auth keys effectively logs out the account, that it can no longer be found in the store. [PerAccountStoreWidget] already expects this error by ignoring it and assuming that the route will be removed: ```dart try { // If this succeeds, globalStore will notify listeners, and // [didChangeDependencies] will run again, this time in the // `store != null` case above. await globalStore.perAccount(widget.accountId); } on AccountNotFoundException { // 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 // (usually by using [routeToRemoveOnLogout]). } ``` (included because unchanged code is not in the diff) To handle 1, we apply the same expectation that the account gets logged out when the exception happens. For `_handlePollError`, while the event queue was not replaced at all, the end result of having the old store disposed is still expected. This partly addresses zulip#890 by handling authentication errors for register-queue. Fixes: zulip#737 Signed-off-by: Zixuan James Li <[email protected]>
This is near term fix for a user-reported issue:https://chat.zulip.org/#narrow/channel/48-mobile/topic/0.2E0.2E19.20Flutter.20.3A.20Cant.20connect.20to.20self.20hosted.20instance/near/2004042
It is not intended to be the full fix. With a better UX, we would bring the user back to the choose-account page without them manually doing so. That's covered by #737 but out-of-scope for this commit.This cherry-picks #1235, and partially addresses #890, by handling
INVALID_API_KEY
errors from thePerAccountStoreWidget
.Fixes: