359 lines
16 KiB
JavaScript
359 lines
16 KiB
JavaScript
import { UnavailabilityError } from 'expo-modules-core';
|
|
import { AppState, Linking, Platform, processColor, } from 'react-native';
|
|
import ExponentWebBrowser from './ExpoWebBrowser';
|
|
import { WebBrowserResultType, WebBrowserPresentationStyle, } from './WebBrowser.types';
|
|
export { WebBrowserResultType, WebBrowserPresentationStyle, };
|
|
const emptyCustomTabsPackages = {
|
|
defaultBrowserPackage: undefined,
|
|
preferredBrowserPackage: undefined,
|
|
browserPackages: [],
|
|
servicePackages: [],
|
|
};
|
|
// @needsAudit
|
|
/**
|
|
* Returns a list of applications package names supporting Custom Tabs, Custom Tabs
|
|
* service, user chosen and preferred one. This may not be fully reliable, since it uses
|
|
* `PackageManager.getResolvingActivities` under the hood. (For example, some browsers might not be
|
|
* present in browserPackages list once another browser is set to default.)
|
|
*
|
|
* @return The promise which fulfils with [`WebBrowserCustomTabsResults`](#webbrowsercustomtabsresults) object.
|
|
* @platform android
|
|
*/
|
|
export async function getCustomTabsSupportingBrowsersAsync() {
|
|
if (!ExponentWebBrowser.getCustomTabsSupportingBrowsersAsync) {
|
|
throw new UnavailabilityError('WebBrowser', 'getCustomTabsSupportingBrowsersAsync');
|
|
}
|
|
if (Platform.OS !== 'android') {
|
|
return emptyCustomTabsPackages;
|
|
}
|
|
else {
|
|
return await ExponentWebBrowser.getCustomTabsSupportingBrowsersAsync();
|
|
}
|
|
}
|
|
// @needsAudit
|
|
/**
|
|
* This method calls `warmUp` method on [CustomTabsClient](https://developer.android.com/reference/android/support/customtabs/CustomTabsClient.html#warmup(long))
|
|
* for specified package.
|
|
*
|
|
* @param browserPackage Package of browser to be warmed up. If not set, preferred browser will be warmed.
|
|
*
|
|
* @return A promise which fulfils with `WebBrowserWarmUpResult` object.
|
|
* @platform android
|
|
*/
|
|
export async function warmUpAsync(browserPackage) {
|
|
if (!ExponentWebBrowser.warmUpAsync) {
|
|
throw new UnavailabilityError('WebBrowser', 'warmUpAsync');
|
|
}
|
|
if (Platform.OS !== 'android') {
|
|
return {};
|
|
}
|
|
else {
|
|
return await ExponentWebBrowser.warmUpAsync(browserPackage);
|
|
}
|
|
}
|
|
// @needsAudit
|
|
/**
|
|
* This method initiates (if needed) [CustomTabsSession](https://developer.android.com/reference/android/support/customtabs/CustomTabsSession.html#maylaunchurl)
|
|
* and calls its `mayLaunchUrl` method for browser specified by the package.
|
|
*
|
|
* @param url The url of page that is likely to be loaded first when opening browser.
|
|
* @param browserPackage Package of browser to be informed. If not set, preferred
|
|
* browser will be used.
|
|
*
|
|
* @return A promise which fulfils with `WebBrowserMayInitWithUrlResult` object.
|
|
* @platform android
|
|
*/
|
|
export async function mayInitWithUrlAsync(url, browserPackage) {
|
|
if (!ExponentWebBrowser.mayInitWithUrlAsync) {
|
|
throw new UnavailabilityError('WebBrowser', 'mayInitWithUrlAsync');
|
|
}
|
|
if (Platform.OS !== 'android') {
|
|
return {};
|
|
}
|
|
else {
|
|
return await ExponentWebBrowser.mayInitWithUrlAsync(url, browserPackage);
|
|
}
|
|
}
|
|
// @needsAudit
|
|
/**
|
|
* This methods removes all bindings to services created by [`warmUpAsync`](#webbrowserwarmupasyncbrowserpackage)
|
|
* or [`mayInitWithUrlAsync`](#webbrowsermayinitwithurlasyncurl-browserpackage). You should call
|
|
* this method once you don't need them to avoid potential memory leaks. However, those binding
|
|
* would be cleared once your application is destroyed, which might be sufficient in most cases.
|
|
*
|
|
* @param browserPackage Package of browser to be cooled. If not set, preferred browser will be used.
|
|
*
|
|
* @return The promise which fulfils with ` WebBrowserCoolDownResult` when cooling is performed, or
|
|
* an empty object when there was no connection to be dismissed.
|
|
* @platform android
|
|
*/
|
|
export async function coolDownAsync(browserPackage) {
|
|
if (!ExponentWebBrowser.coolDownAsync) {
|
|
throw new UnavailabilityError('WebBrowser', 'coolDownAsync');
|
|
}
|
|
if (Platform.OS !== 'android') {
|
|
return {};
|
|
}
|
|
else {
|
|
return await ExponentWebBrowser.coolDownAsync(browserPackage);
|
|
}
|
|
}
|
|
// @needsAudit
|
|
/**
|
|
* Opens the url with Safari in a modal on iOS using [`SFSafariViewController`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller),
|
|
* and Chrome in a new [custom tab](https://developer.chrome.com/multidevice/android/customtabs)
|
|
* on Android. On iOS, the modal Safari will not share cookies with the system Safari. If you need
|
|
* this, use [`openAuthSessionAsync`](#webbrowseropenauthsessionasyncurl-redirecturl-options).
|
|
*
|
|
* @param url The url to open in the web browser.
|
|
* @param browserParams A dictionary of key-value pairs.
|
|
*
|
|
* @return The promise behaves differently based on the platform.
|
|
* On Android promise resolves with `{ type: 'opened' }` if we were able to open browser.
|
|
* On iOS:
|
|
* - If the user closed the web browser, the Promise resolves with `{ type: 'cancel' }`.
|
|
* - If the browser is closed using [`dismissBrowser`](#webbrowserdismissbrowser), the Promise resolves with `{ type: 'dismiss' }`.
|
|
*/
|
|
export async function openBrowserAsync(url, browserParams = {}) {
|
|
if (!ExponentWebBrowser.openBrowserAsync) {
|
|
throw new UnavailabilityError('WebBrowser', 'openBrowserAsync');
|
|
}
|
|
return await ExponentWebBrowser.openBrowserAsync(url, _processOptions(browserParams));
|
|
}
|
|
// @needsAudit
|
|
/**
|
|
* Dismisses the presented web browser.
|
|
*
|
|
* @return The promise that resolves with `{ type: 'dismiss' }` on the successful attempt or throws an error if dismiss functionality is not available.
|
|
* @platform ios
|
|
*/
|
|
export function dismissBrowser() {
|
|
return ExponentWebBrowser.dismissBrowser?.();
|
|
}
|
|
// @needsAudit
|
|
/**
|
|
* # On Android:
|
|
* This will be done using a "custom Chrome tabs" browser, [AppState](https://reactnative.dev/docs/appstate),
|
|
* and [Linking](./linking/) APIs.
|
|
*
|
|
* # On iOS:
|
|
* Opens the url with Safari in a modal using `ASWebAuthenticationSession`. The user will be asked
|
|
* whether to allow the app to authenticate using the given url.
|
|
* To handle redirection back to the mobile application, the redirect URI set in the authentication server
|
|
* has to use the protocol provided as the scheme in **app.json** [`expo.scheme`](./../config/app/#scheme).
|
|
* For example, `demo://` not `https://` protocol.
|
|
* Using `Linking.addEventListener` is not needed and can have side effects.
|
|
*
|
|
* # On web:
|
|
* > This API can only be used in a secure environment (localhost/https).
|
|
* to test this. Otherwise, an error with code [`ERR_WEB_BROWSER_CRYPTO`](#err_web_browser_crypto) will be thrown.
|
|
* This will use the browser's [`window.open()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) API.
|
|
* - _Desktop_: This will create a new web popup window in the browser that can be closed later using `WebBrowser.maybeCompleteAuthSession()`.
|
|
* - _Mobile_: This will open a new tab in the browser which can be closed using `WebBrowser.maybeCompleteAuthSession()`.
|
|
*
|
|
* How this works on web:
|
|
* - A crypto state will be created for verifying the redirect.
|
|
* - This means you need to run with `npx expo start --https`
|
|
* - The state will be added to the window's `localstorage`. This ensures that auth cannot complete
|
|
* unless it's done from a page running with the same origin as it was started.
|
|
* Ex: if `openAuthSessionAsync` is invoked on `https://localhost:19006`, then `maybeCompleteAuthSession`
|
|
* must be invoked on a page hosted from the origin `https://localhost:19006`. Using a different
|
|
* website, or even a different host like `https://128.0.0.*:19006` for example will not work.
|
|
* - A timer will be started to check for every 1000 milliseconds (1 second) to detect if the window
|
|
* has been closed by the user. If this happens then a promise will resolve with `{ type: 'dismiss' }`.
|
|
*
|
|
* > On mobile web, Chrome and Safari will block any call to [`window.open()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/open)
|
|
* which takes too long to fire after a user interaction. This method must be invoked immediately
|
|
* after a user interaction. If the event is blocked, an error with code [`ERR_WEB_BROWSER_BLOCKED`](#err_web_browser_blocked) will be thrown.
|
|
*
|
|
* @param url The url to open in the web browser. This should be a login page.
|
|
* @param redirectUrl _Optional_ - The url to deep link back into your app.
|
|
* On web, this defaults to the output of [`Linking.createURL("")`](./linking/#linkingcreateurlpath-namedparameters).
|
|
* @param options _Optional_ - An object extending the [`WebBrowserOpenOptions`](#webbrowseropenoptions).
|
|
* If there is no native AuthSession implementation available (which is the case on Android)
|
|
* these params will be used in the browser polyfill. If there is a native AuthSession implementation,
|
|
* these params will be ignored.
|
|
*
|
|
* @return
|
|
* - If the user does not permit the application to authenticate with the given url, the Promise fulfills with `{ type: 'cancel' }` object.
|
|
* - If the user closed the web browser, the Promise fulfills with `{ type: 'cancel' }` object.
|
|
* - If the browser is closed using [`dismissBrowser`](#webbrowserdismissbrowser),
|
|
* the Promise fulfills with `{ type: 'dismiss' }` object.
|
|
*/
|
|
export async function openAuthSessionAsync(url, redirectUrl, options = {}) {
|
|
if (_authSessionIsNativelySupported()) {
|
|
if (!ExponentWebBrowser.openAuthSessionAsync) {
|
|
throw new UnavailabilityError('WebBrowser', 'openAuthSessionAsync');
|
|
}
|
|
if (['ios', 'macos', 'web'].includes(Platform.OS)) {
|
|
return ExponentWebBrowser.openAuthSessionAsync(url, redirectUrl, _processOptions(options));
|
|
}
|
|
return ExponentWebBrowser.openAuthSessionAsync(url, redirectUrl);
|
|
}
|
|
else {
|
|
return _openAuthSessionPolyfillAsync(url, redirectUrl, options);
|
|
}
|
|
}
|
|
/**
|
|
* Dismisses the current authentication session. On web, it will close the popup window associated with auth process.
|
|
*
|
|
* @return The `void` on the successful attempt or throws an error if dismiss functionality is not available.
|
|
*
|
|
* @platform ios
|
|
* @platform web
|
|
*/
|
|
export function dismissAuthSession() {
|
|
if (_authSessionIsNativelySupported()) {
|
|
if (!ExponentWebBrowser.dismissAuthSession) {
|
|
throw new UnavailabilityError('WebBrowser', 'dismissAuthSession');
|
|
}
|
|
ExponentWebBrowser.dismissAuthSession();
|
|
}
|
|
else {
|
|
if (!ExponentWebBrowser.dismissBrowser) {
|
|
throw new UnavailabilityError('WebBrowser', 'dismissBrowser');
|
|
}
|
|
ExponentWebBrowser.dismissBrowser();
|
|
}
|
|
}
|
|
// @needsAudit
|
|
/**
|
|
* Possibly completes an authentication session on web in a window popup. The method
|
|
* should be invoked on the page that the window redirects to.
|
|
*
|
|
* @param options
|
|
*
|
|
* @return Returns an object with message about why the redirect failed or succeeded:
|
|
*
|
|
* If `type` is set to `failed`, the reason depends on the message:
|
|
* - `Not supported on this platform`: If the platform doesn't support this method (Android, iOS).
|
|
* - `Cannot use expo-web-browser in a non-browser environment`: If the code was executed in an SSR
|
|
* or node environment.
|
|
* - `No auth session is currently in progress`: (the cached state wasn't found in local storage).
|
|
* This can happen if the window redirects to an origin (website) that is different to the initial
|
|
* website origin. If this happens in development, it may be because the auth started on localhost
|
|
* and finished on your computer port (Ex: `128.0.0.*`). This is controlled by the `redirectUrl`
|
|
* and `returnUrl`.
|
|
* - `Current URL "<URL>" and original redirect URL "<URL>" do not match`: This can occur when the
|
|
* redirect URL doesn't match what was initial defined as the `returnUrl`. You can skip this test
|
|
* in development by passing `{ skipRedirectCheck: true }` to the function.
|
|
*
|
|
* If `type` is set to `success`, the parent window will attempt to close the child window immediately.
|
|
*
|
|
* If the error `ERR_WEB_BROWSER_REDIRECT` was thrown, it may mean that the parent window was
|
|
* reloaded before the auth was completed. In this case you'll need to close the child window manually.
|
|
*
|
|
* @platform web
|
|
*/
|
|
export function maybeCompleteAuthSession(options = {}) {
|
|
if (ExponentWebBrowser.maybeCompleteAuthSession) {
|
|
return ExponentWebBrowser.maybeCompleteAuthSession(options);
|
|
}
|
|
return { type: 'failed', message: 'Not supported on this platform' };
|
|
}
|
|
function _processOptions(options) {
|
|
return {
|
|
...options,
|
|
controlsColor: processColor(options.controlsColor),
|
|
toolbarColor: processColor(options.toolbarColor),
|
|
secondaryToolbarColor: processColor(options.secondaryToolbarColor),
|
|
};
|
|
}
|
|
/* Android polyfill for ASWebAuthenticationSession flow */
|
|
function _authSessionIsNativelySupported() {
|
|
return Platform.OS !== 'android';
|
|
}
|
|
let _redirectSubscription = null;
|
|
/*
|
|
* openBrowserAsync on Android doesn't wait until closed, so we need to polyfill
|
|
* it with AppState
|
|
*/
|
|
// Store the `resolve` function from a Promise to fire when the AppState
|
|
// returns to active
|
|
let _onWebBrowserCloseAndroid = null;
|
|
// If the initial AppState.currentState is null, we assume that the first call to
|
|
// AppState#change event is not actually triggered by a real change,
|
|
// is triggered instead by the bridge capturing the current state
|
|
// (https://reactnative.dev/docs/appstate#basic-usage)
|
|
let _isAppStateAvailable = AppState.currentState !== null;
|
|
function _onAppStateChangeAndroid(state) {
|
|
if (!_isAppStateAvailable) {
|
|
_isAppStateAvailable = true;
|
|
return;
|
|
}
|
|
if (state === 'active' && _onWebBrowserCloseAndroid) {
|
|
_onWebBrowserCloseAndroid();
|
|
}
|
|
}
|
|
async function _openBrowserAndWaitAndroidAsync(startUrl, browserParams = {}) {
|
|
const appStateChangedToActive = new Promise((resolve) => {
|
|
_onWebBrowserCloseAndroid = resolve;
|
|
});
|
|
const stateChangeSubscription = AppState.addEventListener('change', _onAppStateChangeAndroid);
|
|
let result = { type: WebBrowserResultType.CANCEL };
|
|
let type = null;
|
|
try {
|
|
({ type } = await openBrowserAsync(startUrl, browserParams));
|
|
}
|
|
catch (e) {
|
|
stateChangeSubscription.remove();
|
|
_onWebBrowserCloseAndroid = null;
|
|
throw e;
|
|
}
|
|
if (type === 'opened') {
|
|
await appStateChangedToActive;
|
|
result = { type: WebBrowserResultType.DISMISS };
|
|
}
|
|
stateChangeSubscription.remove();
|
|
_onWebBrowserCloseAndroid = null;
|
|
return result;
|
|
}
|
|
async function _openAuthSessionPolyfillAsync(startUrl, returnUrl, browserParams = {}) {
|
|
if (_redirectSubscription) {
|
|
throw new Error(`The WebBrowser's auth session is in an invalid state with a redirect handler set when it should not be`);
|
|
}
|
|
if (_onWebBrowserCloseAndroid) {
|
|
throw new Error(`WebBrowser is already open, only one can be open at a time`);
|
|
}
|
|
try {
|
|
if (Platform.OS === 'android') {
|
|
return await Promise.race([
|
|
_openBrowserAndWaitAndroidAsync(startUrl, browserParams),
|
|
_waitForRedirectAsync(returnUrl),
|
|
]);
|
|
}
|
|
else {
|
|
return await Promise.race([
|
|
openBrowserAsync(startUrl, browserParams),
|
|
_waitForRedirectAsync(returnUrl),
|
|
]);
|
|
}
|
|
}
|
|
finally {
|
|
// We can't dismiss the browser on Android, only call this when it's available.
|
|
// Users on Android need to manually press the 'x' button in Chrome Custom Tabs, sadly.
|
|
if (ExponentWebBrowser.dismissBrowser) {
|
|
ExponentWebBrowser.dismissBrowser();
|
|
}
|
|
_stopWaitingForRedirect();
|
|
}
|
|
}
|
|
function _stopWaitingForRedirect() {
|
|
if (!_redirectSubscription) {
|
|
throw new Error(`The WebBrowser auth session is in an invalid state with no redirect handler when one should be set`);
|
|
}
|
|
_redirectSubscription.remove();
|
|
_redirectSubscription = null;
|
|
}
|
|
function _waitForRedirectAsync(returnUrl) {
|
|
// Note that this Promise never resolves when `returnUrl` is nullish
|
|
return new Promise((resolve) => {
|
|
const redirectHandler = (event) => {
|
|
if (returnUrl && event.url.startsWith(returnUrl)) {
|
|
resolve({ url: event.url, type: 'success' });
|
|
}
|
|
};
|
|
_redirectSubscription = Linking.addEventListener('url', redirectHandler);
|
|
});
|
|
}
|
|
//# sourceMappingURL=WebBrowser.js.map
|