Skip to content
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

Poc/split adapted state #106

Merged
merged 3 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/libs/Navigation/Navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
import isReportOpenInRHP from './isReportOpenInRHP';
import linkingConfig from './linkingConfig';
import getMatchingBottomTabRouteForState from './linkingConfig/getMatchingBottomTabRouteForState';
import linkTo from './linkTo';
import navigationRef from './navigationRef';
import linkTo from './newLinkTo';
import setNavigationActionToMicrotaskQueue from './setNavigationActionToMicrotaskQueue';
import type {NavigationStateRoute, RootStackParamList, State, StateOrRoute} from './types';

Expand Down Expand Up @@ -185,10 +185,10 @@
return;
}
// linkTo(navigationRef.current, route, type, isActiveRoute(route));
linkTo(navigationRef.current, route, type, isActiveRoute(route));

Check failure on line 188 in src/libs/Navigation/Navigation.ts

View workflow job for this annotation

GitHub Actions / typecheck

Expected 2-3 arguments, but got 4.
}

function newGoBack(fallbackRoute?: Route, shouldEnforceFallback = false, shouldPopToTop = false) {

Check failure on line 191 in src/libs/Navigation/Navigation.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

'newGoBack' is defined but never used

Check failure on line 191 in src/libs/Navigation/Navigation.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

'shouldEnforceFallback' is assigned a value but never used
if (!canNavigate('goBack')) {
return;
}
Expand Down
234 changes: 36 additions & 198 deletions src/libs/Navigation/linkTo/index.ts
Original file line number Diff line number Diff line change
@@ -1,223 +1,61 @@
import {getActionFromState} from '@react-navigation/core';
import type {NavigationContainerRef, NavigationState, PartialState} from '@react-navigation/native';
import {findFocusedRoute} from '@react-navigation/native';
import omitBy from 'lodash/omitBy';
import getIsNarrowLayout from '@libs/getIsNarrowLayout';
import isReportOpenInRHP from '@libs/Navigation/isReportOpenInRHP';
import {isCentralPaneName} from '@libs/NavigationUtils';
import shallowCompare from '@libs/ObjectUtils';
import {extractPolicyIDFromPath, getPathWithoutPolicyID} from '@libs/PolicyUtils';
import getActionsFromPartialDiff from '@navigation/AppNavigator/getActionsFromPartialDiff';
import getPartialStateDiff from '@navigation/AppNavigator/getPartialStateDiff';
import dismissModal from '@navigation/dismissModal';
import extractPolicyIDFromQuery from '@navigation/extractPolicyIDFromQuery';
import extrapolateStateFromParams from '@navigation/extrapolateStateFromParams';
import getPolicyIDFromState from '@navigation/getPolicyIDFromState';
import {shallowCompare} from '@libs/ObjectUtils';
import {getPathWithoutPolicyID} from '@libs/PolicyUtils';
import getStateFromPath from '@navigation/getStateFromPath';
import getTopmostBottomTabRoute from '@navigation/getTopmostBottomTabRoute';
import getTopmostCentralPaneRoute from '@navigation/getTopmostCentralPaneRoute';
import getTopmostReportId from '@navigation/getTopmostReportId';
import isSideModalNavigator from '@navigation/isSideModalNavigator';
import linkingConfig from '@navigation/linkingConfig';
import getAdaptedStateFromPath from '@navigation/linkingConfig/getAdaptedStateFromPath';
import getMatchingBottomTabRouteForState from '@navigation/linkingConfig/getMatchingBottomTabRouteForState';
import getMatchingCentralPaneRouteForState from '@navigation/linkingConfig/getMatchingCentralPaneRouteForState';
import replacePathInNestedState from '@navigation/linkingConfig/replacePathInNestedState';
import type {NavigationRoot, RootStackParamList, StackNavigationAction, State} from '@navigation/types';
import type {RootStackParamList, StackNavigationAction} from '@navigation/types';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import type {Route} from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import getActionForBottomTabNavigator from './getActionForBottomTabNavigator';
import getMinimalAction from './getMinimalAction';
import type {ActionPayloadParams} from './types';

export default function linkTo(navigation: NavigationContainerRef<RootStackParamList> | null, path: Route, type?: string, isActiveRoute?: boolean) {
if (!navigation) {
throw new Error("Couldn't find a navigation object. Is your component inside a screen in a navigator?");
}
let root: NavigationRoot = navigation;
let current: NavigationRoot | undefined;
// Traverse up to get the root navigation
// eslint-disable-next-line no-cond-assign
while ((current = root.getParent())) {
root = current;
}

const pathWithoutPolicyID = getPathWithoutPolicyID(`/${path}`) as Route;
const rootState = navigation.getRootState() as NavigationState<RootStackParamList>;
const stateFromPath = getStateFromPath(pathWithoutPolicyID) as PartialState<NavigationState<RootStackParamList>>;
// Creating path with /w/ included if necessary.
const topmostCentralPaneRoute = getTopmostCentralPaneRoute(rootState);

const extractedPolicyID = extractPolicyIDFromPath(`/${path}`);
const policyIDFromState = getPolicyIDFromState(rootState);
const policyID = extractedPolicyID ?? policyIDFromState;
const lastRoute = rootState?.routes?.at(-1);
function shouldDispatchAction(currentState: NavigationState<RootStackParamList>, stateFromPath: PartialState<NavigationState<RootStackParamList>>) {
const currentFocusedRoute = findFocusedRoute(currentState);
const targetFocusedRoute = findFocusedRoute(stateFromPath);

const isNarrowLayout = getIsNarrowLayout();
const areNamesEqual = currentFocusedRoute?.name === targetFocusedRoute?.name;
const areParamsEqual = shallowCompare(currentFocusedRoute?.params as Record<string, unknown> | undefined, targetFocusedRoute?.params as Record<string, unknown> | undefined);

const isWorkspaceScreenOnTop = lastRoute?.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR;

// policyID on SCREENS.SEARCH.CENTRAL_PANE can be present only as part of SearchQuery, while on other pages it's stored in the url in the format: /w/:policyID/
if (policyID && !isWorkspaceScreenOnTop && !policyIDFromState) {
// The stateFromPath doesn't include proper path if there is a policy passed with /w/id.
// We need to replace the path in the state with the proper one.
// To avoid this hacky solution we may want to create custom getActionFromState function in the future.
replacePathInNestedState(stateFromPath, `/w/${policyID}${pathWithoutPolicyID}`);
if (areNamesEqual && areParamsEqual) {
return false;
}

const action: StackNavigationAction = getActionFromState(stateFromPath, linkingConfig.config);

const isReportInRhpOpened = isReportOpenInRHP(rootState);

// If action type is different than NAVIGATE we can't change it to the PUSH safely
if (action?.type === CONST.NAVIGATION.ACTION_TYPE.NAVIGATE) {
const actionPayloadParams = action.payload.params as ActionPayloadParams;

const topRouteName = lastRoute?.name;

// CentralPane screens aren't nested in any navigator, if actionPayloadParams?.screen is undefined, it means the screen name and parameters have to be read directly from action.payload
const targetName = actionPayloadParams?.screen ?? action.payload.name;
const targetParams = actionPayloadParams?.params ?? actionPayloadParams;
const isTargetNavigatorOnTop = topRouteName === action.payload.name;

const isTargetScreenDifferentThanCurrent = !!(!topmostCentralPaneRoute || topmostCentralPaneRoute.name !== targetName);
const areParamsDifferent =
targetName === SCREENS.REPORT
? getTopmostReportId(rootState) !== getTopmostReportId(stateFromPath)
: !shallowCompare(
omitBy(topmostCentralPaneRoute?.params as Record<string, unknown> | undefined, (value) => value === undefined),
omitBy(targetParams as Record<string, unknown> | undefined, (value) => value === undefined),
);

// If this action is navigating to the report screen and the top most navigator is different from the one we want to navigate - PUSH the new screen to the top of the stack by default
if (isCentralPaneName(action.payload.name) && (isTargetScreenDifferentThanCurrent || areParamsDifferent)) {
// We need to push a tab if the tab doesn't match the central pane route that we are going to push.
const topmostBottomTabRoute = getTopmostBottomTabRoute(rootState);

const focusedRoute = findFocusedRoute(stateFromPath);
const policyIDFromQuery = extractPolicyIDFromQuery(focusedRoute);
const matchingBottomTabRoute = getMatchingBottomTabRouteForState(stateFromPath, policyID ?? policyIDFromQuery);
const isOpeningSearch = matchingBottomTabRoute.name === SCREENS.SEARCH.BOTTOM_TAB;
const isNewPolicyID =
((topmostBottomTabRoute?.params as Record<string, string | undefined>)?.policyID ?? '') !==
((matchingBottomTabRoute?.params as Record<string, string | undefined>)?.policyID ?? '');

if (topmostBottomTabRoute && (topmostBottomTabRoute.name !== matchingBottomTabRoute.name || isNewPolicyID || isOpeningSearch)) {
root.dispatch({
type: CONST.NAVIGATION.ACTION_TYPE.PUSH,
payload: matchingBottomTabRoute,
});
}

if (type === CONST.NAVIGATION.TYPE.UP) {
action.type = CONST.NAVIGATION.ACTION_TYPE.REPLACE;
} else {
action.type = CONST.NAVIGATION.ACTION_TYPE.PUSH;
}

// If the type is UP, we deeplinked into one of the RHP flows and we want to replace the current screen with the previous one in the flow
// and at the same time we want the back button to go to the page we were before the deeplink
} else if (type === CONST.NAVIGATION.TYPE.UP) {
if (!areParamsDifferent && isSideModalNavigator(lastRoute?.name) && topmostCentralPaneRoute?.name === targetName) {
dismissModal(navigation);
return;
}
action.type = CONST.NAVIGATION.ACTION_TYPE.REPLACE;

// If this action is navigating to ModalNavigator or WorkspaceNavigator and the last route on the root navigator is not already opened Navigator then push
} else if ((action.payload.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR || isSideModalNavigator(action.payload.name)) && !isTargetNavigatorOnTop) {
if (isSideModalNavigator(topRouteName)) {
dismissModal(navigation);
}

// If this RHP has mandatory central pane and bottom tab screens defined we need to push them.
const {adaptedState, metainfo} = getAdaptedStateFromPath(path, linkingConfig.config);
if (adaptedState && (metainfo.isCentralPaneAndBottomTabMandatory || metainfo.isWorkspaceNavigatorMandatory)) {
const diff = getPartialStateDiff(rootState, adaptedState as State<RootStackParamList>, metainfo);
const diffActions = getActionsFromPartialDiff(diff);
for (const diffAction of diffActions) {
root.dispatch(diffAction);
}
}
// All actions related to FullScreenNavigator on wide screen are pushed when comparing differences between rootState and adaptedState.
if (action.payload.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR) {
return;
}
action.type = CONST.NAVIGATION.ACTION_TYPE.PUSH;
return true;
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
} else if (action.payload.name === NAVIGATORS.BOTTOM_TAB_NAVIGATOR) {
// If path contains a policyID, we should invoke the navigate function
const shouldNavigate = !!extractedPolicyID;
const actionForBottomTabNavigator = getActionForBottomTabNavigator(action, rootState, policyID, shouldNavigate);
export default function linkTo(navigation: NavigationContainerRef<RootStackParamList> | null, path: Route, type?: typeof CONST.NAVIGATION.ACTION_TYPE.REPLACE) {
if (!navigation) {
throw new Error("Couldn't find a navigation object. Is your component inside a screen in a navigator?");
}

if (!actionForBottomTabNavigator) {
return;
}
const pathWithoutPolicyID = getPathWithoutPolicyID(`/${path}`) as Route;

root.dispatch(actionForBottomTabNavigator);
// This is the state generated with the default getStateFromPath function.
// It won't include the whole state that will be generated for this path but the focused route will be correct.
// It is necessary because getActionFromState will generate RESET action for whole state generated with our custom getStateFromPath function.
const stateFromPath = getStateFromPath(pathWithoutPolicyID) as PartialState<NavigationState<RootStackParamList>>;
const currentState = navigation.getRootState() as NavigationState<RootStackParamList>;
const action: StackNavigationAction = getActionFromState(stateFromPath, linkingConfig.config);

// If the layout is wide we need to push matching central pane route to the stack.
if (!isNarrowLayout) {
// stateFromPath should always include bottom tab navigator state, so getMatchingCentralPaneRouteForState will be always defined.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const matchingCentralPaneRoute = getMatchingCentralPaneRouteForState(stateFromPath, rootState)!;
if (matchingCentralPaneRoute && 'name' in matchingCentralPaneRoute) {
root.dispatch({
type: CONST.NAVIGATION.ACTION_TYPE.PUSH,
payload: {
name: matchingCentralPaneRoute.name,
params: matchingCentralPaneRoute.params,
},
});
}
} else {
// If the layout is small we need to pop everything from the central pane so the bottom tab navigator is visible.
root.dispatch({
type: 'POP_TO_TOP',
target: rootState.key,
});
}
return;
}
// We don't want to dispatch action to push/replace with exactly the same route that is already focused.
if (!shouldDispatchAction(currentState, stateFromPath)) {
return;
}

if (action && 'payload' in action && action.payload && 'name' in action.payload && isSideModalNavigator(action.payload.name)) {
// Information about the state may be in the params.
const currentFocusedRoute = findFocusedRoute(extrapolateStateFromParams(rootState));
const targetFocusedRoute = findFocusedRoute(stateFromPath);

// If the current focused route is the same as the target focused route, we don't want to navigate.
if (
currentFocusedRoute?.name === targetFocusedRoute?.name &&
shallowCompare(currentFocusedRoute?.params as Record<string, string | undefined>, targetFocusedRoute?.params as Record<string, string | undefined>)
) {
return;
}

const minimalAction = getMinimalAction(action, navigation.getRootState());
if (minimalAction) {
// There are situations where a route already exists on the current navigation stack
// But we want to push the same route instead of going back in the stack
// Which would break the user navigation history
if (!isActiveRoute && type === CONST.NAVIGATION.ACTION_TYPE.PUSH) {
minimalAction.type = CONST.NAVIGATION.ACTION_TYPE.PUSH;
}
root.dispatch(minimalAction);
return;
}
// If there is no action, just reset the whole state.
if (!action) {
navigation.resetRoot(stateFromPath);
return;
}

// When we navigate from the ReportScreen opened in RHP, this page shouldn't be removed from the navigation state to allow users to go back to it.
if (isReportInRhpOpened && action) {
if (type === CONST.NAVIGATION.ACTION_TYPE.REPLACE) {
action.type = CONST.NAVIGATION.ACTION_TYPE.REPLACE;
} else if (action.type === CONST.NAVIGATION.ACTION_TYPE.NAVIGATE) {
// We want to PUSH by default to add entries to the browser history.
action.type = CONST.NAVIGATION.ACTION_TYPE.PUSH;
}

if (action !== undefined) {
root.dispatch(action);
} else {
root.reset(stateFromPath);
}
const minimalAction = getMinimalAction(action, navigation.getRootState());
navigation.dispatch(minimalAction);
}
25 changes: 25 additions & 0 deletions src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import SCREENS from '@src/SCREENS';

const SEARCH_TO_RHP: string[] = [
SCREENS.SEARCH.REPORT_RHP,
SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_CURRENCY_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_DATE_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_DESCRIPTION_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_MERCHANT_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_REPORT_ID_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_AMOUNT_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_CATEGORY_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_KEYWORD_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_TAX_RATE_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_EXPENSE_TYPE_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_TAG_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_FROM_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_TO_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_IN_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_CARD_RHP,
SCREENS.SEARCH.SAVED_SEARCH_RENAME_RHP,
];

export default SEARCH_TO_RHP;
62 changes: 62 additions & 0 deletions src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import SCREENS from '@src/SCREENS';

// const CENTRAL_PANE_TO_RHP_MAPPING: Partial<Record<keyof SettingsSplitNavigatorParamList, string[]>> = {
const CENTRAL_PANE_TO_RHP_MAPPING: Record<string, string[]> = {
[SCREENS.SETTINGS.PROFILE.ROOT]: [
SCREENS.SETTINGS.PROFILE.DISPLAY_NAME,
SCREENS.SETTINGS.PROFILE.CONTACT_METHODS,
SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS,
SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_VALIDATE_ACTION,
SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD,
SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER,
SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_DATE,
SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_TIME,
SCREENS.SETTINGS.PROFILE.STATUS,
SCREENS.SETTINGS.PROFILE.PRONOUNS,
SCREENS.SETTINGS.PROFILE.TIMEZONE,
SCREENS.SETTINGS.PROFILE.TIMEZONE_SELECT,
SCREENS.SETTINGS.PROFILE.LEGAL_NAME,
SCREENS.SETTINGS.PROFILE.DATE_OF_BIRTH,
SCREENS.SETTINGS.PROFILE.ADDRESS,
SCREENS.SETTINGS.PROFILE.ADDRESS_COUNTRY,
SCREENS.SETTINGS.SHARE_CODE,
SCREENS.SETTINGS.EXIT_SURVEY.REASON,
SCREENS.SETTINGS.EXIT_SURVEY.RESPONSE,
SCREENS.SETTINGS.EXIT_SURVEY.CONFIRM,
],
[SCREENS.SETTINGS.PREFERENCES.ROOT]: [SCREENS.SETTINGS.PREFERENCES.PRIORITY_MODE, SCREENS.SETTINGS.PREFERENCES.LANGUAGE, SCREENS.SETTINGS.PREFERENCES.THEME],
[SCREENS.SETTINGS.WALLET.ROOT]: [
SCREENS.SETTINGS.WALLET.DOMAIN_CARD,
SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.NAME,
SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.PHONE,
SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.ADDRESS,
SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.CONFIRM,
SCREENS.SETTINGS.WALLET.TRANSFER_BALANCE,
SCREENS.SETTINGS.WALLET.CHOOSE_TRANSFER_ACCOUNT,
SCREENS.SETTINGS.WALLET.ENABLE_PAYMENTS,
SCREENS.SETTINGS.WALLET.CARD_ACTIVATE,
SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD,
SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS,
],
[SCREENS.SETTINGS.SECURITY]: [
SCREENS.SETTINGS.TWO_FACTOR_AUTH,
SCREENS.SETTINGS.CLOSE,
SCREENS.SETTINGS.DELEGATE.ADD_DELEGATE,
SCREENS.SETTINGS.DELEGATE.DELEGATE_ROLE,
SCREENS.SETTINGS.DELEGATE.DELEGATE_CONFIRM,
SCREENS.SETTINGS.DELEGATE.DELEGATE_MAGIC_CODE,
],
[SCREENS.SETTINGS.ABOUT]: [SCREENS.SETTINGS.APP_DOWNLOAD_LINKS],
[SCREENS.SETTINGS.SAVE_THE_WORLD]: [SCREENS.I_KNOW_A_TEACHER, SCREENS.INTRO_SCHOOL_PRINCIPAL, SCREENS.I_AM_A_TEACHER],
[SCREENS.SETTINGS.TROUBLESHOOT]: [SCREENS.SETTINGS.CONSOLE],
[SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: [
SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD,
SCREENS.SETTINGS.SUBSCRIPTION.SIZE,
SCREENS.SETTINGS.SUBSCRIPTION.DISABLE_AUTO_RENEW_SURVEY,
SCREENS.SETTINGS.SUBSCRIPTION.REQUEST_EARLY_CANCELLATION,
SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY,
SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_PAYMENT_CURRENCY,
],
};

export default CENTRAL_PANE_TO_RHP_MAPPING;
Loading
Loading