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.manifest?.originalFullName ||
54      Constants.manifest2?.extra?.expoClient?.originalFullName ||
55      Constants.manifest?.id;
56
57    if (!legacyExpoProjectFullName) {
58      let nextSteps = '';
59      if (__DEV__) {
60        if (Constants.executionEnvironment === ExecutionEnvironment.Bare) {
61          nextSteps =
62            ' 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.';
63        } else if (Constants.executionEnvironment === ExecutionEnvironment.StoreClient) {
64          nextSteps =
65            ' Please report this as a bug with the contents of `expo config --type public`.';
66        }
67      }
68
69      if (Constants.manifest2) {
70        nextSteps =
71          ' Prefer AuthRequest (with the useProxy option set to false) in combination with an Expo Development Client build of your application.' +
72          ' To continue using the AuthSession proxy, specify the project full name (@owner/slug) using the projectNameForProxy option.';
73      }
74
75      throw new Error(
76        'Cannot use the AuthSession proxy because the project full name is not defined.' + nextSteps
77      );
78    }
79
80    const redirectUrl = `${SessionUrlProvider.BASE_URL}/${legacyExpoProjectFullName}`;
81    if (__DEV__) {
82      SessionUrlProvider.warnIfAnonymous(legacyExpoProjectFullName, redirectUrl);
83      // TODO: Verify with the dev server that the manifest is up to date.
84    }
85    return redirectUrl;
86  }
87
88  private static getHostAddressQueryParams(): ParsedQs | undefined {
89    let hostUri: string | undefined =
90      Constants.manifest?.hostUri ?? Constants.manifest2?.extra?.expoClient?.hostUri;
91    if (
92      !hostUri &&
93      (ExecutionEnvironment.StoreClient === Constants.executionEnvironment ||
94        Linking.resolveScheme({}))
95    ) {
96      if (!Constants.linkingUri) {
97        hostUri = '';
98      } else {
99        // we're probably not using up-to-date xdl, so just fake it for now
100        // we have to remove the /--/ on the end since this will be inserted again later
101        hostUri = SessionUrlProvider.removeScheme(Constants.linkingUri).replace(/\/--(\/.*)?$/, '');
102      }
103    }
104
105    if (!hostUri) {
106      return undefined;
107    }
108
109    const uriParts = hostUri?.split('?');
110    try {
111      return qs.parse(uriParts?.[1]);
112    } catch {}
113
114    return undefined;
115  }
116
117  private static warnIfAnonymous(id, url): void {
118    if (id.startsWith('@anonymous/')) {
119      console.warn(
120        `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.`
121      );
122    }
123  }
124
125  private static removeScheme(url: string) {
126    return url.replace(/^[a-zA-Z0-9+.-]+:\/\//, '');
127  }
128
129  private static removeLeadingSlash(url: string) {
130    return url.replace(/^\//, '');
131  }
132}
133
134export default new SessionUrlProvider();
135