GT2/Ejectable/node_modules/expo-web-browser/build/ExpoWebBrowser.web.js

287 lines
12 KiB
JavaScript

import { CodedError, Platform } from '@unimodules/core';
import compareUrls from 'compare-urls';
import { AppState, Dimensions } from 'react-native';
import { WebBrowserResultType, } from './WebBrowser.types';
const POPUP_WIDTH = 500;
const POPUP_HEIGHT = 650;
let popupWindow = null;
const listenerMap = new Map();
const getHandle = () => 'ExpoWebBrowserRedirectHandle';
const getOriginUrlHandle = (hash) => `ExpoWebBrowser_OriginUrl_${hash}`;
const getRedirectUrlHandle = (hash) => `ExpoWebBrowser_RedirectUrl_${hash}`;
function dismissPopup() {
if (!popupWindow) {
return;
}
popupWindow.close();
if (listenerMap.has(popupWindow)) {
const { listener, appStateListener, interval } = listenerMap.get(popupWindow);
clearInterval(interval);
window.removeEventListener('message', listener);
AppState.removeEventListener('change', appStateListener);
listenerMap.delete(popupWindow);
const handle = window.localStorage.getItem(getHandle());
if (handle) {
window.localStorage.removeItem(getHandle());
window.localStorage.removeItem(getOriginUrlHandle(handle));
window.localStorage.removeItem(getRedirectUrlHandle(handle));
}
popupWindow = null;
}
}
export default {
get name() {
return 'ExpoWebBrowser';
},
async openBrowserAsync(url, browserParams = {}) {
if (!Platform.isDOMAvailable)
return { type: WebBrowserResultType.CANCEL };
const { windowName = '_blank', windowFeatures } = browserParams;
const features = getPopupFeaturesString(windowFeatures);
window.open(url, windowName, features);
return { type: WebBrowserResultType.OPENED };
},
dismissAuthSession() {
if (!Platform.isDOMAvailable)
return;
dismissPopup();
},
maybeCompleteAuthSession({ skipRedirectCheck, }) {
if (!Platform.isDOMAvailable) {
return {
type: 'failed',
message: 'Cannot use expo-web-browser in a non-browser environment',
};
}
const handle = window.localStorage.getItem(getHandle());
if (!handle) {
return { type: 'failed', message: 'No auth session is currently in progress' };
}
const url = window.location.href;
if (skipRedirectCheck !== true) {
const redirectUrl = window.localStorage.getItem(getRedirectUrlHandle(handle));
// Compare the original redirect url against the current url with it's query params removed.
const currentUrl = window.location.origin + window.location.pathname;
if (!compareUrls(redirectUrl, currentUrl)) {
return {
type: 'failed',
message: `Current URL "${currentUrl}" and original redirect URL "${redirectUrl}" do not match.`,
};
}
}
// Save the link for app state listener
window.localStorage.setItem(getOriginUrlHandle(handle), url);
// Get the window that created the current popup
const parent = window.opener ?? window.parent;
if (!parent) {
throw new CodedError('ERR_WEB_BROWSER_REDIRECT', `The window cannot complete the redirect request because the invoking window doesn't have a reference to it's parent. This can happen if the parent window was reloaded.`);
}
// Send the URL back to the opening window.
parent.postMessage({ url, expoSender: handle }, parent.location);
return { type: 'success', message: `Attempting to complete auth` };
// Maybe set timer to throw an error if the window is still open after attempting to complete.
},
// This method should be invoked from user input.
async openAuthSessionAsync(url, redirectUrl, openOptions) {
if (!Platform.isDOMAvailable)
return { type: WebBrowserResultType.CANCEL };
redirectUrl = redirectUrl ?? getRedirectUrlFromUrlOrGenerate(url);
const state = await getStateFromUrlOrGenerateAsync(url);
// Save handle for session
window.localStorage.setItem(getHandle(), state);
// Save redirect Url for further verification
window.localStorage.setItem(getRedirectUrlHandle(state), redirectUrl);
if (popupWindow == null || popupWindow?.closed) {
const features = getPopupFeaturesString(openOptions?.windowFeatures);
popupWindow = window.open(url, openOptions?.windowName, features);
if (popupWindow) {
try {
popupWindow.focus();
}
catch (e) { }
}
else {
throw new CodedError('ERR_WEB_BROWSER_BLOCKED', 'Popup window was blocked by the browser or failed to open. This can happen in mobile browsers when the window.open() method was invoked too long after a user input was fired.');
}
}
return new Promise(async (resolve) => {
// Create a listener for messages sent from the popup
const listener = (event) => {
if (!event.isTrusted)
return;
// Ensure we trust the sender.
if (event.origin !== window.location.origin) {
return;
}
const { data } = event;
// Use a crypto hash to invalid message.
const handle = window.localStorage.getItem(getHandle());
// Ensure the sender is also from expo-web-browser
if (data.expoSender === handle) {
dismissPopup();
resolve({ type: 'success', url: data.url });
}
};
// Add a listener for receiving messages from the popup.
window.addEventListener('message', listener, false);
// Create an app state listener as a fallback to the popup listener
const appStateListener = (state) => {
if (state !== 'active') {
return;
}
const handle = window.localStorage.getItem(getHandle());
if (handle) {
const url = window.localStorage.getItem(getOriginUrlHandle(handle));
if (url) {
dismissPopup();
resolve({ type: 'success', url });
}
}
};
AppState.addEventListener('change', appStateListener);
// Check if the window has been closed every second.
const interval = setInterval(() => {
if (popupWindow?.closed) {
if (resolve)
resolve({ type: WebBrowserResultType.DISMISS });
clearInterval(interval);
dismissPopup();
}
}, 1000);
// Store the listener and interval for clean up.
listenerMap.set(popupWindow, {
listener,
interval,
appStateListener,
});
});
},
};
// Crypto
function isCryptoAvailable() {
if (!Platform.isDOMAvailable)
return false;
return !!window?.crypto;
}
function isSubtleCryptoAvailable() {
if (!isCryptoAvailable())
return false;
return !!window.crypto.subtle;
}
async function getStateFromUrlOrGenerateAsync(inputUrl) {
const url = new URL(inputUrl);
if (url.searchParams.has('state') && typeof url.searchParams.get('state') === 'string') {
// Ensure we reuse the auth state if it's passed in.
return url.searchParams.get('state');
}
// Generate a crypto state for verifying the return popup.
return await generateStateAsync();
}
function getRedirectUrlFromUrlOrGenerate(inputUrl) {
const url = new URL(inputUrl);
if (url.searchParams.has('redirect_uri') &&
typeof url.searchParams.get('redirect_uri') === 'string') {
// Ensure we reuse the redirect_uri if it's passed in the input url.
return url.searchParams.get('redirect_uri');
}
// Emulate how native uses Constants.linkingUrl
return location.origin + location.pathname;
}
const CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
async function generateStateAsync() {
if (!isSubtleCryptoAvailable()) {
throw new CodedError('ERR_WEB_BROWSER_CRYPTO', `The current environment doesn't support crypto. Ensure you are running from a secure origin (https).`);
}
const encoder = new TextEncoder();
const data = generateRandom(10);
const buffer = encoder.encode(data);
const hashedData = await crypto.subtle.digest('SHA-256', buffer);
const state = btoa(String.fromCharCode(...new Uint8Array(hashedData)));
return state;
}
function generateRandom(size) {
let arr = new Uint8Array(size);
if (arr.byteLength !== arr.length) {
arr = new Uint8Array(arr.buffer);
}
const array = new Uint8Array(arr.length);
if (isCryptoAvailable()) {
window.crypto.getRandomValues(array);
}
else {
for (let i = 0; i < size; i += 1) {
array[i] = (Math.random() * CHARSET.length) | 0;
}
}
return bufferToString(array);
}
function bufferToString(buffer) {
const state = [];
for (let i = 0; i < buffer.byteLength; i += 1) {
const index = buffer[i] % CHARSET.length;
state.push(CHARSET[index]);
}
return state.join('');
}
// Window Features
// Ensure feature string is an object
function normalizePopupFeaturesString(options) {
let windowFeatures = {};
// This should be avoided because it adds extra time to the popup command.
if (typeof options === 'string') {
// Convert string of `key=value,foo=bar` into an object
const windowFeaturePairs = options.split(',');
for (const pair of windowFeaturePairs) {
const [key, value] = pair.trim().split('=');
if (key && value) {
windowFeaturePairs[key] = value;
}
}
}
else if (options) {
windowFeatures = options;
}
return windowFeatures;
}
// Apply default values to the input feature set
function getPopupFeaturesString(options) {
const windowFeatures = normalizePopupFeaturesString(options);
const width = windowFeatures.width ?? POPUP_WIDTH;
const height = windowFeatures.height ?? POPUP_HEIGHT;
const dimensions = Dimensions.get('screen');
const top = windowFeatures.top ?? Math.max(0, (dimensions.height - height) * 0.5);
const left = windowFeatures.left ?? Math.max(0, (dimensions.width - width) * 0.5);
// Create a reasonable popup
// https://developer.mozilla.org/en-US/docs/Web/API/Window/open#Window_features
return featureObjectToString({
...windowFeatures,
// Toolbar buttons (Back, Forward, Reload, Stop buttons).
toolbar: windowFeatures.toolbar ?? 'no',
menubar: windowFeatures.menubar ?? 'no',
// Shows the location bar or the address bar.
location: windowFeatures.location ?? 'yes',
resizable: windowFeatures.resizable ?? 'yes',
// If this feature is on, then the new secondary window has a status bar.
status: windowFeatures.status ?? 'no',
scrollbars: windowFeatures.scrollbars ?? 'yes',
top,
left,
width,
height,
});
}
export function featureObjectToString(features) {
return Object.keys(features).reduce((prev, current) => {
let value = features[current];
if (typeof value === 'boolean') {
value = value ? 'yes' : 'no';
}
if (current && value) {
if (prev)
prev += ',';
return `${prev}${current}=${value}`;
}
return prev;
}, '');
}
//# sourceMappingURL=ExpoWebBrowser.web.js.map