1 | import { UnavailabilityError } from '@unimodules/core';
|
2 | import { AppState, AppStateStatus, Linking, Platform } from 'react-native';
|
3 |
|
4 | import ExponentWebBrowser from './ExpoWebBrowser';
|
5 | import {
|
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 |
|
19 | export {
|
20 | WebBrowserAuthSessionResult,
|
21 | WebBrowserCoolDownResult,
|
22 | WebBrowserCustomTabsResults,
|
23 | WebBrowserMayInitWithUrlResult,
|
24 | WebBrowserOpenOptions,
|
25 | WebBrowserRedirectResult,
|
26 | WebBrowserResult,
|
27 | WebBrowserResultType,
|
28 | WebBrowserWarmUpResult,
|
29 | WebBrowserWindowFeatures,
|
30 | };
|
31 |
|
32 | const emptyCustomTabsPackages: WebBrowserCustomTabsResults = {
|
33 | defaultBrowserPackage: undefined,
|
34 | preferredBrowserPackage: undefined,
|
35 | browserPackages: [],
|
36 | servicePackages: [],
|
37 | };
|
38 |
|
39 | export 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 |
|
50 | export 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 |
|
61 | export 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 |
|
75 | export 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 |
|
86 | let browserLocked = false;
|
87 |
|
88 | export 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 |
|
98 |
|
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 |
|
114 | browserLocked = false;
|
115 | }
|
116 |
|
117 | return result;
|
118 | }
|
119 |
|
120 | export function dismissBrowser(): void {
|
121 | if (!ExponentWebBrowser.dismissBrowser) {
|
122 | throw new UnavailabilityError('WebBrowser', 'dismissBrowser');
|
123 | }
|
124 | ExponentWebBrowser.dismissBrowser();
|
125 | }
|
126 |
|
127 | export 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 |
|
145 | export 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 |
|
161 |
|
162 |
|
163 |
|
164 | export 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 |
|
174 |
|
175 | function _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 |
|
186 | let _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
|
195 | let _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:
|
201 | let _isAppStateAvailable: boolean = AppState.currentState !== null;
|
202 | function _onAppStateChangeAndroid(state: AppStateStatus) {
|
203 | if (!_isAppStateAvailable) {
|
204 | _isAppStateAvailable = true;
|
205 | return;
|
206 | }
|
207 |
|
208 | if (state === 'active' && _onWebBrowserCloseAndroid) {
|
209 | _onWebBrowserCloseAndroid();
|
210 | }
|
211 | }
|
212 |
|
213 | async 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 |
|
235 | async 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 |
|
264 |
|
265 | if (ExponentWebBrowser.dismissBrowser) {
|
266 | ExponentWebBrowser.dismissBrowser();
|
267 | }
|
268 |
|
269 | _stopWaitingForRedirect();
|
270 | }
|
271 | }
|
272 |
|
273 | function _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 |
|
284 | function _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 |