UNPKG

8.83 kBPlain TextView Raw
1import { UnavailabilityError } from '@unimodules/core';
2import { AppState, AppStateStatus, Linking, Platform } from 'react-native';
3
4import ExponentWebBrowser from './ExpoWebBrowser';
5import {
6 RedirectEvent,
7 WebBrowserAuthSessionResult,
8 WebBrowserCoolDownResult,
9 WebBrowserCustomTabsResults,
10 WebBrowserMayInitWithUrlResult,
11 WebBrowserOpenOptions,
12 WebBrowserRedirectResult,
13 WebBrowserResult,
14 WebBrowserResultType,
15 WebBrowserWarmUpResult,
16 WebBrowserWindowFeatures,
17} from './WebBrowser.types';
18
19export {
20 WebBrowserAuthSessionResult,
21 WebBrowserCoolDownResult,
22 WebBrowserCustomTabsResults,
23 WebBrowserMayInitWithUrlResult,
24 WebBrowserOpenOptions,
25 WebBrowserRedirectResult,
26 WebBrowserResult,
27 WebBrowserResultType,
28 WebBrowserWarmUpResult,
29 WebBrowserWindowFeatures,
30};
31
32const emptyCustomTabsPackages: WebBrowserCustomTabsResults = {
33 defaultBrowserPackage: undefined,
34 preferredBrowserPackage: undefined,
35 browserPackages: [],
36 servicePackages: [],
37};
38
39export async function getCustomTabsSupportingBrowsersAsync(): Promise<WebBrowserCustomTabsResults> {
40 if (!ExponentWebBrowser.getCustomTabsSupportingBrowsersAsync) {
41 throw new UnavailabilityError('WebBrowser', 'getCustomTabsSupportingBrowsersAsync');
42 }
43 if (Platform.OS !== 'android') {
44 return emptyCustomTabsPackages;
45 } else {
46 return await ExponentWebBrowser.getCustomTabsSupportingBrowsersAsync();
47 }
48}
49
50export async function warmUpAsync(browserPackage?: string): Promise<WebBrowserWarmUpResult> {
51 if (!ExponentWebBrowser.warmUpAsync) {
52 throw new UnavailabilityError('WebBrowser', 'warmUpAsync');
53 }
54 if (Platform.OS !== 'android') {
55 return {};
56 } else {
57 return await ExponentWebBrowser.warmUpAsync(browserPackage);
58 }
59}
60
61export async function mayInitWithUrlAsync(
62 url: string,
63 browserPackage?: string
64): Promise<WebBrowserMayInitWithUrlResult> {
65 if (!ExponentWebBrowser.mayInitWithUrlAsync) {
66 throw new UnavailabilityError('WebBrowser', 'mayInitWithUrlAsync');
67 }
68 if (Platform.OS !== 'android') {
69 return {};
70 } else {
71 return await ExponentWebBrowser.mayInitWithUrlAsync(url, browserPackage);
72 }
73}
74
75export async function coolDownAsync(browserPackage?: string): Promise<WebBrowserCoolDownResult> {
76 if (!ExponentWebBrowser.coolDownAsync) {
77 throw new UnavailabilityError('WebBrowser', 'coolDownAsync');
78 }
79 if (Platform.OS !== 'android') {
80 return {};
81 } else {
82 return await ExponentWebBrowser.coolDownAsync(browserPackage);
83 }
84}
85
86let browserLocked = false;
87
88export async function openBrowserAsync(
89 url: string,
90 browserParams: WebBrowserOpenOptions = {}
91): Promise<WebBrowserResult> {
92 if (!ExponentWebBrowser.openBrowserAsync) {
93 throw new UnavailabilityError('WebBrowser', 'openBrowserAsync');
94 }
95
96 if (browserLocked) {
97 // Prevent multiple sessions from running at the same time, WebBrowser doesn't
98 // support it this makes the behavior predictable.
99 if (__DEV__) {
100 console.warn(
101 'Attempted to call WebBrowser.openBrowserAsync multiple times while already active. Only one WebBrowser controller can be active at any given time.'
102 );
103 }
104
105 return { type: 'locked' };
106 }
107 browserLocked = true;
108
109 let result: WebBrowserResult;
110 try {
111 result = await ExponentWebBrowser.openBrowserAsync(url, browserParams);
112 } finally {
113 // WebBrowser session complete, unset lock
114 browserLocked = false;
115 }
116
117 return result;
118}
119
120export function dismissBrowser(): void {
121 if (!ExponentWebBrowser.dismissBrowser) {
122 throw new UnavailabilityError('WebBrowser', 'dismissBrowser');
123 }
124 ExponentWebBrowser.dismissBrowser();
125}
126
127export async function openAuthSessionAsync(
128 url: string,
129 redirectUrl: string,
130 browserParams: WebBrowserOpenOptions = {}
131): Promise<WebBrowserAuthSessionResult> {
132 if (_authSessionIsNativelySupported()) {
133 if (!ExponentWebBrowser.openAuthSessionAsync) {
134 throw new UnavailabilityError('WebBrowser', 'openAuthSessionAsync');
135 }
136 if (Platform.OS === 'web') {
137 return ExponentWebBrowser.openAuthSessionAsync(url, redirectUrl, browserParams);
138 }
139 return ExponentWebBrowser.openAuthSessionAsync(url, redirectUrl);
140 } else {
141 return _openAuthSessionPolyfillAsync(url, redirectUrl, browserParams);
142 }
143}
144
145export function dismissAuthSession(): void {
146 if (_authSessionIsNativelySupported()) {
147 if (!ExponentWebBrowser.dismissAuthSession) {
148 throw new UnavailabilityError('WebBrowser', 'dismissAuthSession');
149 }
150 ExponentWebBrowser.dismissAuthSession();
151 } else {
152 if (!ExponentWebBrowser.dismissBrowser) {
153 throw new UnavailabilityError('WebBrowser', 'dismissAuthSession');
154 }
155 ExponentWebBrowser.dismissBrowser();
156 }
157}
158
159/**
160 * Attempts to complete an auth session in the browser.
161 *
162 * @param options
163 */
164export function maybeCompleteAuthSession(
165 options: { skipRedirectCheck?: boolean } = {}
166): { type: 'success' | 'failed'; message: string } {
167 if (ExponentWebBrowser.maybeCompleteAuthSession) {
168 return ExponentWebBrowser.maybeCompleteAuthSession(options);
169 }
170 return { type: 'failed', message: 'Not supported on this platform' };
171}
172
173/* iOS <= 10 and Android polyfill for SFAuthenticationSession flow */
174
175function _authSessionIsNativelySupported(): boolean {
176 if (Platform.OS === 'android') {
177 return false;
178 } else if (Platform.OS === 'web') {
179 return true;
180 }
181
182 const versionNumber = parseInt(String(Platform.Version), 10);
183 return versionNumber >= 11;
184}
185
186let _redirectHandler: ((event: RedirectEvent) => void) | null = null;
187
188/*
189 * openBrowserAsync on Android doesn't wait until closed, so we need to polyfill
190 * it with AppState
191 */
192
193// Store the `resolve` function from a Promise to fire when the AppState
194// returns to active
195let _onWebBrowserCloseAndroid: null | (() => void) = null;
196
197// If the initial AppState.currentState is null, we assume that the first call to
198// AppState#change event is not actually triggered by a real change,
199// is triggered instead by the bridge capturing the current state
200// (https://reactnative.dev/docs/appstate#basic-usage)
201let _isAppStateAvailable: boolean = AppState.currentState !== null;
202function _onAppStateChangeAndroid(state: AppStateStatus) {
203 if (!_isAppStateAvailable) {
204 _isAppStateAvailable = true;
205 return;
206 }
207
208 if (state === 'active' && _onWebBrowserCloseAndroid) {
209 _onWebBrowserCloseAndroid();
210 }
211}
212
213async function _openBrowserAndWaitAndroidAsync(
214 startUrl: string,
215 browserParams: WebBrowserOpenOptions = {}
216): Promise<WebBrowserResult> {
217 const appStateChangedToActive = new Promise(resolve => {
218 _onWebBrowserCloseAndroid = resolve;
219 AppState.addEventListener('change', _onAppStateChangeAndroid);
220 });
221
222 let result: WebBrowserResult = { type: 'cancel' };
223 const { type } = await openBrowserAsync(startUrl, browserParams);
224
225 if (type === 'opened') {
226 await appStateChangedToActive;
227 result = { type: 'dismiss' };
228 }
229
230 AppState.removeEventListener('change', _onAppStateChangeAndroid);
231 _onWebBrowserCloseAndroid = null;
232 return result;
233}
234
235async function _openAuthSessionPolyfillAsync(
236 startUrl: string,
237 returnUrl: string,
238 browserParams: WebBrowserOpenOptions = {}
239): Promise<WebBrowserAuthSessionResult> {
240 if (_redirectHandler) {
241 throw new Error(
242 `The WebBrowser's auth session is in an invalid state with a redirect handler set when it should not be`
243 );
244 }
245
246 if (_onWebBrowserCloseAndroid) {
247 throw new Error(`WebBrowser is already open, only one can be open at a time`);
248 }
249
250 try {
251 if (Platform.OS === 'android') {
252 return await Promise.race([
253 _openBrowserAndWaitAndroidAsync(startUrl, browserParams),
254 _waitForRedirectAsync(returnUrl),
255 ]);
256 } else {
257 return await Promise.race([
258 openBrowserAsync(startUrl, browserParams),
259 _waitForRedirectAsync(returnUrl),
260 ]);
261 }
262 } finally {
263 // We can't dismiss the browser on Android, only call this when it's available.
264 // Users on Android need to manually press the 'x' button in Chrome Custom Tabs, sadly.
265 if (ExponentWebBrowser.dismissBrowser) {
266 ExponentWebBrowser.dismissBrowser();
267 }
268
269 _stopWaitingForRedirect();
270 }
271}
272
273function _stopWaitingForRedirect() {
274 if (!_redirectHandler) {
275 throw new Error(
276 `The WebBrowser auth session is in an invalid state with no redirect handler when one should be set`
277 );
278 }
279
280 Linking.removeEventListener('url', _redirectHandler);
281 _redirectHandler = null;
282}
283
284function _waitForRedirectAsync(returnUrl: string): Promise<WebBrowserRedirectResult> {
285 return new Promise(resolve => {
286 _redirectHandler = (event: RedirectEvent) => {
287 if (event.url.startsWith(returnUrl)) {
288 resolve({ url: event.url, type: 'success' });
289 }
290 };
291
292 Linking.addEventListener('url', _redirectHandler);
293 });
294}
295
\No newline at end of file