Skip to content

Commit

Permalink
[cli] Added auth commands to expo (#16087)
Browse files Browse the repository at this point in the history
* Added auth commands to expo/expo

* Added api tests

Update otp.ts

Update actions.ts

* Update generated.ts

* Update index-test.ts

* Added module loading tests

drop amplitude

Update rudderstackClient.ts

* Apply suggestions from code review

Co-authored-by: James Ide <ide@users.noreply.github.com>
Co-authored-by: Cedric van Putten <me@bycedric.com>

* clean up

lazy load analytics

Update prompts.ts

Update actions.ts

* fix tests

* Update user.ts

* Fix up code

Update registerAsync.ts
Remove unused settings
Drop unused
Update otp.ts
Update whoamiAsync.ts

* Update api-test.ts

* Apply suggestions from code review

Co-authored-by: kgc00 <kirby@expo.io>

* Update rudderstackClient.ts

* Update rudderstackClient.ts

Co-authored-by: James Ide <ide@users.noreply.github.com>
Co-authored-by: Cedric van Putten <me@bycedric.com>
Co-authored-by: kgc00 <kirby@expo.io>
  • Loading branch information
4 people committed Feb 1, 2022
1 parent df0caa1 commit 08f1ae9
Show file tree
Hide file tree
Showing 34 changed files with 2,885 additions and 16 deletions.
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';
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 ? '24TKICqYKilXM480mA7ktgVDdea' : '24TKR7CQAaGgIrLTgu3Fp4OdOkI', // expo unified
'https://cdp.expo.dev/v1/batch',
{
flushInterval: 300,
}
);

// 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(event: 'action', properties: Record<string, any> = {}): void {
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,
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 },
};
}

0 comments on commit 08f1ae9

Please sign in to comment.