1import Constants, { ExecutionEnvironment } from 'expo-constants';
2import * as Linking from 'expo-linking';
3import { Platform } from 'expo-modules-core';
4import qs, { ParsedQs } from 'qs';
5
6export class SessionUrlProvider {
7  private static readonly BASE_URL = `https://auth.expo.io`;
8  private static readonly SESSION_PATH = 'expo-auth-session';
9
10  getDefaultReturnUrl(
11    urlPath?: string,
12    options?: Omit<Linking.CreateURLOptions, 'queryParams'>
13  ): string {
14    const queryParams = SessionUrlProvider.getHostAddressQueryParams();
15    let path = SessionUrlProvider.SESSION_PATH;
16    if (urlPath) {
17      path = [path, SessionUrlProvider.removeLeadingSlash(urlPath)].filter(Boolean).join('/');
18    }
19
20    return Linking.createURL(path, {
21      // The redirect URL doesn't matter for the proxy as long as it's valid, so silence warnings if needed.
22      scheme: options?.scheme ?? Linking.resolveScheme({ isSilent: true }),
23      queryParams,
24      isTripleSlashed: options?.isTripleSlashed,
25    });
26  }
27
28  getStartUrl(authUrl: string, returnUrl: string, projectNameForProxy: string | undefined): string {
29    if (Platform.OS === 'web' && !Platform.isDOMAvailable) {
30      // Return nothing in SSR envs
31      return '';
32    }
33    const queryString = qs.stringify({
34      authUrl,
35      returnUrl,
36    });
37
38    return `${this.getRedirectUrl({ projectNameForProxy })}/start?${queryString}`;
39  }
40
41  getRedirectUrl(options: { projectNameForProxy?: string; urlPath?: string }): string {
42    if (Platform.OS === 'web') {
43      if (Platform.isDOMAvailable) {
44        return [window.location.origin, options.urlPath].filter(Boolean).join('/');
45      } else {
46        // Return nothing in SSR envs
47        return '';
48      }
49    }
50
51    const legacyExpoProjectFullName =
52      options.projectNameForProxy ||
53      Constants.expoConfig?.originalFullName ||
54      Constants.__unsafeNoWarnManifest?.id;
55
56    if (!legacyExpoProjectFullName) {
57      let nextSteps = '';
58      if (__DEV__) {
59        if (Constants.executionEnvironment === ExecutionEnvironment.Bare) {
60          nextSteps =
61            ' Please ensure you have the latest version of expo-constants installed and rebuild your native app. You can verify that originalFullName is defined by running `expo config --type public` and inspecting the output.';
62        } else if (Constants.executionEnvironment === ExecutionEnvironment.StoreClient) {
63          nextSteps =
64            ' Please report this as a bug with the contents of `expo config --type public`.';
65        }
66      }
67
68      if (Constants.manifest2) {
69        nextSteps =
70          ' Prefer AuthRequest (with the useProxy option set to false) in combination with an Expo Development Client build of your application.' +
71          ' To continue using the AuthSession proxy, specify the project full name (@owner/slug) using the projectNameForProxy option.';
72      }
73
74      throw new Error(
75        'Cannot use the AuthSession proxy because the project full name is not defined.' + nextSteps
76      );
77    }
78
79    const redirectUrl = `${SessionUrlProvider.BASE_URL}/${legacyExpoProjectFullName}`;
80    if (__DEV__) {
81      SessionUrlProvider.warnIfAnonymous(legacyExpoProjectFullName, redirectUrl);
82      // TODO: Verify with the dev server that the manifest is up to date.
83    }
84    return redirectUrl;
85  }
86
87  private static getHostAddressQueryParams(): ParsedQs | undefined {
88    let hostUri: string | undefined = Constants.expoConfig?.hostUri;
89    if (
90      !hostUri &&
91      (ExecutionEnvironment.StoreClient === Constants.executionEnvironment ||
92        Linking.resolveScheme({}))
93    ) {
94      if (!Constants.linkingUri) {
95        hostUri = '';
96      } else {
97        // we're probably not using up-to-date xdl, so just fake it for now
98        // we have to remove the /--/ on the end since this will be inserted again later
99        hostUri = SessionUrlProvider.removeScheme(Constants.linkingUri).replace(/\/--(\/.*)?$/, '');
100      }
101    }
102
103    if (!hostUri) {
104      return undefined;
105    }
106
107    const uriParts = hostUri?.split('?');
108    try {
109      return qs.parse(uriParts?.[1]);
110    } catch {}
111
112    return undefined;
113  }
114
115  private static warnIfAnonymous(id, url): void {
116    if (id.startsWith('@anonymous/')) {
117      console.warn(
118        `You are not currently signed in to Expo on your development machine. As a result, the redirect URL for AuthSession will be "${url}". If you are using an OAuth provider that requires adding redirect URLs to an allow list, we recommend that you do not add this URL -- instead, you should sign in to Expo to acquire a unique redirect URL. Additionally, if you do decide to publish this app using Expo, you will need to register an account to do it.`
119      );
120    }
121  }
122
123  private static removeScheme(url: string) {
124    return url.replace(/^[a-zA-Z0-9+.-]+:\/\//, '');
125  }
126
127  private static removeLeadingSlash(url: string) {
128    return url.replace(/^\//, '');
129  }
130}
131
132export default new SessionUrlProvider();
133