-
Notifications
You must be signed in to change notification settings - Fork 3.1k
/
Copy pathLink.ts
189 lines (163 loc) · 8.95 KB
/
Link.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
import Onyx from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import * as API from '@libs/API';
import type {GenerateSpotnanaTokenParams} from '@libs/API/parameters';
import {SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types';
import asyncOpenURL from '@libs/asyncOpenURL';
import * as Environment from '@libs/Environment/Environment';
import Navigation from '@libs/Navigation/Navigation';
import * as Url from '@libs/Url';
import CONFIG from '@src/CONFIG';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
import * as Session from './Session';
let isNetworkOffline = false;
Onyx.connect({
key: ONYXKEYS.NETWORK,
callback: (value) => (isNetworkOffline = value?.isOffline ?? false),
});
let currentUserEmail = '';
let currentUserAccountID = -1;
Onyx.connect({
key: ONYXKEYS.SESSION,
callback: (value) => {
currentUserEmail = value?.email ?? '';
currentUserAccountID = value?.accountID ?? -1;
},
});
function buildOldDotURL(url: string, shortLivedAuthToken?: string): Promise<string> {
const hasHashParams = url.indexOf('#') !== -1;
const hasURLParams = url.indexOf('?') !== -1;
const authTokenParam = shortLivedAuthToken ? `authToken=${shortLivedAuthToken}` : '';
const emailParam = `email=${encodeURIComponent(currentUserEmail)}`;
const paramsArray = [authTokenParam, emailParam];
const params = paramsArray.filter(Boolean).join('&');
return Environment.getOldDotEnvironmentURL().then((environmentURL) => {
const oldDotDomain = Url.addTrailingForwardSlash(environmentURL);
// If the URL contains # or ?, we can assume they don't need to have the `?` token to start listing url parameters.
return `${oldDotDomain}${url}${hasHashParams || hasURLParams ? '&' : '?'}${params}`;
});
}
/**
* @param shouldSkipCustomSafariLogic When true, we will use `Linking.openURL` even if the browser is Safari.
*/
function openExternalLink(url: string, shouldSkipCustomSafariLogic = false) {
asyncOpenURL(Promise.resolve(), url, shouldSkipCustomSafariLogic);
}
function openOldDotLink(url: string) {
if (isNetworkOffline) {
buildOldDotURL(url).then((oldDotURL) => openExternalLink(oldDotURL));
return;
}
// If shortLivedAuthToken is not accessible, fallback to opening the link without the token.
asyncOpenURL(
// eslint-disable-next-line rulesdir/no-api-side-effects-method
API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.OPEN_OLD_DOT_LINK, {}, {})
.then((response) => (response ? buildOldDotURL(url, response.shortLivedAuthToken) : buildOldDotURL(url)))
.catch(() => buildOldDotURL(url)),
(oldDotURL) => oldDotURL,
);
}
function buildTravelDotURL(spotnanaToken?: string, postLoginPath?: string): Promise<string> {
return Promise.all([Environment.getTravelDotEnvironmentURL(), Environment.getSpotnanaEnvironmentTMCID()]).then(([environmentURL, tmcID]) => {
const authCode = spotnanaToken ? `authCode=${spotnanaToken}` : '';
const redirectURL = postLoginPath ? `redirectUrl=${Url.addLeadingForwardSlash(postLoginPath)}` : '';
const tmcIDParam = `tmcId=${tmcID}`;
const paramsArray = [authCode, tmcIDParam, redirectURL];
const params = paramsArray.filter(Boolean).join('&');
const travelDotDomain = Url.addTrailingForwardSlash(environmentURL);
return `${travelDotDomain}auth/code?${params}`;
});
}
/**
* @param postLoginPath When provided, we will redirect the user to this path post login on travelDot. eg: 'trips/:tripID'
*/
function openTravelDotLink(policyID: OnyxEntry<string>, postLoginPath?: string) {
if (policyID === null || policyID === undefined) {
return;
}
const parameters: GenerateSpotnanaTokenParams = {
policyID,
};
asyncOpenURL(
// eslint-disable-next-line rulesdir/no-api-side-effects-method
API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.GENERATE_SPOTNANA_TOKEN, parameters, {})
.then((response) => (response?.spotnanaToken ? buildTravelDotURL(response.spotnanaToken, postLoginPath) : buildTravelDotURL()))
.catch(() => buildTravelDotURL()),
(travelDotURL) => travelDotURL,
);
}
function getInternalNewExpensifyPath(href: string) {
const attrPath = Url.getPathFromURL(href);
return (Url.hasSameExpensifyOrigin(href, CONST.NEW_EXPENSIFY_URL) || Url.hasSameExpensifyOrigin(href, CONST.STAGING_NEW_EXPENSIFY_URL) || href.startsWith(CONST.DEV_NEW_EXPENSIFY_URL)) &&
!CONST.PATHS_TO_TREAT_AS_EXTERNAL.find((path) => attrPath.startsWith(path))
? attrPath
: '';
}
function getInternalExpensifyPath(href: string) {
const attrPath = Url.getPathFromURL(href);
const hasExpensifyOrigin = Url.hasSameExpensifyOrigin(href, CONFIG.EXPENSIFY.EXPENSIFY_URL) || Url.hasSameExpensifyOrigin(href, CONFIG.EXPENSIFY.STAGING_API_ROOT);
if (!hasExpensifyOrigin || attrPath.startsWith(CONFIG.EXPENSIFY.CONCIERGE_URL_PATHNAME) || attrPath.startsWith(CONFIG.EXPENSIFY.DEVPORTAL_URL_PATHNAME)) {
return '';
}
return attrPath;
}
function openLink(href: string, environmentURL: string, isAttachment = false) {
const hasSameOrigin = Url.hasSameExpensifyOrigin(href, environmentURL);
const hasExpensifyOrigin = Url.hasSameExpensifyOrigin(href, CONFIG.EXPENSIFY.EXPENSIFY_URL) || Url.hasSameExpensifyOrigin(href, CONFIG.EXPENSIFY.STAGING_API_ROOT);
const internalNewExpensifyPath = getInternalNewExpensifyPath(href);
const internalExpensifyPath = getInternalExpensifyPath(href);
// There can be messages from Concierge with links to specific NewDot reports. Those URLs look like this:
// https://www.expensify.com.dev/newdotreport?reportID=3429600449838908 and they have a target="_blank" attribute. This is so that when a user is on OldDot,
// clicking on the link will open the chat in NewDot. However, when a user is in NewDot and clicks on the concierge link, the link needs to be handled differently.
// Normally, the link would be sent to Link.openOldDotLink() and opened in a new tab, and that's jarring to the user. Since the intention is to link to a specific NewDot chat,
// the reportID is extracted from the URL and then opened as an internal link, taking the user straight to the chat in the same tab.
if (hasExpensifyOrigin && href.indexOf('newdotreport?reportID=') > -1) {
const reportID = href.split('newdotreport?reportID=').pop();
const reportRoute = ROUTES.REPORT_WITH_ID.getRoute(reportID ?? '-1');
Navigation.navigate(reportRoute);
return;
}
// If we are handling a New Expensify link then we will assume this should be opened by the app internally. This ensures that the links are opened internally via react-navigation
// instead of in a new tab or with a page refresh (which is the default behavior of an anchor tag)
if (internalNewExpensifyPath && hasSameOrigin) {
if (Session.isAnonymousUser() && !Session.canAnonymousUserAccessRoute(internalNewExpensifyPath)) {
Session.signOutAndRedirectToSignIn();
return;
}
Navigation.navigate(internalNewExpensifyPath as Route);
return;
}
// If we are handling an old dot Expensify link we need to open it with openOldDotLink() so we can navigate to it with the user already logged in.
// As attachments also use expensify.com we don't want it working the same as links.
if (internalExpensifyPath && !isAttachment) {
openOldDotLink(internalExpensifyPath);
return;
}
openExternalLink(href);
}
function buildURLWithAuthToken(url: string, shortLivedAuthToken?: string) {
const authTokenParam = shortLivedAuthToken ? `shortLivedAuthToken=${shortLivedAuthToken}` : '';
const emailParam = `email=${encodeURIComponent(currentUserEmail)}`;
const exitTo = `exitTo=${url}`;
const accountID = `accountID=${currentUserAccountID}`;
const paramsArray = [accountID, emailParam, authTokenParam, exitTo];
const params = paramsArray.filter(Boolean).join('&');
return `${CONFIG.EXPENSIFY.NEW_EXPENSIFY_URL}transition?${params}`;
}
/**
* @param shouldSkipCustomSafariLogic When true, we will use `Linking.openURL` even if the browser is Safari.
*/
function openExternalLinkWithToken(url: string, shouldSkipCustomSafariLogic = false) {
asyncOpenURL(
// eslint-disable-next-line rulesdir/no-api-side-effects-method
API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.OPEN_OLD_DOT_LINK, {}, {})
.then((response) => (response ? buildURLWithAuthToken(url, response.shortLivedAuthToken) : buildURLWithAuthToken(url)))
.catch(() => buildURLWithAuthToken(url)),
(link) => link,
shouldSkipCustomSafariLogic,
);
}
export {buildOldDotURL, openOldDotLink, openExternalLink, openLink, getInternalNewExpensifyPath, getInternalExpensifyPath, openTravelDotLink, buildTravelDotURL, openExternalLinkWithToken};