-
Notifications
You must be signed in to change notification settings - Fork 3.9k
clean up the deep link code #17452
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
clean up the deep link code #17452
Changes from all commits
db996cb
cc48aaf
728cc18
f1f19a7
4945eb1
6dc06df
cff9643
3c73416
a129009
cb5f434
1f252d6
5f71937
a737600
c7b323d
c5ee140
d9a3cbc
5e2a6d9
54ee01c
0349229
d858603
65b0f7c
4105086
edbae30
637af52
bb0ddd0
8913f88
03d69bb
bdaf961
8758e31
144abda
9e7f595
fbdbb7d
96be874
ac8ad4e
f9f89da
e465908
92faead
5a66cbc
b0adfd3
3faec2b
4ed6c97
e195537
e31d0bd
7d70e7b
d5937cc
4a69d1e
be41317
57ef6bd
db7a9cb
0c87306
8c7c2c4
b6b672f
1298b0f
2bb7f31
61acb1b
80101fa
6d3bdce
bf37347
3120a38
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; |
| 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 | ||
|
|
@@ -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(); | ||
|
tgolen marked this conversation as resolved.
|
||
| params.set('exitTo', `${window.location.pathname}${window.location.search}${window.location.hash}`); | ||
| if (email && shortLivedAuthToken) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this means that either both of them are passed or none of them are?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, These two are the required parameters for signing in. If these two parameters are not present, such as when the login page is opened in a browser, the use can also open the login page in the desktop app via deep-link. This is consistent with our production environment. : ) |
||
| 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}; | ||
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.
NAB, is it really "between" apps though? We are actually coming from OldDot I think and don't use this screen to navigate to OldDot. This change makes it more ambiguous IMO and doesn't seem necessary.