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