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