import { AppState, Linking, Platform, AppStateStatus } from 'react-native';
import { UnavailabilityError } from '@unimodules/core';
import ExponentWebBrowser from './ExpoWebBrowser';
import {
RedirectEvent,
OpenBrowserOptions,
AuthSessionResult,
CustomTabsBrowsersResults,
BrowserResult,
RedirectResult,
MayInitWithUrlResult,
WarmUpResult,
CoolDownResult,
} from './WebBrowser.types';
const emptyCustomTabsPackages: CustomTabsBrowsersResults = {
defaultBrowserPackage: undefined,
preferredBrowserPackage: undefined,
browserPackages: [],
servicePackages: [],
};
export async function getCustomTabsSupportingBrowsersAsync(): Promise<CustomTabsBrowsersResults> {
if (!ExponentWebBrowser.getCustomTabsSupportingBrowsersAsync) {
throw new UnavailabilityError('WebBrowser', 'getCustomTabsSupportingBrowsersAsync');
}
if (Platform.OS !== 'android') {
return emptyCustomTabsPackages;
} else {
return await ExponentWebBrowser.getCustomTabsSupportingBrowsersAsync();
}
}
export async function warmUpAsync(browserPackage?: string): Promise<WarmUpResult> {
if (!ExponentWebBrowser.warmUpAsync) {
throw new UnavailabilityError('WebBrowser', 'warmUpAsync');
}
if (Platform.OS !== 'android') {
return {};
} else {
return await ExponentWebBrowser.warmUpAsync(browserPackage);
}
}
export async function mayInitWithUrlAsync(
url: string,
browserPackage?: string
): Promise<MayInitWithUrlResult> {
if (!ExponentWebBrowser.mayInitWithUrlAsync) {
throw new UnavailabilityError('WebBrowser', 'mayInitWithUrlAsync');
}
if (Platform.OS !== 'android') {
return {};
} else {
return await ExponentWebBrowser.mayInitWithUrlAsync(url, browserPackage);
}
}
export async function coolDownAsync(browserPackage?: string): Promise<CoolDownResult> {
if (!ExponentWebBrowser.coolDownAsync) {
throw new UnavailabilityError('WebBrowser', 'coolDownAsync');
}
if (Platform.OS !== 'android') {
return {};
} else {
return await ExponentWebBrowser.coolDownAsync(browserPackage);
}
}
export async function openBrowserAsync(
url: string,
browserParams: OpenBrowserOptions = {}
): Promise<BrowserResult> {
if (!ExponentWebBrowser.openBrowserAsync) {
throw new UnavailabilityError('WebBrowser', 'openBrowserAsync');
}
return await ExponentWebBrowser.openBrowserAsync(url, browserParams);
}
export function dismissBrowser(): void {
if (!ExponentWebBrowser.dismissBrowser) {
throw new UnavailabilityError('WebBrowser', 'dismissBrowser');
}
ExponentWebBrowser.dismissBrowser();
}
export async function openAuthSessionAsync(
url: string,
redirectUrl: string
): Promise<AuthSessionResult> {
if (_authSessionIsNativelySupported()) {
if (!ExponentWebBrowser.openAuthSessionAsync) {
throw new UnavailabilityError('WebBrowser', 'openAuthSessionAsync');
}
return ExponentWebBrowser.openAuthSessionAsync(url, redirectUrl);
} else {
return _openAuthSessionPolyfillAsync(url, redirectUrl);
}
}
export function dismissAuthSession(): void {
if (_authSessionIsNativelySupported()) {
if (!ExponentWebBrowser.dismissAuthSession) {
throw new UnavailabilityError('WebBrowser', 'dismissAuthSession');
}
ExponentWebBrowser.dismissAuthSession();
} else {
if (!ExponentWebBrowser.dismissBrowser) {
throw new UnavailabilityError('WebBrowser', 'dismissAuthSession');
}
ExponentWebBrowser.dismissBrowser();
}
}
/* iOS <= 10 and Android polyfill for SFAuthenticationSession flow */
function _authSessionIsNativelySupported(): boolean {
if (Platform.OS === 'android') {
return false;
}
const versionNumber = parseInt(String(Platform.Version), 10);
return versionNumber >= 11;
}
let _redirectHandler: ((event: RedirectEvent) => void) | null = 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 | (() => void) = null;
function _onAppStateChangeAndroid(state: AppStateStatus) {
if (state === 'active' && _onWebBrowserCloseAndroid) {
_onWebBrowserCloseAndroid();
}
}
async function _openBrowserAndWaitAndroidAsync(startUrl: string): Promise<BrowserResult> {
let appStateChangedToActive = new Promise(resolve => {
_onWebBrowserCloseAndroid = resolve;
AppState.addEventListener('change', _onAppStateChangeAndroid);
});
let result: BrowserResult = { type: 'cancel' };
let { type } = await openBrowserAsync(startUrl);
if (type === 'opened') {
await appStateChangedToActive;
result = { type: 'dismiss' };
}
AppState.removeEventListener('change', _onAppStateChangeAndroid);
_onWebBrowserCloseAndroid = null;
return result;
}
async function _openAuthSessionPolyfillAsync(
startUrl: string,
returnUrl: string
): Promise<AuthSessionResult> {
if (_redirectHandler) {
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),
_waitForRedirectAsync(returnUrl),
]);
} else {
return await Promise.race([openBrowserAsync(startUrl), _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 (!_redirectHandler) {
throw new Error(
`The WebBrowser auth session is in an invalid state with no redirect handler when one should be set`
);
}
Linking.removeEventListener('url', _redirectHandler);
_redirectHandler = null;
}
function _waitForRedirectAsync(returnUrl: string): Promise<RedirectResult> {
return new Promise(resolve => {
_redirectHandler = (event: RedirectEvent) => {
if (event.url.startsWith(returnUrl)) {
resolve({ url: event.url, type: 'success' });
}
};
Linking.addEventListener('url', _redirectHandler);
});
}