1import assert from 'assert'; 2import chalk from 'chalk'; 3 4import * as Log from '../../log'; 5import { env } from '../../utils/env'; 6import { CommandError } from '../../utils/errors'; 7import { learnMore } from '../../utils/link'; 8import promptAsync, { Question } from '../../utils/prompts'; 9import { ApiV2Error } from '../rest/client'; 10import { retryUsernamePasswordAuthWithOTPAsync } from './otp'; 11import { Actor, getUserAsync, loginAsync, ssoLoginAsync } from './user'; 12 13/** Show login prompt while prompting for missing credentials. */ 14export async function showLoginPromptAsync({ 15 printNewLine = false, 16 otp, 17 ...options 18}: { 19 printNewLine?: boolean; 20 username?: string; 21 password?: string; 22 otp?: string; 23 sso?: boolean | undefined; 24} = {}): Promise<void> { 25 if (env.EXPO_OFFLINE) { 26 throw new CommandError('OFFLINE', 'Cannot authenticate in offline-mode'); 27 } 28 const hasCredentials = options.username && options.password; 29 const sso = options.sso; 30 31 if (printNewLine) { 32 Log.log(); 33 } 34 35 if (sso) { 36 await ssoLoginAsync(); 37 return; 38 } 39 40 Log.log( 41 hasCredentials 42 ? `Logging in to EAS with email or username (exit and run 'eas login' for other options)` 43 : `Log in to EAS with email or username (exit and run 'eas login' for other options)` 44 ); 45 46 let username = options.username; 47 let password = options.password; 48 49 if (!hasCredentials) { 50 const resolved = await promptAsync( 51 [ 52 !options.username && { 53 type: 'text', 54 name: 'username', 55 message: 'Email or username', 56 }, 57 !options.password && { 58 type: 'password', 59 name: 'password', 60 message: 'Password', 61 }, 62 ].filter(Boolean) as Question<string>[], 63 { 64 nonInteractiveHelp: `Use the EXPO_TOKEN environment variable to authenticate in CI (${learnMore( 65 'https://docs.expo.dev/accounts/programmatic-access/' 66 )})`, 67 } 68 ); 69 username ??= resolved.username; 70 password ??= resolved.password; 71 } 72 // This is just for the types. 73 assert(username && password); 74 75 try { 76 await loginAsync({ 77 username, 78 password, 79 otp, 80 }); 81 } catch (e) { 82 if (e instanceof ApiV2Error && e.expoApiV2ErrorCode === 'ONE_TIME_PASSWORD_REQUIRED') { 83 await retryUsernamePasswordAuthWithOTPAsync( 84 username, 85 password, 86 e.expoApiV2ErrorMetadata as any 87 ); 88 } else { 89 throw e; 90 } 91 } 92} 93 94/** Ensure the user is logged in, if not, prompt to login. */ 95export async function ensureLoggedInAsync(): Promise<Actor> { 96 let user = await getUserAsync().catch(() => null); 97 98 if (!user) { 99 Log.warn(chalk.yellow`An Expo user account is required to proceed.`); 100 await showLoginPromptAsync({ printNewLine: true }); 101 user = await getUserAsync(); 102 } 103 104 assert(user, 'User should be logged in'); 105 return user; 106} 107