18d307f52SEvan Baconimport assert from 'assert'; 28d307f52SEvan Baconimport chalk from 'chalk'; 38d307f52SEvan Bacon 4*8a424bebSJames Ideimport { retryUsernamePasswordAuthWithOTPAsync } from './otp'; 5*8a424bebSJames Ideimport { Actor, getUserAsync, loginAsync, ssoLoginAsync } from './user'; 68d307f52SEvan Baconimport * as Log from '../../log'; 7e32ccf9fSEvan Baconimport { env } from '../../utils/env'; 8e32ccf9fSEvan Baconimport { CommandError } from '../../utils/errors'; 98d307f52SEvan Baconimport { learnMore } from '../../utils/link'; 108d307f52SEvan Baconimport promptAsync, { Question } from '../../utils/prompts'; 118d307f52SEvan Baconimport { ApiV2Error } from '../rest/client'; 128d307f52SEvan Bacon 138d307f52SEvan Bacon/** Show login prompt while prompting for missing credentials. */ 148d307f52SEvan Baconexport async function showLoginPromptAsync({ 158d307f52SEvan Bacon printNewLine = false, 168d307f52SEvan Bacon otp, 178d307f52SEvan Bacon ...options 188d307f52SEvan Bacon}: { 198d307f52SEvan Bacon printNewLine?: boolean; 208d307f52SEvan Bacon username?: string; 218d307f52SEvan Bacon password?: string; 228d307f52SEvan Bacon otp?: string; 23d88ac65dSlzkb sso?: boolean | undefined; 248d307f52SEvan Bacon} = {}): Promise<void> { 25e32ccf9fSEvan Bacon if (env.EXPO_OFFLINE) { 26e32ccf9fSEvan Bacon throw new CommandError('OFFLINE', 'Cannot authenticate in offline-mode'); 27e32ccf9fSEvan Bacon } 288d307f52SEvan Bacon const hasCredentials = options.username && options.password; 29d88ac65dSlzkb const sso = options.sso; 308d307f52SEvan Bacon 318d307f52SEvan Bacon if (printNewLine) { 328d307f52SEvan Bacon Log.log(); 338d307f52SEvan Bacon } 348d307f52SEvan Bacon 35d88ac65dSlzkb if (sso) { 36d88ac65dSlzkb await ssoLoginAsync(); 37d88ac65dSlzkb return; 38d88ac65dSlzkb } 39d88ac65dSlzkb 40d88ac65dSlzkb Log.log( 41d88ac65dSlzkb hasCredentials 42d88ac65dSlzkb ? `Logging in to EAS with email or username (exit and run 'eas login' for other options)` 43d88ac65dSlzkb : `Log in to EAS with email or username (exit and run 'eas login' for other options)` 44d88ac65dSlzkb ); 458d307f52SEvan Bacon 468d307f52SEvan Bacon let username = options.username; 478d307f52SEvan Bacon let password = options.password; 488d307f52SEvan Bacon 498d307f52SEvan Bacon if (!hasCredentials) { 508d307f52SEvan Bacon const resolved = await promptAsync( 518d307f52SEvan Bacon [ 528d307f52SEvan Bacon !options.username && { 538d307f52SEvan Bacon type: 'text', 548d307f52SEvan Bacon name: 'username', 558d307f52SEvan Bacon message: 'Email or username', 568d307f52SEvan Bacon }, 578d307f52SEvan Bacon !options.password && { 588d307f52SEvan Bacon type: 'password', 598d307f52SEvan Bacon name: 'password', 608d307f52SEvan Bacon message: 'Password', 618d307f52SEvan Bacon }, 628d307f52SEvan Bacon ].filter(Boolean) as Question<string>[], 638d307f52SEvan Bacon { 648d307f52SEvan Bacon nonInteractiveHelp: `Use the EXPO_TOKEN environment variable to authenticate in CI (${learnMore( 658d307f52SEvan Bacon 'https://docs.expo.dev/accounts/programmatic-access/' 668d307f52SEvan Bacon )})`, 678d307f52SEvan Bacon } 688d307f52SEvan Bacon ); 6929975bfdSEvan Bacon username ??= resolved.username; 7029975bfdSEvan Bacon password ??= resolved.password; 718d307f52SEvan Bacon } 7229975bfdSEvan Bacon // This is just for the types. 7329975bfdSEvan Bacon assert(username && password); 748d307f52SEvan Bacon 758d307f52SEvan Bacon try { 768d307f52SEvan Bacon await loginAsync({ 778d307f52SEvan Bacon username, 788d307f52SEvan Bacon password, 798d307f52SEvan Bacon otp, 808d307f52SEvan Bacon }); 818d307f52SEvan Bacon } catch (e) { 828d307f52SEvan Bacon if (e instanceof ApiV2Error && e.expoApiV2ErrorCode === 'ONE_TIME_PASSWORD_REQUIRED') { 838d307f52SEvan Bacon await retryUsernamePasswordAuthWithOTPAsync( 848d307f52SEvan Bacon username, 858d307f52SEvan Bacon password, 868d307f52SEvan Bacon e.expoApiV2ErrorMetadata as any 878d307f52SEvan Bacon ); 888d307f52SEvan Bacon } else { 898d307f52SEvan Bacon throw e; 908d307f52SEvan Bacon } 918d307f52SEvan Bacon } 928d307f52SEvan Bacon} 938d307f52SEvan Bacon 948d307f52SEvan Bacon/** Ensure the user is logged in, if not, prompt to login. */ 958d307f52SEvan Baconexport async function ensureLoggedInAsync(): Promise<Actor> { 968d307f52SEvan Bacon let user = await getUserAsync().catch(() => null); 978d307f52SEvan Bacon 988d307f52SEvan Bacon if (!user) { 998d307f52SEvan Bacon Log.warn(chalk.yellow`An Expo user account is required to proceed.`); 1008d307f52SEvan Bacon await showLoginPromptAsync({ printNewLine: true }); 1018d307f52SEvan Bacon user = await getUserAsync(); 1028d307f52SEvan Bacon } 1038d307f52SEvan Bacon 1048d307f52SEvan Bacon assert(user, 'User should be logged in'); 1058d307f52SEvan Bacon return user; 1068d307f52SEvan Bacon} 107