UNPKG

12 kBJavaScriptView Raw
1import { CodedError } from '@unimodules/core';
2import compareUrls from 'compare-urls';
3import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
4import { AppState, Dimensions } from 'react-native';
5import { WebBrowserResultType, } from './WebBrowser.types';
6const POPUP_WIDTH = 500;
7const POPUP_HEIGHT = 650;
8let popupWindow = null;
9const listenerMap = new Map();
10const getHandle = () => 'ExpoWebBrowserRedirectHandle';
11const getOriginUrlHandle = (hash) => `ExpoWebBrowser_OriginUrl_${hash}`;
12const getRedirectUrlHandle = (hash) => `ExpoWebBrowser_RedirectUrl_${hash}`;
13function dismissPopup() {
14 if (!popupWindow) {
15 return;
16 }
17 popupWindow.close();
18 if (listenerMap.has(popupWindow)) {
19 const { listener, appStateListener, interval } = listenerMap.get(popupWindow);
20 clearInterval(interval);
21 window.removeEventListener('message', listener);
22 AppState.removeEventListener('change', appStateListener);
23 listenerMap.delete(popupWindow);
24 const handle = window.localStorage.getItem(getHandle());
25 if (handle) {
26 window.localStorage.removeItem(getHandle());
27 window.localStorage.removeItem(getOriginUrlHandle(handle));
28 window.localStorage.removeItem(getRedirectUrlHandle(handle));
29 }
30 popupWindow = null;
31 }
32}
33export default {
34 get name() {
35 return 'ExpoWebBrowser';
36 },
37 async openBrowserAsync(url, browserParams = {}) {
38 if (!canUseDOM)
39 return { type: WebBrowserResultType.CANCEL };
40 const { windowName = '_blank', windowFeatures } = browserParams;
41 const features = getPopupFeaturesString(windowFeatures);
42 window.open(url, windowName, features);
43 return { type: WebBrowserResultType.OPENED };
44 },
45 dismissAuthSession() {
46 if (!canUseDOM)
47 return;
48 dismissPopup();
49 },
50 maybeCompleteAuthSession({ skipRedirectCheck, }) {
51 if (!canUseDOM) {
52 return {
53 type: 'failed',
54 message: 'Cannot use expo-web-browser in a non-browser environment',
55 };
56 }
57 const handle = window.localStorage.getItem(getHandle());
58 if (!handle) {
59 return { type: 'failed', message: 'No auth session is currently in progress' };
60 }
61 const url = window.location.href;
62 if (skipRedirectCheck !== true) {
63 const redirectUrl = window.localStorage.getItem(getRedirectUrlHandle(handle));
64 // Compare the original redirect url against the current url with it's query params removed.
65 const currentUrl = window.location.origin + window.location.pathname;
66 if (!compareUrls(redirectUrl, currentUrl)) {
67 return {
68 type: 'failed',
69 message: `Current URL "${currentUrl}" and original redirect URL "${redirectUrl}" do not match.`,
70 };
71 }
72 }
73 // Save the link for app state listener
74 window.localStorage.setItem(getOriginUrlHandle(handle), url);
75 // Get the window that created the current popup
76 const parent = window.opener ?? window.parent;
77 if (!parent) {
78 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.`);
79 }
80 // Send the URL back to the opening window.
81 parent.postMessage({ url, expoSender: handle }, parent.location);
82 return { type: 'success', message: `Attempting to complete auth` };
83 // Maybe set timer to throw an error if the window is still open after attempting to complete.
84 },
85 // This method should be invoked from user input.
86 async openAuthSessionAsync(url, redirectUrl, openOptions) {
87 if (!canUseDOM)
88 return { type: WebBrowserResultType.CANCEL };
89 redirectUrl = redirectUrl ?? getRedirectUrlFromUrlOrGenerate(url);
90 const state = await getStateFromUrlOrGenerateAsync(url);
91 // Save handle for session
92 window.localStorage.setItem(getHandle(), state);
93 // Save redirect Url for further verification
94 window.localStorage.setItem(getRedirectUrlHandle(state), redirectUrl);
95 if (popupWindow == null || popupWindow?.closed) {
96 const features = getPopupFeaturesString(openOptions?.windowFeatures);
97 popupWindow = window.open(url, openOptions?.windowName, features);
98 if (popupWindow) {
99 try {
100 popupWindow.focus();
101 }
102 catch (e) { }
103 }
104 else {
105 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.');
106 }
107 }
108 return new Promise(async (resolve) => {
109 // Create a listener for messages sent from the popup
110 const listener = (event) => {
111 if (!event.isTrusted)
112 return;
113 // Ensure we trust the sender.
114 if (event.origin !== window.location.origin) {
115 return;
116 }
117 const { data } = event;
118 // Use a crypto hash to invalid message.
119 const handle = window.localStorage.getItem(getHandle());
120 // Ensure the sender is also from expo-web-browser
121 if (data.expoSender === handle) {
122 dismissPopup();
123 resolve({ type: 'success', url: data.url });
124 }
125 };
126 // Add a listener for receiving messages from the popup.
127 window.addEventListener('message', listener, false);
128 // Create an app state listener as a fallback to the popup listener
129 const appStateListener = (state) => {
130 if (state !== 'active') {
131 return;
132 }
133 const handle = window.localStorage.getItem(getHandle());
134 if (handle) {
135 const url = window.localStorage.getItem(getOriginUrlHandle(handle));
136 if (url) {
137 dismissPopup();
138 resolve({ type: 'success', url });
139 }
140 }
141 };
142 AppState.addEventListener('change', appStateListener);
143 // Check if the window has been closed every second.
144 const interval = setInterval(() => {
145 if (popupWindow?.closed) {
146 if (resolve)
147 resolve({ type: WebBrowserResultType.DISMISS });
148 clearInterval(interval);
149 dismissPopup();
150 }
151 }, 1000);
152 // Store the listener and interval for clean up.
153 listenerMap.set(popupWindow, {
154 listener,
155 interval,
156 appStateListener,
157 });
158 });
159 },
160};
161// Crypto
162function isCryptoAvailable() {
163 if (!canUseDOM)
164 return false;
165 return !!window?.crypto;
166}
167function isSubtleCryptoAvailable() {
168 if (!isCryptoAvailable())
169 return false;
170 return !!window.crypto.subtle;
171}
172async function getStateFromUrlOrGenerateAsync(inputUrl) {
173 const url = new URL(inputUrl);
174 if (url.searchParams.has('state') && typeof url.searchParams.get('state') === 'string') {
175 // Ensure we reuse the auth state if it's passed in.
176 return url.searchParams.get('state');
177 }
178 // Generate a crypto state for verifying the return popup.
179 return await generateStateAsync();
180}
181function getRedirectUrlFromUrlOrGenerate(inputUrl) {
182 const url = new URL(inputUrl);
183 if (url.searchParams.has('redirect_uri') &&
184 typeof url.searchParams.get('redirect_uri') === 'string') {
185 // Ensure we reuse the redirect_uri if it's passed in the input url.
186 return url.searchParams.get('redirect_uri');
187 }
188 // Emulate how native uses Constants.linkingUrl
189 return location.origin + location.pathname;
190}
191const CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
192async function generateStateAsync() {
193 if (!isSubtleCryptoAvailable()) {
194 throw new CodedError('ERR_WEB_BROWSER_CRYPTO', `The current environment doesn't support crypto. Ensure you are running from a secure origin (https).`);
195 }
196 const encoder = new TextEncoder();
197 const data = generateRandom(10);
198 const buffer = encoder.encode(data);
199 const hashedData = await crypto.subtle.digest('SHA-256', buffer);
200 const state = btoa(String.fromCharCode(...new Uint8Array(hashedData)));
201 return state;
202}
203function generateRandom(size) {
204 let arr = new Uint8Array(size);
205 if (arr.byteLength !== arr.length) {
206 arr = new Uint8Array(arr.buffer);
207 }
208 const array = new Uint8Array(arr.length);
209 if (isCryptoAvailable()) {
210 window.crypto.getRandomValues(array);
211 }
212 else {
213 for (let i = 0; i < size; i += 1) {
214 array[i] = (Math.random() * CHARSET.length) | 0;
215 }
216 }
217 return bufferToString(array);
218}
219function bufferToString(buffer) {
220 const state = [];
221 for (let i = 0; i < buffer.byteLength; i += 1) {
222 const index = buffer[i] % CHARSET.length;
223 state.push(CHARSET[index]);
224 }
225 return state.join('');
226}
227// Window Features
228// Ensure feature string is an object
229function normalizePopupFeaturesString(options) {
230 let windowFeatures = {};
231 // This should be avoided because it adds extra time to the popup command.
232 if (typeof options === 'string') {
233 // Convert string of `key=value,foo=bar` into an object
234 const windowFeaturePairs = options.split(',');
235 for (const pair of windowFeaturePairs) {
236 const [key, value] = pair.trim().split('=');
237 if (key && value) {
238 windowFeaturePairs[key] = value;
239 }
240 }
241 }
242 else if (options) {
243 windowFeatures = options;
244 }
245 return windowFeatures;
246}
247// Apply default values to the input feature set
248function getPopupFeaturesString(options) {
249 const windowFeatures = normalizePopupFeaturesString(options);
250 const width = windowFeatures.width ?? POPUP_WIDTH;
251 const height = windowFeatures.height ?? POPUP_HEIGHT;
252 const dimensions = Dimensions.get('screen');
253 const top = windowFeatures.top ?? Math.max(0, (dimensions.height - height) * 0.5);
254 const left = windowFeatures.left ?? Math.max(0, (dimensions.width - width) * 0.5);
255 // Create a reasonable popup
256 // https://developer.mozilla.org/en-US/docs/Web/API/Window/open#Window_features
257 return featureObjectToString({
258 ...windowFeatures,
259 // Toolbar buttons (Back, Forward, Reload, Stop buttons).
260 toolbar: windowFeatures.toolbar ?? 'no',
261 menubar: windowFeatures.menubar ?? 'no',
262 // Shows the location bar or the address bar.
263 location: windowFeatures.location ?? 'yes',
264 resizable: windowFeatures.resizable ?? 'yes',
265 // If this feature is on, then the new secondary window has a status bar.
266 status: windowFeatures.status ?? 'no',
267 scrollbars: windowFeatures.scrollbars ?? 'yes',
268 top,
269 left,
270 width,
271 height,
272 });
273}
274export function featureObjectToString(features) {
275 return Object.keys(features).reduce((prev, current) => {
276 let value = features[current];
277 if (typeof value === 'boolean') {
278 value = value ? 'yes' : 'no';
279 }
280 if (current && value) {
281 if (prev)
282 prev += ',';
283 return `${prev}${current}=${value}`;
284 }
285 return prev;
286 }, '');
287}
288//# sourceMappingURL=ExpoWebBrowser.web.js.map
\No newline at end of file