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