[$250] Hybrid - Android - WS Switcher - App is closed when switching workspace and using device back button.
If you havenβt already, check out our contributing guidelines for onboarding and email [email protected] to request to join our Slack channel!
Version Number: 9.0.71-0 Reproducible in staging?: Yes Reproducible in production?: No If this was caught on HybridApp, is this reproducible on New Expensify Standalone?: No, reproducible on hybrid only If this was caught during regression testing, add the test name, ID and link from TestRail: N/A Email or phone of affected tester (no customers): [email protected] Issue reported by: Applause Internal Team
Action Performed:
- Launch the Expensify app.
- Tap on the workspace switcher on the top left corner.
- Select any workspace.
- Use the device back button.
- Verify that now all the chats are displayed again on inbox.
Expected Result:
After switching workspaces and using device back button, all the chats should be displayed again on inbox.
Actual Result:
The app is closed instead of displaying all chats again, when the user switch workspaces and uses device back button. When the app is reopened, all chats are displayed in inbox.
Workaround:
Unknown
Platforms:
- [ ] Android: Standalone
- [x] Android: HybridApp
- [ ] Android: mWeb Chrome
- [ ] iOS: Standalone
- [ ] iOS: HybridApp
- [ ] iOS: mWeb Safari
- [ ] MacOS: Chrome / Safari
- [ ] MacOS: Desktop
Screenshots/Videos
https://github.com/user-attachments/assets/d0d7c9cf-0123-4df6-a844-d8083efbebe8
Upwork Automation - Do Not Edit
- Upwork Job URL: https://www.upwork.com/jobs/~021864885051135538956
- Upwork Job ID: 1864885051135538956
- Last Price Increase: 2024-12-06
Issue Owner
Current Issue Owner: @eh2077
Triggered auto assignment to @RachCHopkins (Bug), see https://stackoverflow.com/c/expensify/questions/14418 for more details. Please add this bug to a GH project, as outlined in the SO.
Can Repro - When I hit the device back button it closes the app entirely. I am on 9.0.71-1
Job added to Upwork: https://www.upwork.com/jobs/~021864885051135538956
Triggered auto assignment to Contributor-plus team member for initial proposal review - @eh2077 (External)
Proposal
Please re-state the problem that we are trying to solve in this issue
When the user switches to a specific workspace chats and then uses device back button, the app is closed instead of displaying all chats again (Expensify).
What is the root cause of that problem?
[!note] The issue is present on Android: Native as well as on Android: HybridApp (more details below).
In the WorkspaceSwitcherPage we have the selectPolicy function which is called when we switch to a specific workspace chats. If we follow Navigation.navigateWithSwitchPolicyID down to source we arrive to the switchPolicyID function where at the bottom we get to the root cause of why, when we use the device back button the app will close instead of returning back to all chats (Expensify):
https://github.com/Expensify/App/blob/b595f5d24141a403fcc3f44c73d00cd6951dca7d/src/libs/Navigation/switchPolicyID.ts#L150-L156
as this block comes into action on narrow layout devices because of this condition:
https://github.com/Expensify/App/blob/b595f5d24141a403fcc3f44c73d00cd6951dca7d/src/libs/Navigation/switchPolicyID.ts#L115-L116
Because that block pops all central pane routes out in order for the bottom tab navigator to be visible, this means that the only route present in the stack is BottomTabNavigator which is the same as when we open the app -> which when pressing hardware / device back button it will close the app as expected because there's no other route to go back to.
What's the code responsible for handling the hardware / device back button and close the app on Android ?
We have this android platform specific function setupCustomAndroidBackHandler which is used to dictate the behaviour and if we look at its code, there is this block which is Android: HybridApp specific that tells the app to close when isLastScreenOnStack is true which is the case for our issue:
https://github.com/Expensify/App/blob/b595f5d24141a403fcc3f44c73d00cd6951dca7d/src/libs/Navigation/setupCustomAndroidBackHandler/index.android.ts#L27-L29
For Android: Native though we don't have any code in this function to specifically handle behaviour, therefore it defaults to the standard behaviour (close the app) at the end of the function:
https://github.com/Expensify/App/blob/b595f5d24141a403fcc3f44c73d00cd6951dca7d/src/libs/Navigation/setupCustomAndroidBackHandler/index.android.ts#L66-L67
What changes do you think we should make in order to solve the problem?
To solve our issue on both Android: Native and HybridApp we have to implement the following changes:
- Under: https://github.com/Expensify/App/blob/b595f5d24141a403fcc3f44c73d00cd6951dca7d/src/libs/Navigation/setupCustomAndroidBackHandler/index.android.ts#L25
add the following logic:
const isWorkspaceHomeOnlyRoute = bottomTabRoutes.every((route) => route.name === SCREENS.HOME && !!(route.params as {policyID?: string})?.policyID);
if (isLastScreenOnStack && isWorkspaceHomeOnlyRoute) {
navigationRef.dispatch({...StackActions.replace(SCREENS.HOME)});
return true;
}
Explanation
isWorkspaceHomeOnlyRoutechecks whether inbottomTabRoutes(array) every Home route has thepolicyIDparam- If
isLastScreenOnStackandisWorkspaceHomeOnlyRouteare bothtrue- this will always be the case when we are on the specific workspace chats home screen since there is only one route in the stack and it is the Home route withpolicyIDparam - When we go back with device back button and this condition is
true-> we will replace the Home screen withpolicyIDparam with the Home screen without any params, which is the Expensify home page with all chats.
This will fix the issue as required by the Expected result (see video below) because when we will perform the go back action we will have the Home screen in the stack and will be navigated to Home (all chats), then if we go back again from Home then the app will close as expected.
Additional explanation regarding Android: HybridApp -> under the new logic added above we will keep the Android: HybridApp logic unchanged since in case the newly added condition doesn't pass it means we want to keep the existing NativeModules.HybridAppModule.exitApp() logic because without this I assume the app will not close as the Android: Native app does by default.
cc @adamgrzybowski @Kicu for a take on the proposed solution as they worked on setupCustomAndroidBackHandler
Result video (before / after)
Android: Native
| Before | After |
|---|---|
Proposal
Please re-state the problem that we are trying to solve in this issue.
Android - WS Switcher - App is closed when switching workspace and using device back button.
What is the root cause of that problem?
- The issue originates from this commit in the PR: Migrate E/App to use PlatformStackNavigation.
- Prior to this change, the problem did not exist.
- The root cause is the use of the
Replacenavigation method instead ofPush. - When switching to another workspace and pressing the back button, the app closes because the stack contains only one entry, which triggers this condition.
What changes do you think we should make in order to solve the problem?
- When switching workspaces from
WorkspaceSwitcherPage, we should use thePushnavigation method instead ofReplace. To implement we need: - Update the getActionForBottomTabNavigator function
- Add an optional prop shouldReplaceHome with a default value of true here https://github.com/Expensify/App/blob/7925c9b555af60156c62947ae63b517d6c31587f/src/libs/Navigation/switchPolicyID.ts#L29
- And here https://github.com/Expensify/App/blob/7925c9b555af60156c62947ae63b517d6c31587f/src/libs/Navigation/switchPolicyID.ts#L76 https://github.com/Expensify/App/blob/7925c9b555af60156c62947ae63b517d6c31587f/src/libs/Navigation/switchPolicyID.ts#L102 https://github.com/Expensify/App/blob/7925c9b555af60156c62947ae63b517d6c31587f/src/libs/Navigation/switchPolicyID.ts#L29
- Update the navigateWithSwitchPolicyID function in WorkspaceSwitcherPage:
- Pass
shouldReplaceHome: false. - This ensures the navigation method is
Pushinstead ofReplacewhen called from this screen. - (Optional) Add a condition to ensure this change only applies to Android.
- Pass
Results
### Demo Videos| Before | After |
|---|---|
What specific scenarios should we cover in automated tests to prevent reintroducing this issue in the future?
Because this issue come from this, we need to check other related cases.
What alternative solutions did you explore? (Optional)
N/A
Proposal
Please re-state the problem that we are trying to solve in this issue.
App closes when switching workspaces and pressing the back button, instead of showing Inbox.
What is the root cause of that problem?
Navigation stack isn't updated correctly, causing the app to exit on back press.
What changes do you think we should make in order to solve the problem?
The first block updates the stack to push SCREENS.HOME or SCREENS.INBOX after workspace switch, directing the user to the Inbox or Home screen. The second block handles back button presses, ensuring the app navigates to Inbox instead of exiting. The third block updates the central pane and handles the navigation stack based on the layout.
- Handle Back Button Navigation Correctly
if (checkIfActionPayloadNameIsEqual(actionForBottomTabNavigator, SCREENS.HOME)) {
delete params.reportID;
// Ensure the navigation stack is updated to include SCREENS.HOME or SCREENS.INBOX
root.dispatch({
type: CONST.NAVIGATION.ACTION_TYPE.PUSH,
payload: {
name: SCREENS.HOME, // Or SCREENS.INBOX if needed
params,
},
});
}
- Prevent App Exit on Back Press
// Handle back press fallback to prevent app exit
root.dispatch({
type: CONST.NAVIGATION.ACTION_TYPE.PUSH,
payload: {
name: SCREENS.INBOX, // Redirect to Inbox instead of closing the app
params: {},
},
});
- Central Pane Updates Correctly
if (shouldAddToCentralPane) {
root.dispatch({
type: CONST.NAVIGATION.ACTION_TYPE.PUSH,
payload: {
name: topmostCentralPaneRoute?.name || SCREENS.HOME, // Default to HOME if undefined
params,
},
});
} else {
// If the layout is narrow, pop the stack to show the bottom tab navigator
root.dispatch({
type: 'POP_TO_TOP',
target: rootState.key,
});
}
What specific scenarios should we cover in automated tests to prevent reintroducing this issue in the future?
- Test workspace switching and back button behavior.
- Verify stack integrity after multiple switches.
- Test error scenarios (e.g., offline, app backgrounding).
What alternative solutions did you explore? (Optional)
- Frontend fallback to redirect to Inbox.
- Backend consistency improvements.
- Store previous navigation stack.
@RachCHopkins, @eh2077 Whoops! This issue is 2 days overdue. Let's get this updated quick!
Reviewing proposals
I am unsure why we decided to use setParams for switching the policyID. It replaces an entry in history instead of pushing a new screen with a new policy which is an additive action. This change seems to be the root cause of this problem.
But if we assume that switching policy with setParams is the way, then your @ikevin127 proposal makes sense to me.
One thing worth mentioning is that if we go
global > policyX > policyY and then back, the policy I expect would be "policyX" and the outcome is the policy will be set to global.
However, I'm not sure this is that important, considering that with the changes to setParams, history no longer works for policies anyway.
Alternatively, the second proposal by @linhvovan29546 suggests going back to the old method with PUSH instead of REPLACE. That makes even more sense to me as it should fix the history but we would need to understand which problem was supposed to be solved by using setParams
cc: @mountiny you seem to have some context about native stacks @WojtekBoman as policy police
// If there already is a 'Home' route, we want to change the params rather than pushing a new 'Home' route, // so that the screen does not get re-mounted. This would cause an empty screen/white flash when navigating back from the workspace switcher.
I think that @chrispader @kirillzyusko added this to fix a bug they encountered, but I agree that if this ruins the history its not the best solution and we might want to rethink it.
But we need to make sure there is no regression after switching back to that.
But if we assume that switching policy with
setParamsis the way
I think that's the way as it was done on purpose in this PR:
- https://github.com/Expensify/App/pull/53102
This is the reason why I mentioned using replace in my proposal because based on that recent PR I assumed we don't want to keep history of switching in-between workspaces.
But if we do, my proposal can be flexible and details like replacing the navigation method with push instead of my proposal's suggested replace can surely be handled during PR.
I don't consider other proposals mentioning push instead of replace a significant enough difference from my proposal to constitute a new proposal given our contributor guidelines mentioning:
Note: Before submitting a proposal on an issue, be sure to read any other existing proposals. ALL NEW PROPOSALS MUST BE DIFFERENT FROM EXISTING PROPOSALS. The difference should be important, meaningful or considerable.
Another reason would be that the Expected result clearly states:
After switching workspaces and using device back button, all the chats should be displayed again on inbox.
and there were no details mentioning the expected behaviour when switching between multiple workspaces then attempting the hardware go back button.
I can throw my 2 cents here, I was responsible for making sure workspace switcher behaves correctly in search pages.
I think at this point in the app (imo) we are sitting on the fence. Some places we treat WS switcher like a global switch/filter (so replace) but in others we treat it much more like a part of search history and (important) we keep it in URL.
When I was working on search I made the policyID work correctly with the data from URL, so that these steps work:
- go to Inbox
- choose a policy (id:
1) via workspace switcher - go to Search (via bottom bar)
- in search policy is the same
- choose a different policy (id:
2) (it changes in the URL) - press back button
- you land on Inbox with policy
1because that was in the history
So as you can see at least in βοΈ flow, policyID behaves more like stacked history.
I don't have strong opinions which is better, but if we move into the direction of replace then we should check other places in the app and make them behave like that as well.
I agree with @Kicu I think that the policyId should be kept in history so you can go back to the previously chosen policy
This was just an oversight on our part as its quite niche flow @chrispader @kirillzyusko Could you please weigh in when you get a moment?
I think that @chrispader @kirillzyusko added this to fix a bug they encountered, but I agree that if this ruins the history its not the best solution and we might want to rethink it.
But we need to make sure there is no regression after switching back to that.
@mountiny I think the code changes to fix this issue. For the blank screen issue, it seems to be related to native-stack. However I can fix it by running Navigation.navigateWithSwitchPolicyID inside requestAnimationFrame in the JS.
@mountiny I think the problem comes from the changes made by @chrispader? If so I would wait when @chrispader returns from its vacation and can shed more light on the issue (because I'm not aware what @chrispader was attempting to solve)
For me it looks like there was a Home screen that was re-mounting somehow and @chrispader fixed it via replace calls, but it produced other issues.
After reading this conversation I think that the fix that @chrispader made could produce https://github.com/Expensify/App/issues/53359 as well (or at the problem is very highly related to each other).
Also what @Kicu explains makes sense to me. So maybe we just need to go back with push approach (assuming it will fix this problem) and check that a reported issue not reproducible anymore? In this case this:
I think the code changes to fix https://github.com/Expensify/App/pull/49937#issuecomment-2470562319. For the blank screen issue, it seems to be related to native-stack. However I can fix it by running Navigation.navigateWithSwitchPolicyID inside requestAnimationFrame in the JS.
Makes more sense for me/sounds like a better approach π
Yes, so the problem i was trying to solve was that we would see a blank HomeScreen for a brief moment the when we would change the workspace, because the screen is re-mounted and therefore re-renders.
Under the previous stack navigation, this was not causing issues, but with @react-navigation/native-stack we can see that the mounting and rendering of the screen content is still ongoing while the screen is already displayed.
I was not aware, that we want to be able to keep a "workspace history" that we can go back to. So the fix with setParams is not going to work in this case.
This is the original i and @kirillzyusko are referring to, reported by @ishpaul777: https://github.com/Expensify/App/pull/49937#issuecomment-2466304096
https://github.com/user-attachments/assets/1a2dac4c-627a-44c1-b836-b5427058ffbf
@mountiny I think the code changes to fix this issue. For the blank screen issue, it seems to be related to
native-stack. However I can fix it by running Navigation.navigateWithSwitchPolicyID insiderequestAnimationFramein the JS.
Putting Navigation.navigateWithSwitchPolicyID in requestAnimationFrame() doesn't fix the problem for me. This is what i get:
https://github.com/user-attachments/assets/1a257df9-03b3-4047-afc5-bb45f1488460
A fix that removes the blank screen but basically just delays the route push and the content re-rendering is putting the Navigation.navigateWithSwitchPolicyID in an InteractionManager.runAfterInteractions(...). This is not as beautiful/sexy UI-wise as it was before NativeStack, but it doesn't look as weird as a blank screen:
https://github.com/user-attachments/assets/d975f37b-1e72-482b-a4cb-1afef9b7dbf8
wdyt? @mountiny @kirillzyusko
@kirillzyusko i can confirm that the changes discussed are also causing https://github.com/Expensify/App/issues/53359. This change also fixes the issue on the search page:
https://github.com/user-attachments/assets/27f59b33-c014-4e87-af27-e64029039363
Not overdue, valuable discussions continue!
@mountiny @eh2077 is somebody already assigned/working on this? Lmk if i should prepare a PR, otherwise here are the changes needed to revert my previous changes and apply the InteractionManager.runAfterInteractions() fix:
https://github.com/Expensify/App/pull/54030
@chrispader Cool! The InteractionManager.runAfterInteractions() solution also makes sense to me. I think we can move this forward with @chrispader 's fix here https://github.com/Expensify/App/pull/54030
Edited: We can revisit this issue after reworking the original PR.
@mountiny All yours!
πππ C+ reviewed
Triggered auto assignment to @Beamanator, see https://stackoverflow.com/c/expensify/questions/7972 for more details.
Sounds good, lets make sure this is working fine on web too
https://github.com/Expensify/App/pull/54030 is ready for review!
β οΈ Looks like this issue was linked to a Deploy Blocker here
If you are the assigned CME please investigate whether the linked PR caused a regression and leave a comment with the results.
If a regression has occurred and you are the assigned CM follow the instructions here.
If this regression could have been avoided please consider also proposing a recommendation to the PR checklist so that we can avoid it in the future.
Reviewing label has been removed, please complete the "BugZero Checklist".
The solution for this issue has been :rocket: deployed to production :rocket: in version 9.0.78-6 and is now subject to a 7-day regression period :calendar:. Here is the list of pull requests that resolve this issue:
- https://github.com/Expensify/App/pull/54030
If no regressions arise, payment will be issued on 2025-01-02. :confetti_ball:
For reference, here are some details about the assignees on this issue:
- @chrispader does not require payment (Contractor)
- @eh2077 requires payment through NewDot Manual Requests
@eh2077 @RachCHopkins @eh2077 The PR fixing this issue has been merged! The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the BugZero Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]