-
Notifications
You must be signed in to change notification settings - Fork 3.1k
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
Desktop deeplink #13606
Desktop deeplink #13606
Changes from all commits
ec5becf
b7c02f5
c3d7186
b1b9ba6
90a4e1b
5a4093d
d55e7dc
b703499
18ef187
f987ce5
b9bfa2b
2ca2f5d
4610615
2f4f957
14484bc
8e650d7
2936aea
c87eaf4
c52e525
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 |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import ROUTES from '../../ROUTES'; | ||
|
||
/** @type {Array<object>} Routes regex used for desktop deeplinking */ | ||
export default [ | ||
{ | ||
// /reports/* | ||
pattern: `/${ROUTES.REPORT}($|(//*))`, | ||
}, | ||
{ | ||
// /settings/* | ||
pattern: `/${ROUTES.SETTINGS}($|(//*))`, | ||
}, | ||
{ | ||
// /setpassword/* | ||
pattern: '/setpassword($|(//*))', | ||
}, | ||
{ | ||
// /details/* | ||
pattern: `/${ROUTES.DETAILS}($|(//*))`, | ||
}, | ||
{ | ||
// /v/* | ||
pattern: '/v($|(//*))', | ||
}, | ||
{ | ||
// /bank-account/* | ||
pattern: `/${ROUTES.BANK_ACCOUNT}($|(//*))`, | ||
}, | ||
{ | ||
// /iou/* | ||
pattern: '/iou($|(//*))', | ||
}, | ||
{ | ||
// /enable-payments/* | ||
pattern: `/${ROUTES.ENABLE_PAYMENTS}($|(//*))`, | ||
}, | ||
{ | ||
// /statements/* | ||
pattern: '/statements($|(//*))', | ||
}, | ||
{ | ||
// /concierge/* | ||
pattern: `/${ROUTES.CONCIERGE}($|(//*))`, | ||
}, | ||
]; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import PropTypes from 'prop-types'; | ||
import {PureComponent} from 'react'; | ||
|
||
const propTypes = { | ||
/** Children to render. */ | ||
children: PropTypes.node.isRequired, | ||
}; | ||
|
||
class DeeplinkWrapper extends PureComponent { | ||
render() { | ||
return this.props.children; | ||
} | ||
} | ||
|
||
DeeplinkWrapper.propTypes = propTypes; | ||
export default DeeplinkWrapper; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
import _ from 'underscore'; | ||
import {View} from 'react-native'; | ||
import PropTypes from 'prop-types'; | ||
import React, {PureComponent} from 'react'; | ||
import deeplinkRoutes from './deeplinkRoutes'; | ||
import FullScreenLoadingIndicator from '../FullscreenLoadingIndicator'; | ||
import TextLink from '../TextLink'; | ||
import * as Illustrations from '../Icon/Illustrations'; | ||
import withLocalize, {withLocalizePropTypes} from '../withLocalize'; | ||
import Text from '../Text'; | ||
import styles from '../../styles/styles'; | ||
import CONST from '../../CONST'; | ||
import CONFIG from '../../CONFIG'; | ||
import Icon from '../Icon'; | ||
import * as Expensicons from '../Icon/Expensicons'; | ||
import colors from '../../styles/colors'; | ||
import * as Browser from '../../libs/Browser'; | ||
|
||
const propTypes = { | ||
/** Children to render. */ | ||
children: PropTypes.node.isRequired, | ||
|
||
...withLocalizePropTypes, | ||
}; | ||
|
||
class DeeplinkWrapper extends PureComponent { | ||
constructor(props) { | ||
super(props); | ||
|
||
this.state = { | ||
appInstallationCheckStatus: this.isMacOSWeb() ? CONST.DESKTOP_DEEPLINK_APP_STATE.CHECKING : CONST.DESKTOP_DEEPLINK_APP_STATE.NOT_INSTALLED, | ||
}; | ||
} | ||
|
||
componentDidMount() { | ||
if (!this.isMacOSWeb()) { | ||
return; | ||
} | ||
|
||
let focused = true; | ||
|
||
window.addEventListener('blur', () => { | ||
focused = false; | ||
}); | ||
|
||
NikkiWines marked this conversation as resolved.
Show resolved
Hide resolved
|
||
setTimeout(() => { | ||
if (!focused) { | ||
this.setState({appInstallationCheckStatus: CONST.DESKTOP_DEEPLINK_APP_STATE.INSTALLED}); | ||
} else { | ||
this.setState({appInstallationCheckStatus: CONST.DESKTOP_DEEPLINK_APP_STATE.NOT_INSTALLED}); | ||
} | ||
}, 500); | ||
|
||
// check if pathname matches with deeplink routes | ||
const matchedRoute = _.find(deeplinkRoutes, (route) => { | ||
const routeRegex = new RegExp(route.pattern); | ||
return routeRegex.test(window.location.pathname); | ||
}); | ||
|
||
if (matchedRoute) { | ||
this.setState({deeplinkMatch: true}); | ||
this.openRouteInDesktopApp(); | ||
} else { | ||
this.setState({deeplinkMatch: false}); | ||
} | ||
} | ||
|
||
openRouteInDesktopApp() { | ||
const expensifyUrl = new URL(CONFIG.EXPENSIFY.NEW_EXPENSIFY_URL); | ||
const expensifyDeeplinkUrl = `${CONST.DEEPLINK_BASE_URL}${expensifyUrl.host}${window.location.pathname}`; | ||
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. We forgot to pass the query params here which caused a few URLs to break in our App. Here is the regression issue #15059 |
||
|
||
// 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 | ||
Comment on lines
+72
to
+74
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. Can you explain this one? I think we should open in the desktop app popup only if the Desktop app is available, other we should straight away load the web. cc: @NikkiWines 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. Slack with Desktop app installedScreen.Recording.2023-01-09.at.6.32.56.PM.movSlack without Desktop appScreen.Recording.2023-01-09.at.6.33.23.PM.movThere 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. Please note that this is Safari only thing. The videos you posted are in Chrome. 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. How does it work on safari for slack vs our app? 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. It works the same way. Slack doesn't automatically open their webapp like we're doing. They always display the "launching" screen. 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. We only show the "launching" screen if the user has the app installed, otherwise if will open the webapp automatically. There's only that Safari bug that was previously discussed. 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. Sorry, I miss understood the comment here. 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. No worries |
||
if (CONST.BROWSER.SAFARI === Browser.getBrowser()) { | ||
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(() => { | ||
NikkiWines marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (!iframe.parentNode) { | ||
return; | ||
} | ||
|
||
iframe.parentNode.removeChild(iframe); | ||
}, 100); | ||
} else { | ||
window.location.href = expensifyDeeplinkUrl; | ||
} | ||
} | ||
|
||
isMacOSWeb() { | ||
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. Why did we put this in the component instead of the 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. No specific reasons we just missed out on that Review. |
||
return !Browser.isMobile() && ( | ||
typeof navigator === 'object' | ||
&& typeof navigator.userAgent === 'string' | ||
&& /Mac/i.test(navigator.userAgent) | ||
&& !/Electron/i.test(navigator.userAgent) | ||
); | ||
} | ||
|
||
render() { | ||
if (this.state.appInstallationCheckStatus === CONST.DESKTOP_DEEPLINK_APP_STATE.CHECKING) { | ||
return <FullScreenLoadingIndicator style={styles.flex1} />; | ||
} | ||
|
||
if ( | ||
this.state.deeplinkMatch | ||
&& this.state.appInstallationCheckStatus === CONST.DESKTOP_DEEPLINK_APP_STATE.INSTALLED | ||
) { | ||
return ( | ||
<View style={styles.deeplinkWrapperContainer}> | ||
<View style={styles.deeplinkWrapperMessage}> | ||
<View style={styles.mb2}> | ||
<Icon | ||
width={200} | ||
height={164} | ||
src={Illustrations.RocketBlue} | ||
/> | ||
</View> | ||
<Text style={[styles.textHeadline, styles.textXXLarge]}> | ||
{this.props.translate('deeplinkWrapper.launching')} | ||
</Text> | ||
<View style={styles.mt2}> | ||
<Text style={[styles.fontSizeNormal, styles.textAlignCenter]}> | ||
{this.props.translate('deeplinkWrapper.redirectedToDesktopApp')} | ||
{'\n'} | ||
{this.props.translate('deeplinkWrapper.youCanAlso')} | ||
{' '} | ||
<TextLink onPress={() => this.setState({deeplinkMatch: false})}> | ||
{this.props.translate('deeplinkWrapper.openLinkInBrowser')} | ||
</TextLink> | ||
. | ||
</Text> | ||
</View> | ||
</View> | ||
<View style={styles.deeplinkWrapperFooter}> | ||
<Icon | ||
width={154} | ||
height={34} | ||
fill={colors.green} | ||
src={Expensicons.ExpensifyWordmark} | ||
/> | ||
</View> | ||
</View> | ||
); | ||
} | ||
|
||
return this.props.children; | ||
} | ||
} | ||
|
||
DeeplinkWrapper.propTypes = propTypes; | ||
export default withLocalize(DeeplinkWrapper); |
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.
@cowboycito
Wondering if can we construct this from the apple-site association file, instead of hard coding here as it may come in handy in the future.
cc: @NikkiWines
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, that sounds like a great call @Santhosh-Sellavel. Best to reduce duplication wherever possible
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 just tried importing that file to use it as a reference to construct the deeplink routes. 2 issues came up: it's outside the
src
directory and the file doesn't have an extension (which causes for webpack to not know how to load the file). I honestly don't think it's worth to go on that route. Any thoughts? @Santhosh-Sellavel @NikkiWinesThere 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 am not sure, I will wait for @NikkiWines
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.
Hmm, yeah looking into it more I can't find a simple route to get this to work. In which case I think this is fine as it is now.