1import { UnavailabilityError } from 'expo-modules-core';
2import { AppState, Linking, Platform, processColor, } from 'react-native';
3import ExponentWebBrowser from './ExpoWebBrowser';
4import { WebBrowserResultType, WebBrowserPresentationStyle, } from './WebBrowser.types';
5export { WebBrowserResultType, WebBrowserPresentationStyle, };
6const emptyCustomTabsPackages = {
7    defaultBrowserPackage: undefined,
8    preferredBrowserPackage: undefined,
9    browserPackages: [],
10    servicePackages: [],
11};
12// @needsAudit
13/**
14 * Returns a list of applications package names supporting Custom Tabs, Custom Tabs
15 * service, user chosen and preferred one. This may not be fully reliable, since it uses
16 * `PackageManager.getResolvingActivities` under the hood. (For example, some browsers might not be
17 * present in browserPackages list once another browser is set to default.)
18 *
19 * @return The promise which fulfils with [`WebBrowserCustomTabsResults`](#webbrowsercustomtabsresults) object.
20 * @platform android
21 */
22export async function getCustomTabsSupportingBrowsersAsync() {
23    if (!ExponentWebBrowser.getCustomTabsSupportingBrowsersAsync) {
24        throw new UnavailabilityError('WebBrowser', 'getCustomTabsSupportingBrowsersAsync');
25    }
26    if (Platform.OS !== 'android') {
27        return emptyCustomTabsPackages;
28    }
29    else {
30        return await ExponentWebBrowser.getCustomTabsSupportingBrowsersAsync();
31    }
32}
33// @needsAudit
34/**
35 * This method calls `warmUp` method on [CustomTabsClient](https://developer.android.com/reference/android/support/customtabs/CustomTabsClient.html#warmup(long))
36 * for specified package.
37 *
38 * @param browserPackage Package of browser to be warmed up. If not set, preferred browser will be warmed.
39 *
40 * @return A promise which fulfils with `WebBrowserWarmUpResult` object.
41 * @platform android
42 */
43export async function warmUpAsync(browserPackage) {
44    if (!ExponentWebBrowser.warmUpAsync) {
45        throw new UnavailabilityError('WebBrowser', 'warmUpAsync');
46    }
47    if (Platform.OS !== 'android') {
48        return {};
49    }
50    else {
51        return await ExponentWebBrowser.warmUpAsync(browserPackage);
52    }
53}
54// @needsAudit
55/**
56 * This method initiates (if needed) [CustomTabsSession](https://developer.android.com/reference/android/support/customtabs/CustomTabsSession.html#maylaunchurl)
57 * and calls its `mayLaunchUrl` method for browser specified by the package.
58 *
59 * @param url The url of page that is likely to be loaded first when opening browser.
60 * @param browserPackage Package of browser to be informed. If not set, preferred
61 * browser will be used.
62 *
63 * @return A promise which fulfils with `WebBrowserMayInitWithUrlResult` object.
64 * @platform android
65 */
66export async function mayInitWithUrlAsync(url, browserPackage) {
67    if (!ExponentWebBrowser.mayInitWithUrlAsync) {
68        throw new UnavailabilityError('WebBrowser', 'mayInitWithUrlAsync');
69    }
70    if (Platform.OS !== 'android') {
71        return {};
72    }
73    else {
74        return await ExponentWebBrowser.mayInitWithUrlAsync(url, browserPackage);
75    }
76}
77// @needsAudit
78/**
79 * This methods removes all bindings to services created by [`warmUpAsync`](#webbrowserwarmupasyncbrowserpackage)
80 * or [`mayInitWithUrlAsync`](#webbrowsermayinitwithurlasyncurl-browserpackage). You should call
81 * this method once you don't need them to avoid potential memory leaks. However, those binding
82 * would be cleared once your application is destroyed, which might be sufficient in most cases.
83 *
84 * @param browserPackage Package of browser to be cooled. If not set, preferred browser will be used.
85 *
86 * @return The promise which fulfils with ` WebBrowserCoolDownResult` when cooling is performed, or
87 * an empty object when there was no connection to be dismissed.
88 * @platform android
89 */
90export async function coolDownAsync(browserPackage) {
91    if (!ExponentWebBrowser.coolDownAsync) {
92        throw new UnavailabilityError('WebBrowser', 'coolDownAsync');
93    }
94    if (Platform.OS !== 'android') {
95        return {};
96    }
97    else {
98        return await ExponentWebBrowser.coolDownAsync(browserPackage);
99    }
100}
101let browserLocked = false;
102// @needsAudit
103/**
104 * Opens the url with Safari in a modal on iOS using [`SFSafariViewController`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller),
105 * and Chrome in a new [custom tab](https://developer.chrome.com/multidevice/android/customtabs)
106 * on Android. On iOS, the modal Safari will not share cookies with the system Safari. If you need
107 * this, use [`openAuthSessionAsync`](#webbrowseropenauthsessionasyncurl-redirecturl-browserparams).
108 *
109 * @param url The url to open in the web browser.
110 * @param browserParams A dictionary of key-value pairs.
111 *
112 * @return The promise behaves differently based on the platform.
113 * On Android promise resolves with `{type: 'opened'}` if we were able to open browser.
114 * On iOS:
115 * - If the user closed the web browser, the Promise resolves with `{ type: 'cancel' }`.
116 * - If the browser is closed using [`dismissBrowser`](#webbrowserdismissbrowser), the Promise resolves with `{ type: 'dismiss' }`.
117 */
118export async function openBrowserAsync(url, browserParams = {}) {
119    if (!ExponentWebBrowser.openBrowserAsync) {
120        throw new UnavailabilityError('WebBrowser', 'openBrowserAsync');
121    }
122    if (browserLocked) {
123        // Prevent multiple sessions from running at the same time, WebBrowser doesn't
124        // support it this makes the behavior predictable.
125        if (__DEV__) {
126            console.warn('Attempted to call WebBrowser.openBrowserAsync multiple times while already active. Only one WebBrowser controller can be active at any given time.');
127        }
128        return { type: WebBrowserResultType.LOCKED };
129    }
130    browserLocked = true;
131    let result;
132    try {
133        result = await ExponentWebBrowser.openBrowserAsync(url, _processOptions(browserParams));
134    }
135    finally {
136        // WebBrowser session complete, unset lock
137        browserLocked = false;
138    }
139    return result;
140}
141// @needsAudit
142/**
143 * Dismisses the presented web browser.
144 *
145 * @return The `void` on successful attempt, or throws error, if dismiss functionality is not avaiable.
146 * @platform ios
147 */
148export function dismissBrowser() {
149    if (!ExponentWebBrowser.dismissBrowser) {
150        throw new UnavailabilityError('WebBrowser', 'dismissBrowser');
151    }
152    ExponentWebBrowser.dismissBrowser();
153}
154// @needsAudit
155/**
156 * # On Android:
157 * This will be done using a "custom Chrome tabs" browser, [AppState](https://reactnative.dev/docs/appstate),
158 * and [Linking](./linking/) APIs.
159 *
160 * # On iOS:
161 * Opens the url with Safari in a modal using `ASWebAuthenticationSession`. The user will be asked
162 * whether to allow the app to authenticate using the given url.
163 * To handle redirection back to the mobile application, the redirect URI set in the authentication server
164 * has to use the protocol provided as the scheme in **app.json** [`expo.scheme`](./../config/app/#scheme).
165 * For example, `demo://` not `https://` protocol.
166 * Using `Linking.addEventListener` is not needed and can have side effects.
167 *
168 * # On web:
169 * > This API can only be used in a secure environment (`https`). You can use expo `start:web --https`
170 * to test this. Otherwise, an error with code [`ERR_WEB_BROWSER_CRYPTO`](#errwebbrowsercrypto) will be thrown.
171 * This will use the browser's [`window.open()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) API.
172 * - _Desktop_: This will create a new web popup window in the browser that can be closed later using `WebBrowser.maybeCompleteAuthSession()`.
173 * - _Mobile_: This will open a new tab in the browser which can be closed using `WebBrowser.maybeCompleteAuthSession()`.
174 *
175 * How this works on web:
176 * - A crypto state will be created for verifying the redirect.
177 *   - This means you need to run with `npx expo start --https`
178 * - The state will be added to the window's `localstorage`. This ensures that auth cannot complete
179 *   unless it's done from a page running with the same origin as it was started.
180 *   Ex: if `openAuthSessionAsync` is invoked on `https://localhost:19006`, then `maybeCompleteAuthSession`
181 *   must be invoked on a page hosted from the origin `https://localhost:19006`. Using a different
182 *   website, or even a different host like `https://128.0.0.*:19006` for example will not work.
183 * - A timer will be started to check for every 1000 milliseconds (1 second) to detect if the window
184 *   has been closed by the user. If this happens then a promise will resolve with `{ type: 'dismiss' }`.
185 *
186 * > On mobile web, Chrome and Safari will block any call to [`window.open()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/open)
187 * which takes too long to fire after a user interaction. This method must be invoked immediately
188 * after a user interaction. If the event is blocked, an error with code [`ERR_WEB_BROWSER_BLOCKED`](#errwebbrowserblocked) will be thrown.
189 *
190 * @param url The url to open in the web browser. This should be a login page.
191 * @param redirectUrl _Optional_ - The url to deep link back into your app.
192 * On web, this defaults to the output of [`Linking.createURL("")`](./linking/#linkingcreateurlpath-namedparameters).
193 * @param options _Optional_ - An object extending the [`WebBrowserOpenOptions`](#webbrowseropenoptions).
194 * If there is no native AuthSession implementation available (which is the case on Android)
195 * these params will be used in the browser polyfill. If there is a native AuthSession implementation,
196 * these params will be ignored.
197 *
198 * @return
199 * - If the user does not permit the application to authenticate with the given url, the Promise fulfills with `{ type: 'cancel' }` object.
200 * - If the user closed the web browser, the Promise fulfills with `{ type: 'cancel' }` object.
201 * - If the browser is closed using [`dismissBrowser`](#webbrowserdismissbrowser),
202 * the Promise fulfills with `{ type: 'dismiss' }` object.
203 */
204export async function openAuthSessionAsync(url, redirectUrl, options = {}) {
205    if (_authSessionIsNativelySupported()) {
206        if (!ExponentWebBrowser.openAuthSessionAsync) {
207            throw new UnavailabilityError('WebBrowser', 'openAuthSessionAsync');
208        }
209        if (['ios', 'web'].includes(Platform.OS)) {
210            return ExponentWebBrowser.openAuthSessionAsync(url, redirectUrl, _processOptions(options));
211        }
212        return ExponentWebBrowser.openAuthSessionAsync(url, redirectUrl);
213    }
214    else {
215        return _openAuthSessionPolyfillAsync(url, redirectUrl, options);
216    }
217}
218// @docsMissing
219export function dismissAuthSession() {
220    if (_authSessionIsNativelySupported()) {
221        if (!ExponentWebBrowser.dismissAuthSession) {
222            throw new UnavailabilityError('WebBrowser', 'dismissAuthSession');
223        }
224        ExponentWebBrowser.dismissAuthSession();
225    }
226    else {
227        if (!ExponentWebBrowser.dismissBrowser) {
228            throw new UnavailabilityError('WebBrowser', 'dismissAuthSession');
229        }
230        ExponentWebBrowser.dismissBrowser();
231    }
232}
233// @needsAudit
234/**
235 * Possibly completes an authentication session on web in a window popup. The method
236 * should be invoked on the page that the window redirects to.
237 *
238 * @param options
239 *
240 * @return Returns an object with message about why the redirect failed or succeeded:
241 *
242 * If `type` is set to `failed`, the reason depends on the message:
243 * - `Not supported on this platform`: If the platform doesn't support this method (iOS, Android).
244 * - `Cannot use expo-web-browser in a non-browser environment`: If the code was executed in an SSR
245 *   or node environment.
246 * - `No auth session is currently in progress`: (the cached state wasn't found in local storage).
247 *   This can happen if the window redirects to an origin (website) that is different to the initial
248 *   website origin. If this happens in development, it may be because the auth started on localhost
249 *   and finished on your computer port (Ex: `128.0.0.*`). This is controlled by the `redirectUrl`
250 *   and `returnUrl`.
251 * - `Current URL "<URL>" and original redirect URL "<URL>" do not match`: This can occur when the
252 *   redirect URL doesn't match what was initial defined as the `returnUrl`. You can skip this test
253 *   in development by passing `{ skipRedirectCheck: true }` to the function.
254 *
255 * If `type` is set to `success`, the parent window will attempt to close the child window immediately.
256 *
257 * If the error `ERR_WEB_BROWSER_REDIRECT` was thrown, it may mean that the parent window was
258 * reloaded before the auth was completed. In this case you'll need to close the child window manually.
259 *
260 * @platform web
261 */
262export function maybeCompleteAuthSession(options = {}) {
263    if (ExponentWebBrowser.maybeCompleteAuthSession) {
264        return ExponentWebBrowser.maybeCompleteAuthSession(options);
265    }
266    return { type: 'failed', message: 'Not supported on this platform' };
267}
268function _processOptions(options) {
269    return {
270        ...options,
271        controlsColor: processColor(options.controlsColor),
272        toolbarColor: processColor(options.toolbarColor),
273        secondaryToolbarColor: processColor(options.secondaryToolbarColor),
274    };
275}
276/* iOS <= 10 and Android polyfill for SFAuthenticationSession flow */
277function _authSessionIsNativelySupported() {
278    if (Platform.OS === 'android') {
279        return false;
280    }
281    else if (Platform.OS === 'web') {
282        return true;
283    }
284    const versionNumber = parseInt(String(Platform.Version), 10);
285    return versionNumber >= 11;
286}
287let _redirectSubscription = null;
288/*
289 * openBrowserAsync on Android doesn't wait until closed, so we need to polyfill
290 * it with AppState
291 */
292// Store the `resolve` function from a Promise to fire when the AppState
293// returns to active
294let _onWebBrowserCloseAndroid = null;
295// If the initial AppState.currentState is null, we assume that the first call to
296// AppState#change event is not actually triggered by a real change,
297// is triggered instead by the bridge capturing the current state
298// (https://reactnative.dev/docs/appstate#basic-usage)
299let _isAppStateAvailable = AppState.currentState !== null;
300function _onAppStateChangeAndroid(state) {
301    if (!_isAppStateAvailable) {
302        _isAppStateAvailable = true;
303        return;
304    }
305    if (state === 'active' && _onWebBrowserCloseAndroid) {
306        _onWebBrowserCloseAndroid();
307    }
308}
309async function _openBrowserAndWaitAndroidAsync(startUrl, browserParams = {}) {
310    const appStateChangedToActive = new Promise((resolve) => {
311        _onWebBrowserCloseAndroid = resolve;
312    });
313    const stateChangeSubscription = AppState.addEventListener('change', _onAppStateChangeAndroid);
314    let result = { type: WebBrowserResultType.CANCEL };
315    let type = null;
316    try {
317        ({ type } = await openBrowserAsync(startUrl, browserParams));
318    }
319    catch (e) {
320        stateChangeSubscription.remove();
321        _onWebBrowserCloseAndroid = null;
322        throw e;
323    }
324    if (type === 'opened') {
325        await appStateChangedToActive;
326        result = { type: WebBrowserResultType.DISMISS };
327    }
328    stateChangeSubscription.remove();
329    _onWebBrowserCloseAndroid = null;
330    return result;
331}
332async function _openAuthSessionPolyfillAsync(startUrl, returnUrl, browserParams = {}) {
333    if (_redirectSubscription) {
334        throw new Error(`The WebBrowser's auth session is in an invalid state with a redirect handler set when it should not be`);
335    }
336    if (_onWebBrowserCloseAndroid) {
337        throw new Error(`WebBrowser is already open, only one can be open at a time`);
338    }
339    try {
340        if (Platform.OS === 'android') {
341            return await Promise.race([
342                _openBrowserAndWaitAndroidAsync(startUrl, browserParams),
343                _waitForRedirectAsync(returnUrl),
344            ]);
345        }
346        else {
347            return await Promise.race([
348                openBrowserAsync(startUrl, browserParams),
349                _waitForRedirectAsync(returnUrl),
350            ]);
351        }
352    }
353    finally {
354        // We can't dismiss the browser on Android, only call this when it's available.
355        // Users on Android need to manually press the 'x' button in Chrome Custom Tabs, sadly.
356        if (ExponentWebBrowser.dismissBrowser) {
357            ExponentWebBrowser.dismissBrowser();
358        }
359        _stopWaitingForRedirect();
360    }
361}
362function _stopWaitingForRedirect() {
363    if (!_redirectSubscription) {
364        throw new Error(`The WebBrowser auth session is in an invalid state with no redirect handler when one should be set`);
365    }
366    _redirectSubscription.remove();
367    _redirectSubscription = null;
368}
369function _waitForRedirectAsync(returnUrl) {
370    // Note that this Promise never resolves when `returnUrl` is nullish
371    return new Promise((resolve) => {
372        const redirectHandler = (event) => {
373            if (returnUrl && event.url.startsWith(returnUrl)) {
374                resolve({ url: event.url, type: 'success' });
375            }
376        };
377        _redirectSubscription = Linking.addEventListener('url', redirectHandler);
378    });
379}
380//# sourceMappingURL=WebBrowser.js.map