307 lines
10 KiB
JavaScript
307 lines
10 KiB
JavaScript
import { Platform, UnavailabilityError } from '@unimodules/core';
|
|
import Constants from 'expo-constants';
|
|
import invariant from 'invariant';
|
|
import qs from 'qs';
|
|
import { useEffect, useState } from 'react';
|
|
import URL from 'url-parse';
|
|
import NativeLinking from './ExpoLinking';
|
|
import { hasCustomScheme, resolveScheme } from './Schemes';
|
|
function validateURL(url) {
|
|
invariant(typeof url === 'string', 'Invalid URL: should be a string. Was: ' + url);
|
|
invariant(url, 'Invalid URL: cannot be empty');
|
|
}
|
|
function getHostUri() {
|
|
if (Constants.manifest?.hostUri) {
|
|
return Constants.manifest.hostUri;
|
|
}
|
|
else if (!Constants.manifest?.hostUri && !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() {
|
|
const hostUri = getHostUri();
|
|
return !!(hostUri &&
|
|
(/^(.*\.)?(expo\.io|exp\.host|exp\.direct|expo\.test)(:.*)?(\/.*)?$/.test(hostUri) ||
|
|
Constants.manifest?.developer));
|
|
}
|
|
function removeScheme(url) {
|
|
return url.replace(/^[a-zA-Z0-9+.-]+:\/\//, '');
|
|
}
|
|
function removePort(url) {
|
|
return url.replace(/(?=([a-zA-Z0-9+.-]+:\/\/)?[^/]):\d+/, '');
|
|
}
|
|
function removeLeadingSlash(url) {
|
|
return url.replace(/^\//, '');
|
|
}
|
|
function removeTrailingSlashAndQueryString(url) {
|
|
return url.replace(/\/?\?.*$/, '');
|
|
}
|
|
function ensureLeadingSlash(input, shouldAppend) {
|
|
const hasSlash = input.endsWith('/');
|
|
if (hasSlash && !shouldAppend) {
|
|
return input.substring(0, input.length - 1);
|
|
}
|
|
else if (!hasSlash && shouldAppend) {
|
|
return `${input}/`;
|
|
}
|
|
return input;
|
|
}
|
|
function ensureTrailingSlash(input, shouldAppend) {
|
|
const hasSlash = input.startsWith('/');
|
|
if (hasSlash && !shouldAppend) {
|
|
return input.substring(1);
|
|
}
|
|
else if (!hasSlash && shouldAppend) {
|
|
return `/${input}`;
|
|
}
|
|
return input;
|
|
}
|
|
/**
|
|
* Create a URL that works for the environment the app is currently running in.
|
|
* The scheme in bare and standalone must be defined in the app.json under `expo.scheme`.
|
|
*
|
|
* **Examples**
|
|
*
|
|
* - Bare: empty string
|
|
* - Standalone, Custom: `yourscheme:///path`
|
|
* - Web (dev): `https://localhost:19006/path`
|
|
* - Web (prod): `https://myapp.com/path`
|
|
* - Expo Client (dev): `exp://128.0.0.1:19000/--/path`
|
|
* - Expo Client (prod): `exp://exp.host/@yourname/your-app/--/path`
|
|
*
|
|
* @param path addition path components to append to the base URL.
|
|
* @param queryParams An object of parameters that will be converted into a query string.
|
|
*/
|
|
export function makeUrl(path = '', queryParams, scheme) {
|
|
return createURL(path, { queryParams, scheme, isTripleSlashed: true });
|
|
}
|
|
/**
|
|
* Create a URL that works for the environment the app is currently running in.
|
|
* The scheme in bare and standalone must be defined in the Expo config (app.config.js or app.json) under `expo.scheme`.
|
|
*
|
|
* **Examples**
|
|
*
|
|
* - Bare: `<scheme>://path` -- uses provided scheme or scheme from Expo config `scheme`.
|
|
* - Standalone, Custom: `yourscheme://path`
|
|
* - Web (dev): `https://localhost:19006/path`
|
|
* - Web (prod): `https://myapp.com/path`
|
|
* - Expo Client (dev): `exp://128.0.0.1:19000/--/path`
|
|
* - Expo Client (prod): `exp://exp.host/@yourname/your-app/--/path`
|
|
*
|
|
* @param path addition path components to append to the base URL.
|
|
* @param scheme URI protocol `<scheme>://` that must be built into your native app.
|
|
* @param queryParams An object of parameters that will be converted into a query string.
|
|
*/
|
|
export function createURL(path, { scheme, queryParams = {}, isTripleSlashed = false } = {}) {
|
|
if (Platform.OS === 'web') {
|
|
if (!Platform.isDOMAvailable)
|
|
return '';
|
|
const origin = ensureLeadingSlash(window.location.origin, false);
|
|
let queryString = qs.stringify(queryParams);
|
|
if (queryString) {
|
|
queryString = `?${queryString}`;
|
|
}
|
|
let outputPath = path;
|
|
if (outputPath)
|
|
outputPath = ensureTrailingSlash(path, true);
|
|
return encodeURI(`${origin}${outputPath}${queryString}`);
|
|
}
|
|
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 {
|
|
const parsedParams = qs.parse(queryString);
|
|
if (typeof parsedParams === 'object') {
|
|
paramsFromHostUri = parsedParams;
|
|
}
|
|
}
|
|
catch (e) { }
|
|
queryParams = {
|
|
...queryParams,
|
|
...paramsFromHostUri,
|
|
};
|
|
}
|
|
queryString = qs.stringify(queryParams);
|
|
if (queryString) {
|
|
queryString = `?${queryString}`;
|
|
}
|
|
hostUri = ensureTrailingSlash(hostUri, !isTripleSlashed);
|
|
return encodeURI(`${resolvedScheme}:${isTripleSlashed ? '/' : ''}/${hostUri}${path}${queryString}`);
|
|
}
|
|
/**
|
|
* Returns the components and query parameters for a given URL.
|
|
*
|
|
* @param url Input URL to parse
|
|
*/
|
|
export function parse(url) {
|
|
validateURL(url);
|
|
const parsed = URL(url, /* parseQueryString */ true);
|
|
for (const param in parsed.query) {
|
|
parsed.query[param] = decodeURIComponent(parsed.query[param]);
|
|
}
|
|
const queryParams = parsed.query;
|
|
const hostUri = getHostUri() || '';
|
|
const hostUriStripped = removePort(removeTrailingSlashAndQueryString(hostUri));
|
|
let path = parsed.pathname || null;
|
|
let hostname = parsed.hostname || null;
|
|
let scheme = parsed.protocol || null;
|
|
if (scheme) {
|
|
// Remove colon at end
|
|
scheme = scheme.substring(0, scheme.length - 1);
|
|
}
|
|
if (path) {
|
|
path = removeLeadingSlash(path);
|
|
let expoPrefix = 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,
|
|
};
|
|
}
|
|
/**
|
|
* Add a handler to Linking changes by listening to the `url` event type
|
|
* and providing the handler
|
|
*
|
|
* See https://reactnative.dev/docs/linking.html#addeventlistener
|
|
*/
|
|
export function addEventListener(type, handler) {
|
|
NativeLinking.addEventListener(type, handler);
|
|
}
|
|
/**
|
|
* Remove a handler by passing the `url` event type and the handler.
|
|
*
|
|
* See https://reactnative.dev/docs/linking.html#removeeventlistener
|
|
*/
|
|
export function removeEventListener(type, handler) {
|
|
NativeLinking.removeEventListener(type, handler);
|
|
}
|
|
/**
|
|
* **Native:** Parses the link that opened the app. If no link opened the app, all the fields will be \`null\`.
|
|
* **Web:** Parses the current window URL.
|
|
*/
|
|
export async function parseInitialURLAsync() {
|
|
const initialUrl = await NativeLinking.getInitialURL();
|
|
if (!initialUrl) {
|
|
return {
|
|
scheme: null,
|
|
hostname: null,
|
|
path: null,
|
|
queryParams: null,
|
|
};
|
|
}
|
|
return parse(initialUrl);
|
|
}
|
|
/**
|
|
* Launch an Android intent with optional extras
|
|
*
|
|
* @platform android
|
|
*/
|
|
export async function sendIntent(action, extras) {
|
|
if (Platform.OS === 'android') {
|
|
return await NativeLinking.sendIntent(action, extras);
|
|
}
|
|
throw new UnavailabilityError('Linking', 'sendIntent');
|
|
}
|
|
/**
|
|
* Attempt to open the system settings for an the app.
|
|
*
|
|
* @platform ios
|
|
*/
|
|
export async function openSettings() {
|
|
if (Platform.OS === 'web') {
|
|
throw new UnavailabilityError('Linking', 'openSettings');
|
|
}
|
|
if (NativeLinking.openSettings) {
|
|
return await NativeLinking.openSettings();
|
|
}
|
|
await openURL('app-settings:');
|
|
}
|
|
/**
|
|
* If the app launch was triggered by an app link,
|
|
* it will give the link url, otherwise it will give `null`
|
|
*/
|
|
export async function getInitialURL() {
|
|
return (await NativeLinking.getInitialURL()) ?? null;
|
|
}
|
|
/**
|
|
* Try to open the given `url` with any of the installed apps.
|
|
*/
|
|
export async function openURL(url) {
|
|
validateURL(url);
|
|
return await NativeLinking.openURL(url);
|
|
}
|
|
/**
|
|
* Determine whether or not an installed app can handle a given URL.
|
|
* On web this always returns true because there is no API for detecting what URLs can be opened.
|
|
*/
|
|
export async function canOpenURL(url) {
|
|
validateURL(url);
|
|
return await NativeLinking.canOpenURL(url);
|
|
}
|
|
/**
|
|
* Returns the initial URL followed by any subsequent changes to the URL.
|
|
*/
|
|
export function useURL() {
|
|
const [url, setLink] = useState(null);
|
|
function onChange(event) {
|
|
setLink(event.url);
|
|
}
|
|
useEffect(() => {
|
|
getInitialURL().then(url => setLink(url));
|
|
addEventListener('url', onChange);
|
|
return () => removeEventListener('url', onChange);
|
|
}, []);
|
|
return url;
|
|
}
|
|
/**
|
|
* Returns the initial URL followed by any subsequent changes to the URL.
|
|
* @deprecated Use `useURL` instead.
|
|
*/
|
|
export function useUrl() {
|
|
console.warn(`Linking.useUrl has been deprecated in favor of Linking.useURL. This API will be removed in SDK 44.`);
|
|
return useURL();
|
|
}
|
|
export * from './Linking.types';
|
|
//# sourceMappingURL=Linking.js.map
|