xref: /expo/packages/@expo/cli/src/api/user/actions.ts (revision 604792ab)
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 } 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} = {}): Promise<void> {
24  if (env.EXPO_OFFLINE) {
25    throw new CommandError('OFFLINE', 'Cannot authenticate in offline-mode');
26  }
27  const hasCredentials = options.username && options.password;
28
29  if (printNewLine) {
30    Log.log();
31  }
32
33  Log.log(hasCredentials ? 'Logging in to EAS' : 'Log in to EAS');
34
35  let username = options.username;
36  let password = options.password;
37
38  if (!hasCredentials) {
39    const resolved = await promptAsync(
40      [
41        !options.username && {
42          type: 'text',
43          name: 'username',
44          message: 'Email or username',
45        },
46        !options.password && {
47          type: 'password',
48          name: 'password',
49          message: 'Password',
50        },
51      ].filter(Boolean) as Question<string>[],
52      {
53        nonInteractiveHelp: `Use the EXPO_TOKEN environment variable to authenticate in CI (${learnMore(
54          'https://docs.expo.dev/accounts/programmatic-access/'
55        )})`,
56      }
57    );
58    username ??= resolved.username;
59    password ??= resolved.password;
60  }
61  // This is just for the types.
62  assert(username && password);
63
64  try {
65    await loginAsync({
66      username,
67      password,
68      otp,
69    });
70  } catch (e) {
71    if (e instanceof ApiV2Error && e.expoApiV2ErrorCode === 'ONE_TIME_PASSWORD_REQUIRED') {
72      await retryUsernamePasswordAuthWithOTPAsync(
73        username,
74        password,
75        e.expoApiV2ErrorMetadata as any
76      );
77    } else {
78      throw e;
79    }
80  }
81}
82
83/** Ensure the user is logged in, if not, prompt to login. */
84export async function ensureLoggedInAsync(): Promise<Actor> {
85  let user = await getUserAsync().catch(() => null);
86
87  if (!user) {
88    Log.warn(chalk.yellow`An Expo user account is required to proceed.`);
89    await showLoginPromptAsync({ printNewLine: true });
90    user = await getUserAsync();
91  }
92
93  assert(user, 'User should be logged in');
94  return user;
95}
96