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