expo-linking
Version:
Create and open deep links universally
196 lines (170 loc) • 6.27 kB
text/typescript
import Constants from 'expo-constants';
import { CreateURLOptions, ParsedURL } from './Linking.types';
import { hasCustomScheme, resolveScheme } from './Schemes';
import { validateURL } from './validateURL';
function getHostUri(): string | null {
if (Constants.expoConfig?.hostUri) {
return Constants.expoConfig.hostUri;
} else if (!hasCustomScheme()) {
// we're probably not using up-to-date xdl, so just fake it for now
// we have to remove the /--/ on the end since this will be inserted again later
return removeScheme(Constants.linkingUri).replace(/\/--($|\/.*$)/, '');
} else {
return null;
}
}
function isExpoHosted(): boolean {
const hostUri = getHostUri();
return !!(
hostUri &&
(/^(.*\.)?(expo\.io|exp\.host|exp\.direct|expo\.test|expo\.dev)(:.*)?(\/.*)?$/.test(hostUri) ||
Constants.expoGoConfig?.developer)
);
}
function removeScheme(url: string): string {
return url.replace(/^[a-zA-Z0-9+.-]+:\/\//, '');
}
function removePort(url: string): string {
return url.replace(/(?=([a-zA-Z0-9+.-]+:\/\/)?[^/]):\d+/, '');
}
function removeLeadingSlash(url: string): string {
return url.replace(/^\//, '');
}
function removeTrailingSlashAndQueryString(url: string): string {
return url.replace(/\/?\?.*$/, '');
}
function ensureLeadingSlash(input: string, shouldAppend: boolean): string {
const hasSlash = input.startsWith('/');
if (hasSlash && !shouldAppend) {
return input.substring(1);
} else if (!hasSlash && shouldAppend) {
return `/${input}`;
}
return input;
}
// @needsAudit
/**
* Helper method for constructing a deep link into your app, given an optional path and set of query
* parameters. Creates a URI scheme with two slashes by default.
*
* The scheme must be defined in the [app config](./../config/app) under `expo.scheme`
* or `expo.{android,ios}.scheme`. Platform-specific schemes defined under `expo.{android,ios}.scheme`
* take precedence over universal schemes defined under `expo.scheme`.
*
* # Examples
* - Development and production builds: `<scheme>://path` - uses the optional `scheme` property if provided, and otherwise uses the first scheme defined by your app config
* - Web (dev): `https://localhost:19006/path`
* - Web (prod): `https://myapp.com/path`
* - Expo Go (dev): `exp://128.0.0.1:8081/--/path`
*
* The behavior of this method in Expo Go for published updates is undefined and should not be relied upon.
* The created URL in this case is neither stable nor predictable during the lifetime of the app.
* If a stable URL is needed, for example in authorization callbacks, a build (or development build)
* of your application should be used and the scheme provided.
*
* @param path Addition path components to append to the base URL.
* @param namedParameters Additional options object.
* @return A URL string which points to your app with the given deep link information.
*/
export function createURL(
path: string,
{ scheme, queryParams = {}, isTripleSlashed = false }: CreateURLOptions = {}
): string {
const resolvedScheme = resolveScheme({ scheme });
let hostUri = getHostUri() || '';
if (hasCustomScheme() && isExpoHosted()) {
hostUri = '';
}
if (path) {
if (isExpoHosted() && hostUri) {
path = `/--/${removeLeadingSlash(path)}`;
}
if (isTripleSlashed && !path.startsWith('/')) {
path = `/${path}`;
}
} else {
path = '';
}
// merge user-provided query params with any that were already in the hostUri
// e.g. release-channel
let queryString = '';
const queryStringMatchResult = hostUri.match(/(.*)\?(.+)/);
if (queryStringMatchResult) {
hostUri = queryStringMatchResult[1];
queryString = queryStringMatchResult[2];
let paramsFromHostUri = {};
try {
paramsFromHostUri = Object.fromEntries(
// @ts-ignore: [Symbol.iterator] is indeed, available on every platform.
new URLSearchParams(queryString)
);
} catch {}
queryParams = {
...queryParams,
...paramsFromHostUri,
};
}
queryString = new URLSearchParams(
// For legacy purposes, we'll strip out the nullish values before creating the URL.
Object.fromEntries(
Object.entries(queryParams).filter(([, value]) => value != null) as [string, string][]
)
).toString();
if (queryString) {
queryString = `?${queryString}`;
}
hostUri = ensureLeadingSlash(hostUri, !isTripleSlashed);
// URLSearchParams.stringify already encodes query parameters, so we only need to encode the remaining part of the URL.
const encodedURI = encodeURI(`${resolvedScheme}:${isTripleSlashed ? '/' : ''}/${hostUri}${path}`);
return `${encodedURI}${queryString}`;
}
// @needsAudit
/**
* Helper method for parsing out deep link information from a URL.
* @param url A URL that points to the currently running experience (for example, an output of `Linking.createURL()`).
* @return A `ParsedURL` object.
*/
export function parse(url: string): ParsedURL {
validateURL(url);
const queryParams: Record<string, string> = {};
let path: string | null = null;
let hostname: string | null = null;
let scheme: string | null = null;
try {
const parsed = new URL(url);
parsed.searchParams.forEach((value, key) => {
queryParams[key] = decodeURIComponent(value);
});
path = parsed.pathname || null;
hostname = parsed.hostname || null;
scheme = parsed.protocol || null;
} catch {
path = url;
}
const hostUri = getHostUri() || '';
const hostUriStripped = removePort(removeTrailingSlashAndQueryString(hostUri));
if (scheme) {
// Remove colon at end
scheme = scheme.substring(0, scheme.length - 1);
}
if (path) {
path = removeLeadingSlash(path);
let expoPrefix: string | null = null;
if (hostUriStripped) {
const parts = hostUriStripped.split('/');
expoPrefix = parts.slice(1).concat(['--/']).join('/');
}
if (isExpoHosted() && !hasCustomScheme() && expoPrefix && path.startsWith(expoPrefix)) {
path = path.substring(expoPrefix.length);
hostname = null;
} else if (path.indexOf('+') > -1) {
path = path.substring(path.indexOf('+') + 1);
}
}
return {
hostname,
path,
queryParams,
scheme,
};
}