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