Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[cli] Added auth commands to expo #16087

Merged
merged 16 commits into from Feb 1, 2022
5 changes: 5 additions & 0 deletions packages/expo/bin/cli.ts
Expand Up @@ -11,6 +11,11 @@ const commands: { [command: string]: () => Promise<Command> } = {
// Add a new command here
prebuild: () => import('../cli/prebuild').then((i) => i.expoPrebuild),
config: () => import('../cli/config').then((i) => i.expoConfig),
// Auth
login: () => import('../cli/login').then((i) => i.expoLogin),
logout: () => import('../cli/logout').then((i) => i.expoLogout),
register: () => import('../cli/register').then((i) => i.expoRegister),
whoami: () => import('../cli/whoami').then((i) => i.expoWhoami),
};

const args = arg(
Expand Down
51 changes: 51 additions & 0 deletions packages/expo/cli/login/index.ts
@@ -0,0 +1,51 @@
#!/usr/bin/env node
import chalk from 'chalk';

import { Command } from '../../bin/cli';
import * as Log from '../log';
import { assertArgs } from '../utils/args';
import { logCmdError } from '../utils/errors';

export const expoLogin: Command = async (argv) => {
const args = assertArgs(
{
// Types
'--help': Boolean,
'--username': String,
'--password': String,
'--otp': String,
// Aliases
'-h': '--help',
'-u': '--username',
'-p': '--password',
},
argv
);

if (args['--help']) {
Log.exit(
chalk`
{bold Description}
Log in to an Expo account

{bold Usage}
$ npx expo login

Options
-u, --username <string> Username
-p, --password <string> Password
--otp <string> One-time password from your 2FA device
-h, --help Output usage information
`,
0
);
}

const { showLoginPromptAsync } = await import('../utils/user/actions');
return showLoginPromptAsync({
// Parsed options
username: args['--username'],
password: args['--password'],
otp: args['--otp'],
}).catch(logCmdError);
};
38 changes: 38 additions & 0 deletions packages/expo/cli/logout/index.ts
@@ -0,0 +1,38 @@
#!/usr/bin/env node
import chalk from 'chalk';

import { Command } from '../../bin/cli';
import * as Log from '../log';
import { assertArgs } from '../utils/args';
import { logCmdError } from '../utils/errors';

export const expoLogout: Command = async (argv) => {
const args = assertArgs(
{
// Types
'--help': Boolean,
// Aliases
'-h': '--help',
},
argv
);

if (args['--help']) {
Log.exit(
chalk`
{bold Description}
Log out of an Expo account

{bold Usage}
$ npx expo logout

Options
-h, --help Output usage information
`,
0
);
}

const { logoutAsync } = await import('../utils/user/user');
return logoutAsync().catch(logCmdError);
};
38 changes: 38 additions & 0 deletions packages/expo/cli/register/index.ts
@@ -0,0 +1,38 @@
#!/usr/bin/env node
import chalk from 'chalk';

import { Command } from '../../bin/cli';
import * as Log from '../log';
import { assertArgs } from '../utils/args';
import { logCmdError } from '../utils/errors';

export const expoRegister: Command = async (argv) => {
const args = assertArgs(
{
// Types
'--help': Boolean,
// Aliases
'-h': '--help',
},
argv
);

if (args['--help']) {
Log.exit(
chalk`
{bold Description}
Sign up for a new Expo account

{bold Usage}
$ npx expo register

Options
-h, --help Output usage information
`,
0
);
}

const { registerAsync } = await import('./registerAsync');
return registerAsync().catch(logCmdError);
};
34 changes: 34 additions & 0 deletions packages/expo/cli/register/registerAsync.ts
@@ -0,0 +1,34 @@
import openBrowserAsync from 'better-opn';

import { CI } from '../utils/env';
import { CommandError } from '../utils/errors';
import { learnMore } from '../utils/link';
import { ora } from '../utils/ora';

export async function registerAsync() {
if (CI) {
throw new CommandError(
'NON_INTERACTIVE',
`Cannot register an account in CI. Use the EXPO_TOKEN environment variable to authenticate in CI (${learnMore(
'https://docs.expo.dev/accounts/programmatic-access/'
)})`
);
}

const registrationUrl = `https://expo.dev/signup`;
const failedMessage = `Unable to open a web browser. Register an account at: ${registrationUrl}`;
const spinner = ora(`Opening ${registrationUrl}`).start();
try {
const opened = await openBrowserAsync(registrationUrl);

if (opened) {
spinner.succeed(`Opened ${registrationUrl}`);
} else {
spinner.fail(failedMessage);
}
return;
} catch (error) {
spinner.fail(failedMessage);
throw error;
}
}
48 changes: 48 additions & 0 deletions packages/expo/cli/utils/__tests__/api-test.ts
@@ -0,0 +1,48 @@
import assert from 'assert';
import { RequestError } from 'got/dist/source';
import nock from 'nock';

import { ApiV2Error, apiClient, getExpoApiBaseUrl } from '../api';

it('converts Expo APIv2 error to ApiV2Error', async () => {
nock(getExpoApiBaseUrl())
.post('/v2/test')
.reply(400, {
errors: [
{
message: 'hellomessage',
code: 'TEST_CODE',
stack: 'line 1: hello',
details: { who: 'world' },
metadata: { an: 'object' },
},
],
});

expect.assertions(5);

try {
await apiClient.post('test');
} catch (error: any) {
assert(error instanceof ApiV2Error);

expect(error.message).toEqual('hellomessage');
expect(error.expoApiV2ErrorCode).toEqual('TEST_CODE');
expect(error.expoApiV2ErrorDetails).toEqual({ who: 'world' });
expect(error.expoApiV2ErrorMetadata).toEqual({ an: 'object' });
expect(error.expoApiV2ErrorServerStack).toEqual('line 1: hello');
}
});

it('does not convert non-APIv2 error to ApiV2Error', async () => {
nock(getExpoApiBaseUrl()).post('/v2/test').reply(500, 'Something went wrong');

expect.assertions(2);

try {
await apiClient.post('test');
} catch (error: any) {
expect(error).toBeInstanceOf(RequestError);
expect(error).not.toBeInstanceOf(ApiV2Error);
}
});
99 changes: 99 additions & 0 deletions packages/expo/cli/utils/analytics/rudderstackClient.ts
@@ -0,0 +1,99 @@
import RudderAnalytics from '@expo/rudder-sdk-node';
EvanBacon marked this conversation as resolved.
Show resolved Hide resolved
import os from 'os';
import { v4 as uuidv4 } from 'uuid';

import { EXPO_LOCAL, EXPO_STAGING, EXPO_NO_TELEMETRY } from '../env';
import UserSettings from '../user/UserSettings';

const PLATFORM_TO_ANALYTICS_PLATFORM: { [platform: string]: string } = {
darwin: 'Mac',
win32: 'Windows',
linux: 'Linux',
};

let client: RudderAnalytics | null = null;
let identified = false;
let identifyData: {
userId: string;
deviceId: string;
traits: Record<string, any>;
} | null = null;

function getClient(): RudderAnalytics {
if (client) {
return client;
}

client = new RudderAnalytics(
EXPO_STAGING || EXPO_LOCAL ? '1wpX20Da4ltFGSXbPFYUL00Chb7' : '1wpXLFxmujq86etH6G6cc90hPcC',
'https://cdp.expo.dev/v1/batch',
{
flushInterval: 300,
}
);
EvanBacon marked this conversation as resolved.
Show resolved Hide resolved

// Install flush on exit...
process.on('SIGINT', () => client?.flush?.());
process.on('SIGTERM', () => client?.flush?.());

return client;
}

export async function setUserDataAsync(userId: string, traits: Record<string, any>): Promise<void> {
if (EXPO_NO_TELEMETRY) {
return;
}
const savedDeviceId = await UserSettings.getAsync('analyticsDeviceId', null);
const deviceId = savedDeviceId ?? uuidv4();
if (!savedDeviceId) {
await UserSettings.setAsync('analyticsDeviceId', deviceId);
}

identifyData = {
userId,
deviceId,
traits,
};

ensureIdentified();
}

export function logEvent(name: string, properties: Record<string, any> = {}): void {
EvanBacon marked this conversation as resolved.
Show resolved Hide resolved
if (EXPO_NO_TELEMETRY) {
return;
}
ensureIdentified();

const { userId, deviceId } = identifyData ?? {};
const commonEventProperties = { source_version: process.env.__EXPO_VERSION, source: 'expo' };

const identity = { userId: userId ?? undefined, anonymousId: deviceId ?? uuidv4() };
getClient().track({
event: name,
EvanBacon marked this conversation as resolved.
Show resolved Hide resolved
properties: { ...properties, ...commonEventProperties },
...identity,
context: getContext(),
});
}

function ensureIdentified(): void {
if (EXPO_NO_TELEMETRY || identified || !identifyData) {
return;
}

getClient().identify({
userId: identifyData.userId,
anonymousId: identifyData.deviceId,
traits: identifyData.traits,
});
identified = true;
}

function getContext(): Record<string, any> {
const platform = PLATFORM_TO_ANALYTICS_PLATFORM[os.platform()] || os.platform();
return {
os: { name: platform, version: os.release() },
device: { type: platform, model: platform },
app: { name: 'expo', version: process.env.__EXPO_VERSION },
};
}