1import Constants from 'expo-constants'; 2import qs from 'qs'; 3import URL from 'url-parse'; 4 5import { CreateURLOptions, ParsedURL } from './Linking.types'; 6import { hasCustomScheme, resolveScheme } from './Schemes'; 7import { validateURL } from './validateURL'; 8 9function getHostUri(): string | null { 10 if (Constants.expoConfig?.hostUri) { 11 return Constants.expoConfig.hostUri; 12 } else if (!hasCustomScheme()) { 13 // we're probably not using up-to-date xdl, so just fake it for now 14 // we have to remove the /--/ on the end since this will be inserted again later 15 return removeScheme(Constants.linkingUri).replace(/\/--($|\/.*$)/, ''); 16 } else { 17 return null; 18 } 19} 20 21function isExpoHosted(): boolean { 22 const hostUri = getHostUri(); 23 return !!( 24 hostUri && 25 (/^(.*\.)?(expo\.io|exp\.host|exp\.direct|expo\.test|expo\.dev)(:.*)?(\/.*)?$/.test(hostUri) || 26 Constants.expoGoConfig?.developer) 27 ); 28} 29 30function removeScheme(url: string): string { 31 return url.replace(/^[a-zA-Z0-9+.-]+:\/\//, ''); 32} 33 34function removePort(url: string): string { 35 return url.replace(/(?=([a-zA-Z0-9+.-]+:\/\/)?[^/]):\d+/, ''); 36} 37 38function removeLeadingSlash(url: string): string { 39 return url.replace(/^\//, ''); 40} 41 42function removeTrailingSlashAndQueryString(url: string): string { 43 return url.replace(/\/?\?.*$/, ''); 44} 45 46function ensureLeadingSlash(input: string, shouldAppend: boolean): string { 47 const hasSlash = input.startsWith('/'); 48 if (hasSlash && !shouldAppend) { 49 return input.substring(1); 50 } else if (!hasSlash && shouldAppend) { 51 return `/${input}`; 52 } 53 return input; 54} 55 56// @needsAudit 57/** 58 * Helper method for constructing a deep link into your app, given an optional path and set of query 59 * parameters. Creates a URI scheme with two slashes by default. 60 * 61 * The scheme in bare and standalone must be defined in the Expo config (`app.config.js` or `app.json`) 62 * under `expo.scheme`. 63 * 64 * # Examples 65 * - Bare: `<scheme>://path` - uses provided scheme or scheme from Expo config `scheme`. 66 * - Standalone, Custom: `yourscheme://path` 67 * - Web (dev): `https://localhost:19006/path` 68 * - Web (prod): `https://myapp.com/path` 69 * - Expo Client (dev): `exp://128.0.0.1:8081/--/path` 70 * - Expo Client (prod): `exp://exp.host/@yourname/your-app/--/path` 71 * 72 * @param path Addition path components to append to the base URL. 73 * @param namedParameters Additional options object. 74 * @return A URL string which points to your app with the given deep link information. 75 */ 76export function createURL( 77 path: string, 78 { scheme, queryParams = {}, isTripleSlashed = false }: CreateURLOptions = {} 79): string { 80 const resolvedScheme = resolveScheme({ scheme }); 81 82 let hostUri = getHostUri() || ''; 83 84 if (hasCustomScheme() && isExpoHosted()) { 85 hostUri = ''; 86 } 87 88 if (path) { 89 if (isExpoHosted() && hostUri) { 90 path = `/--/${removeLeadingSlash(path)}`; 91 } 92 if (isTripleSlashed && !path.startsWith('/')) { 93 path = `/${path}`; 94 } 95 } else { 96 path = ''; 97 } 98 99 // merge user-provided query params with any that were already in the hostUri 100 // e.g. release-channel 101 let queryString = ''; 102 const queryStringMatchResult = hostUri.match(/(.*)\?(.+)/); 103 if (queryStringMatchResult) { 104 hostUri = queryStringMatchResult[1]; 105 queryString = queryStringMatchResult[2]; 106 let paramsFromHostUri = {}; 107 try { 108 const parsedParams = qs.parse(queryString); 109 if (typeof parsedParams === 'object') { 110 paramsFromHostUri = parsedParams; 111 } 112 } catch {} 113 queryParams = { 114 ...queryParams, 115 ...paramsFromHostUri, 116 }; 117 } 118 queryString = qs.stringify(queryParams); 119 if (queryString) { 120 queryString = `?${queryString}`; 121 } 122 123 hostUri = ensureLeadingSlash(hostUri, !isTripleSlashed); 124 125 return encodeURI( 126 `${resolvedScheme}:${isTripleSlashed ? '/' : ''}/${hostUri}${path}${queryString}` 127 ); 128} 129 130// @needsAudit 131/** 132 * Helper method for parsing out deep link information from a URL. 133 * @param url A URL that points to the currently running experience (e.g. an output of `Linking.createURL()`). 134 * @return A `ParsedURL` object. 135 */ 136export function parse(url: string): ParsedURL { 137 validateURL(url); 138 139 const parsed = URL(url, /* parseQueryString */ true); 140 141 for (const param in parsed.query) { 142 parsed.query[param] = decodeURIComponent(parsed.query[param]!); 143 } 144 const queryParams = parsed.query; 145 146 const hostUri = getHostUri() || ''; 147 const hostUriStripped = removePort(removeTrailingSlashAndQueryString(hostUri)); 148 149 let path = parsed.pathname || null; 150 let hostname = parsed.hostname || null; 151 let scheme = parsed.protocol || null; 152 153 if (scheme) { 154 // Remove colon at end 155 scheme = scheme.substring(0, scheme.length - 1); 156 } 157 158 if (path) { 159 path = removeLeadingSlash(path); 160 161 let expoPrefix: string | null = null; 162 if (hostUriStripped) { 163 const parts = hostUriStripped.split('/'); 164 expoPrefix = parts.slice(1).concat(['--/']).join('/'); 165 } 166 167 if (isExpoHosted() && !hasCustomScheme() && expoPrefix && path.startsWith(expoPrefix)) { 168 path = path.substring(expoPrefix.length); 169 hostname = null; 170 } else if (path.indexOf('+') > -1) { 171 path = path.substring(path.indexOf('+') + 1); 172 } 173 } 174 175 return { 176 hostname, 177 path, 178 queryParams, 179 scheme, 180 }; 181} 182