Skip to content

Commit

Permalink
Merge pull request #17452 from ntdiary/clean-up-deep-link-code
Browse files Browse the repository at this point in the history
clean up the deep link code
  • Loading branch information
arosiclair authored Jul 18, 2023
2 parents 6197c8e + 3120a38 commit e776ac7
Show file tree
Hide file tree
Showing 19 changed files with 192 additions and 206 deletions.
3 changes: 0 additions & 3 deletions src/ONYXKEYS.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,6 @@ export default {
// Is app in beta version
IS_BETA: 'isBeta',

// Whether the auth token is valid
IS_TOKEN_VALID: 'isTokenValid',

// The theme setting set by the user in preferences.
// This can be either "light", "dark" or "system"
PREFERRED_THEME: 'preferredTheme',
Expand Down
2 changes: 1 addition & 1 deletion src/ROUTES.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export default {
getReportWelcomeMessageRoute: (reportID) => `r/${reportID}/welcomeMessage`,
REPORT_SETTINGS_WRITE_CAPABILITY: 'r/:reportID/settings/who-can-post',
getReportSettingsWriteCapabilityRoute: (reportID) => `r/${reportID}/settings/who-can-post`,
TRANSITION_FROM_OLD_DOT: 'transition',
TRANSITION_BETWEEN_APPS: 'transition',
VALIDATE_LOGIN: 'v/:accountID/:validateCode',
GET_ASSISTANCE: 'get-assistance/:taskID',
getGetAssistanceRoute: (taskID) => `get-assistance/${taskID}`,
Expand Down
2 changes: 1 addition & 1 deletion src/SCREENS.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default {
REPORT: 'Report',
REPORT_ATTACHMENTS: 'ReportAttachments',
NOT_FOUND: 'not-found',
TRANSITION_FROM_OLD_DOT: 'TransitionFromOldDot',
TRANSITION_BETWEEN_APPS: 'TransitionBetweenApps',
SETTINGS: {
PREFERENCES: 'Settings_Preferences',
},
Expand Down
141 changes: 13 additions & 128 deletions src/components/DeeplinkWrapper/index.website.js
Original file line number Diff line number Diff line change
@@ -1,157 +1,42 @@
import PropTypes from 'prop-types';
import React, {PureComponent} from 'react';
import {withOnyx} from 'react-native-onyx';
import FullScreenLoadingIndicator from '../FullscreenLoadingIndicator';
import styles from '../../styles/styles';
import {PureComponent} from 'react';
import Str from 'expensify-common/lib/str';
import * as Browser from '../../libs/Browser';
import ROUTES from '../../ROUTES';
import * as App from '../../libs/actions/App';
import CONST from '../../CONST';
import CONFIG from '../../CONFIG';
import * as Browser from '../../libs/Browser';
import ONYXKEYS from '../../ONYXKEYS';
import * as Authentication from '../../libs/Authentication';
import DeeplinkRedirectLoadingIndicator from './DeeplinkRedirectLoadingIndicator';
import * as Session from '../../libs/actions/Session';

const propTypes = {
/** Children to render. */
children: PropTypes.node.isRequired,

/** Session info for the currently logged-in user. */
session: PropTypes.shape({
/** Currently logged-in user email */
email: PropTypes.string,

/** Currently logged-in user authToken */
authToken: PropTypes.string,
}),
};

const defaultProps = {
session: {
email: '',
authToken: '',
},
};

class DeeplinkWrapper extends PureComponent {
constructor(props) {
super(props);

this.state = {
appInstallationCheckStatus:
this.isMacOSWeb() && CONFIG.ENVIRONMENT !== CONST.ENVIRONMENT.DEV ? CONST.DESKTOP_DEEPLINK_APP_STATE.CHECKING : CONST.DESKTOP_DEEPLINK_APP_STATE.NOT_INSTALLED,
shouldOpenLinkInBrowser: false,
};
this.focused = true;
this.openLinkInBrowser = this.openLinkInBrowser.bind(this);
}

componentDidMount() {
if (!this.isMacOSWeb() || CONFIG.ENVIRONMENT === CONST.ENVIRONMENT.DEV) {
return;
}

window.addEventListener('blur', () => {
this.focused = false;
});

const expensifyUrl = new URL(CONFIG.EXPENSIFY.NEW_EXPENSIFY_URL);
const params = new URLSearchParams();
params.set('exitTo', `${window.location.pathname}${window.location.search}${window.location.hash}`);
if (!this.props.session.authToken) {
const expensifyDeeplinkUrl = `${CONST.DEEPLINK_BASE_URL}${expensifyUrl.host}/transition?${params.toString()}`;
this.openRouteInDesktopApp(expensifyDeeplinkUrl);
return;
}

// There's no support for anonymous users on desktop
if (Session.isAnonymousUser()) {
// If the current url path is /transition..., meaning it was opened from oldDot, during this transition period:
// 1. The user session may not exist, because sign-in has not been completed yet.
// 2. There may be non-idempotent operations (e.g. create a new workspace), which obviously should not be executed again in the desktop app.
// So we need to wait until after sign-in and navigation are complete before starting the deeplink redirect.
if (Str.startsWith(window.location.pathname, Str.normalizeUrl(ROUTES.TRANSITION_BETWEEN_APPS))) {
App.beginDeepLinkRedirectAfterTransition();
return;
}

Authentication.getShortLivedAuthToken()
.then((shortLivedAuthToken) => {
params.set('email', this.props.session.email);
params.set('shortLivedAuthToken', `${shortLivedAuthToken}`);
const expensifyDeeplinkUrl = `${CONST.DEEPLINK_BASE_URL}${expensifyUrl.host}/transition?${params.toString()}`;
this.openRouteInDesktopApp(expensifyDeeplinkUrl);
})
.catch(() => {
// If the request is successful, we call the updateAppInstallationCheckStatus before the prompt pops up.
// If not, we only need to make sure that the state will be updated.
this.updateAppInstallationCheckStatus();
});
}

updateAppInstallationCheckStatus() {
setTimeout(() => {
if (!this.focused) {
this.setState({appInstallationCheckStatus: CONST.DESKTOP_DEEPLINK_APP_STATE.INSTALLED});
} else {
this.setState({appInstallationCheckStatus: CONST.DESKTOP_DEEPLINK_APP_STATE.NOT_INSTALLED});
}
}, 500);
}

openRouteInDesktopApp(expensifyDeeplinkUrl) {
this.updateAppInstallationCheckStatus();

const browser = Browser.getBrowser();

// This check is necessary for Safari, otherwise, if the user
// does NOT have the Expensify desktop app installed, it's gonna
// show an error in the page saying that the address is invalid
// It is also necessary for Firefox, otherwise the window.location.href redirect
// will abort the fetch request from NetInfo, which will cause the app to go offline temporarily.
if (browser === CONST.BROWSER.SAFARI || browser === CONST.BROWSER.FIREFOX) {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.contentWindow.location.href = expensifyDeeplinkUrl;

// Since we're creating an iframe for Safari to handle
// deeplink we need to give this iframe some time for
// it to do what it needs to do. After that we can just
// remove the iframe.
setTimeout(() => {
if (!iframe.parentNode) {
return;
}

iframe.parentNode.removeChild(iframe);
}, 100);
} else {
window.location.href = expensifyDeeplinkUrl;
}
App.beginDeepLinkRedirect();
}

isMacOSWeb() {
return !Browser.isMobile() && typeof navigator === 'object' && typeof navigator.userAgent === 'string' && /Mac/i.test(navigator.userAgent) && !/Electron/i.test(navigator.userAgent);
}

openLinkInBrowser() {
this.setState({shouldOpenLinkInBrowser: true});
}

shouldShowDeeplinkLoadingIndicator() {
const routeRegex = new RegExp(CONST.REGEX.ROUTES.VALIDATE_LOGIN);
return routeRegex.test(window.location.pathname);
}

render() {
if (this.state.appInstallationCheckStatus === CONST.DESKTOP_DEEPLINK_APP_STATE.CHECKING) {
return <FullScreenLoadingIndicator style={styles.flex1} />;
}

if (this.state.appInstallationCheckStatus === CONST.DESKTOP_DEEPLINK_APP_STATE.INSTALLED && this.shouldShowDeeplinkLoadingIndicator() && !this.state.shouldOpenLinkInBrowser) {
return <DeeplinkRedirectLoadingIndicator openLinkInBrowser={this.openLinkInBrowser} />;
}

return this.props.children;
}
}

DeeplinkWrapper.propTypes = propTypes;
DeeplinkWrapper.defaultProps = defaultProps;
export default withOnyx({
session: {key: ONYXKEYS.SESSION},
})(DeeplinkWrapper);
export default DeeplinkWrapper;
11 changes: 1 addition & 10 deletions src/libs/Authentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,4 @@ function reauthenticate(command = '') {
});
}

function getShortLivedAuthToken() {
return Network.post('OpenOldDotLink', {shouldRetry: false}).then((response) => {
if (response && response.shortLivedAuthToken) {
return Promise.resolve(response.shortLivedAuthToken);
}
return Promise.reject();
});
}

export {reauthenticate, Authenticate, getShortLivedAuthToken};
export {reauthenticate, Authenticate};
4 changes: 3 additions & 1 deletion src/libs/Browser/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ function isMobileChrome() {
return false;
}

export {getBrowser, isMobile, isMobileSafari, isMobileChrome};
function openRouteInDesktopApp() {}

export {getBrowser, isMobile, isMobileSafari, isMobileChrome, openRouteInDesktopApp};
42 changes: 41 additions & 1 deletion src/libs/Browser/index.web.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import CONST from '../../CONST';
import CONFIG from '../../CONFIG';

/**
* Fetch browser name from UA string
Expand Down Expand Up @@ -60,4 +61,43 @@ function isMobileChrome() {
return /Android/i.test(userAgent) && /chrome|chromium|crios/i.test(userAgent);
}

export {getBrowser, isMobile, isMobileSafari, isMobileChrome};
/**
* The session information needs to be passed to the Desktop app, and the only way to do that is by using query params. There is no other way to transfer the data.
* @param {String} shortLivedAuthToken
* @param {String} email
*/
function openRouteInDesktopApp(shortLivedAuthToken = '', email = '') {
const params = new URLSearchParams();
params.set('exitTo', `${window.location.pathname}${window.location.search}${window.location.hash}`);
if (email && shortLivedAuthToken) {
params.set('email', email);
params.set('shortLivedAuthToken', shortLivedAuthToken);
}
const expensifyUrl = new URL(CONFIG.EXPENSIFY.NEW_EXPENSIFY_URL);
const expensifyDeeplinkUrl = `${CONST.DEEPLINK_BASE_URL}${expensifyUrl.host}/transition?${params.toString()}`;

const browser = getBrowser();

// This check is necessary for Safari, otherwise, if the user
// does NOT have the Expensify desktop app installed, it's gonna
// show an error in the page saying that the address is invalid.
// It is also necessary for Firefox, otherwise the window.location.href redirect
// will abort the fetch request from NetInfo, which will cause the app to go offline temporarily.
if (browser === CONST.BROWSER.SAFARI || browser === CONST.BROWSER.FIREFOX) {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.contentWindow.location.href = expensifyDeeplinkUrl;

// Since we're creating an iframe for Safari to handle deeplink,
// we need to give Safari some time to open the pop-up window.
// After that we can just remove the iframe.
setTimeout(() => {
document.body.removeChild(iframe);
}, 0);
} else {
window.location.href = expensifyDeeplinkUrl;
}
}

export {getBrowser, isMobile, isMobileSafari, isMobileChrome, openRouteInDesktopApp};
7 changes: 6 additions & 1 deletion src/libs/Middleware/Reauthentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,12 @@ function Reauthentication(response, request, isFromSequentialQueue) {
// of the new response created by handleExpiredAuthToken.
const shouldRetry = lodashGet(request, 'data.shouldRetry');
const apiRequestType = lodashGet(request, 'data.apiRequestType');
if (!shouldRetry && !apiRequestType) {

// For the SignInWithShortLivedAuthToken command, if the short token expires, the server returns a 407 error,
// and credentials are still empty at this time, which causes reauthenticate to throw an error (requireParameters),
// and the subsequent SaveResponseInOnyx also cannot be executed, so we need this parameter to skip the reauthentication logic.
const skipReauthentication = lodashGet(request, 'data.skipReauthentication');
if ((!shouldRetry && !apiRequestType) || skipReauthentication) {
if (isFromSequentialQueue) {
return data;
}
Expand Down
2 changes: 1 addition & 1 deletion src/libs/Navigation/AppNavigator/AuthScreens.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ class AuthScreens extends React.Component {
}}
/>
<RootStack.Screen
name={SCREENS.TRANSITION_FROM_OLD_DOT}
name={SCREENS.TRANSITION_BETWEEN_APPS}
options={defaultScreenOptions}
getComponent={() => {
const LogOutPreviousUserPage = require('../../../pages/LogOutPreviousUserPage').default;
Expand Down
2 changes: 1 addition & 1 deletion src/libs/Navigation/AppNavigator/PublicScreens.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function PublicScreens() {
component={SignInPage}
/>
<RootStack.Screen
name={SCREENS.TRANSITION_FROM_OLD_DOT}
name={SCREENS.TRANSITION_BETWEEN_APPS}
options={defaultScreenOptions}
component={LogInWithShortLivedAuthTokenPage}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/libs/Navigation/linkingConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default {
// Main Routes
ValidateLogin: ROUTES.VALIDATE_LOGIN,
UnlinkLogin: ROUTES.UNLINK_LOGIN,
[SCREENS.TRANSITION_FROM_OLD_DOT]: ROUTES.TRANSITION_FROM_OLD_DOT,
[SCREENS.TRANSITION_BETWEEN_APPS]: ROUTES.TRANSITION_BETWEEN_APPS,
Concierge: ROUTES.CONCIERGE,
[SCREENS.REPORT_ATTACHMENTS]: ROUTES.REPORT_ATTACHMENTS,

Expand Down
Loading

0 comments on commit e776ac7

Please sign in to comment.