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