Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 43 additions & 2 deletions src/__tests__/tui/auth-effects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,20 @@
getPath: vi.fn(() => '/tmp/.leetcode/credentials.v2.enc.json'),
},
mockLeetCodeClient: {
setSite: vi.fn(),
setCredentials: vi.fn(),
checkAuth: vi.fn(),
},
}));

const { mockConfig } = vi.hoisted(() => ({
mockConfig: {
setSite: vi.fn(),
getSite: vi.fn(() => 'leetcode.com'),
getConfig: vi.fn(() => ({ site: 'leetcode.com' })),
},
}));

vi.mock('../../storage/credentials.js', () => ({
credentials: mockCredentials,
describeCredentialStatus: vi.fn((status: { reason: string | null }) => {
Expand All @@ -60,6 +69,10 @@
leetcodeClient: mockLeetCodeClient,
}));

vi.mock('../../storage/config.js', () => ({
config: mockConfig,
}));

import { executeCommand } from '../../tui/commands/effects.js';

async function flushAsync(): Promise<void> {
Expand All @@ -80,11 +93,11 @@
readOnly: true,
reason: null,
path: null,
} as any);

Check warning on line 96 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 20)

Unexpected any. Specify a different type

Check warning on line 96 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 22)

Unexpected any. Specify a different type

Check warning on line 96 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 24)

Unexpected any. Specify a different type

Check warning on line 96 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 24)

Unexpected any. Specify a different type

Check warning on line 96 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 20)

Unexpected any. Specify a different type

Check warning on line 96 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 22)

Unexpected any. Specify a different type

Check warning on line 96 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 24)

Unexpected any. Specify a different type

Check warning on line 96 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 20)

Unexpected any. Specify a different type

Check warning on line 96 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 22)

Unexpected any. Specify a different type

const dispatched: AppMsg[] = [];
executeCommand(
{ type: 'CMD_LOGIN', session: 'session-token', csrf: 'csrf-token' },
{ type: 'CMD_LOGIN', session: 'session-token', csrf: 'csrf-token', site: 'leetcode.com' },
(msg) => dispatched.push(msg)
);

Expand All @@ -105,11 +118,11 @@
readOnly: false,
reason: 'KEYCHAIN_UNAVAILABLE',
path: null,
} as any);

Check warning on line 121 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 20)

Unexpected any. Specify a different type

Check warning on line 121 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 22)

Unexpected any. Specify a different type

Check warning on line 121 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 24)

Unexpected any. Specify a different type

Check warning on line 121 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 24)

Unexpected any. Specify a different type

Check warning on line 121 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 20)

Unexpected any. Specify a different type

Check warning on line 121 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 22)

Unexpected any. Specify a different type

Check warning on line 121 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 24)

Unexpected any. Specify a different type

Check warning on line 121 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 20)

Unexpected any. Specify a different type

Check warning on line 121 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 22)

Unexpected any. Specify a different type

const dispatched: AppMsg[] = [];
executeCommand(
{ type: 'CMD_LOGIN', session: 'session-token', csrf: 'csrf-token' },
{ type: 'CMD_LOGIN', session: 'session-token', csrf: 'csrf-token', site: 'leetcode.com' },
(msg) => dispatched.push(msg)
);

Expand All @@ -132,7 +145,7 @@
source: null,
path: null,
message: 'Unset LEETCODE_SESSION and LEETCODE_CSRF_TOKEN to log out.',
} as any);

Check warning on line 148 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 20)

Unexpected any. Specify a different type

Check warning on line 148 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 22)

Unexpected any. Specify a different type

Check warning on line 148 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 24)

Unexpected any. Specify a different type

Check warning on line 148 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 24)

Unexpected any. Specify a different type

Check warning on line 148 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 20)

Unexpected any. Specify a different type

Check warning on line 148 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 22)

Unexpected any. Specify a different type

Check warning on line 148 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 24)

Unexpected any. Specify a different type

Check warning on line 148 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 20)

Unexpected any. Specify a different type

Check warning on line 148 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 22)

Unexpected any. Specify a different type

const dispatched: AppMsg[] = [];
executeCommand({ type: 'CMD_LOGOUT' }, (msg) => dispatched.push(msg));
Expand All @@ -155,7 +168,7 @@
readOnly: false,
reason: 'KEYCHAIN_UNAVAILABLE',
path: null,
} as any);

Check warning on line 171 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 20)

Unexpected any. Specify a different type

Check warning on line 171 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 22)

Unexpected any. Specify a different type

Check warning on line 171 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 24)

Unexpected any. Specify a different type

Check warning on line 171 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 24)

Unexpected any. Specify a different type

Check warning on line 171 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 20)

Unexpected any. Specify a different type

Check warning on line 171 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 22)

Unexpected any. Specify a different type

Check warning on line 171 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 24)

Unexpected any. Specify a different type

Check warning on line 171 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 20)

Unexpected any. Specify a different type

Check warning on line 171 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 22)

Unexpected any. Specify a different type

const dispatched: AppMsg[] = [];
executeCommand({ type: 'CMD_CHECK_AUTH' }, (msg) => dispatched.push(msg));
Expand All @@ -165,4 +178,32 @@
expect(dispatched).toContainEqual({ type: 'GLOBAL_ERROR', error: 'System keychain is unavailable.' });
expect(dispatched).toContainEqual({ type: 'AUTH_CHECK_COMPLETE', user: null });
});

it('should persist selected site before verifying login credentials', async () => {
mockCredentials.status.mockResolvedValueOnce({
mode: 'keychain',
backend: 'keychain',
source: null,
hasCredentials: false,
readOnly: false,
reason: null,
path: null,
} as any);

Check warning on line 191 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 20)

Unexpected any. Specify a different type

Check warning on line 191 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 22)

Unexpected any. Specify a different type

Check warning on line 191 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 24)

Unexpected any. Specify a different type

Check warning on line 191 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 24)

Unexpected any. Specify a different type

Check warning on line 191 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 20)

Unexpected any. Specify a different type

Check warning on line 191 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 22)

Unexpected any. Specify a different type

Check warning on line 191 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 24)

Unexpected any. Specify a different type

Check warning on line 191 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 20)

Unexpected any. Specify a different type

Check warning on line 191 in src/__tests__/tui/auth-effects.test.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 22)

Unexpected any. Specify a different type
mockLeetCodeClient.checkAuth.mockResolvedValueOnce({
isSignedIn: true,
username: 'dong',
});

const dispatched: AppMsg[] = [];
executeCommand(
{ type: 'CMD_LOGIN', session: 'session-token', csrf: 'csrf-token', site: 'leetcode.cn' },
(msg) => dispatched.push(msg)
);

await flushAsync();

expect(mockConfig.setSite).toHaveBeenCalledWith('leetcode.cn');
expect(mockLeetCodeClient.setSite).toHaveBeenCalledWith('leetcode.cn');
expect(dispatched).toContainEqual({ type: 'LOGIN_SUCCESS', username: 'dong' });
});
});
44 changes: 44 additions & 0 deletions src/__tests__/tui/login-screen.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, it, vi } from 'vitest';

const { mockConfig } = vi.hoisted(() => ({
mockConfig: {
getSite: vi.fn(() => 'leetcode.com'),
getConfig: vi.fn(() => ({ site: 'leetcode.com' })),
},
}));

vi.mock('../../storage/config.js', () => ({
config: mockConfig,
}));

import { init, update } from '../../tui/screens/login/index.js';

describe('TUI login screen', () => {
it('should move from instructions to site selection before credential entry', () => {
const [initialModel] = init();

const [siteModel] = update({ type: 'LOGIN_SUBMIT' }, initialModel);
const [inputModel] = update({ type: 'LOGIN_SUBMIT' }, siteModel);

expect(siteModel.step).toBe('site');
expect(inputModel.step).toBe('input');
expect(inputModel.site).toBe('leetcode.com');
});

it('should include selected site in the login command payload', () => {
const [initialModel] = init();
const [siteModel] = update({ type: 'LOGIN_SUBMIT' }, initialModel);
const [cnModel] = update({ type: 'LOGIN_SWITCH_SITE' }, siteModel);
const [inputModel] = update({ type: 'LOGIN_SUBMIT' }, cnModel);
const [sessionModel] = update({ type: 'LOGIN_SESSION_INPUT', value: 'session-token' }, inputModel);
const [readyModel] = update({ type: 'LOGIN_CSRF_INPUT', value: 'csrf-token' }, sessionModel);
const [, cmd] = update({ type: 'LOGIN_SUBMIT' }, readyModel);

expect(cmd).toEqual({
type: 'CMD_LOGIN',
session: 'session-token',
csrf: 'csrf-token',
site: 'leetcode.cn',
});
});
});
12 changes: 10 additions & 2 deletions src/tui/commands/effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
return;

case 'CMD_LOGIN':
login(cmd.session, cmd.csrf, dispatch);
login(cmd.session, cmd.csrf, cmd.site, dispatch);
return;

default:
Expand Down Expand Up @@ -244,7 +244,7 @@
function saveConfig(key: string, value: string): void {
switch (key) {
case 'language':
config.setLanguage(value as any);

Check warning on line 247 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 20)

Unexpected any. Specify a different type

Check warning on line 247 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 22)

Unexpected any. Specify a different type

Check warning on line 247 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 24)

Unexpected any. Specify a different type

Check warning on line 247 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 24)

Unexpected any. Specify a different type

Check warning on line 247 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 20)

Unexpected any. Specify a different type

Check warning on line 247 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 22)

Unexpected any. Specify a different type

Check warning on line 247 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 24)

Unexpected any. Specify a different type

Check warning on line 247 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 20)

Unexpected any. Specify a different type

Check warning on line 247 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 22)

Unexpected any. Specify a different type
break;
case 'site': {
const site = normalizeLeetCodeSiteInput(value);
Expand Down Expand Up @@ -287,7 +287,7 @@
try {
const code = await fs.readFile(filePath, 'utf-8');
return { code, lang: leetcodeLang, questionId: problem.questionId };
} catch (e) {

Check warning on line 290 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 20)

'e' is defined but never used

Check warning on line 290 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 22)

'e' is defined but never used

Check warning on line 290 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 24)

'e' is defined but never used

Check warning on line 290 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 24)

'e' is defined but never used

Check warning on line 290 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 20)

'e' is defined but never used

Check warning on line 290 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 22)

'e' is defined but never used

Check warning on line 290 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 24)

'e' is defined but never used

Check warning on line 290 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 20)

'e' is defined but never used

Check warning on line 290 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 22)

'e' is defined but never used
const rootPath = path.join(workDir, fileName);
try {
const code = await fs.readFile(rootPath, 'utf-8');
Expand Down Expand Up @@ -340,7 +340,7 @@

try {
await fs.mkdir(targetDir, { recursive: true });
} catch (e) {}

Check warning on line 343 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 20)

'e' is defined but never used

Check warning on line 343 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 22)

'e' is defined but never used

Check warning on line 343 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest, 24)

'e' is defined but never used

Check warning on line 343 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 24)

'e' is defined but never used

Check warning on line 343 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 20)

'e' is defined but never used

Check warning on line 343 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 22)

'e' is defined but never used

Check warning on line 343 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 24)

'e' is defined but never used

Check warning on line 343 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 20)

'e' is defined but never used

Check warning on line 343 in src/tui/commands/effects.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest, 22)

'e' is defined but never used

const fileName = getSolutionFileName(problem.questionFrontendId, problem.titleSlug, language);
const filePath = path.join(targetDir, fileName);
Expand Down Expand Up @@ -554,7 +554,12 @@
dispatch({ type: 'AUTH_CHECK_COMPLETE', user: null });
}

async function login(session: string, csrf: string, dispatch: Dispatch): Promise<void> {
async function login(
session: string,
csrf: string,
site: import('../../types.js').LeetCodeSite,
dispatch: Dispatch
): Promise<void> {
const credentialStatus = await credentials.status();
if (credentialStatus.mode === 'env-readonly') {
dispatch({
Expand All @@ -573,6 +578,9 @@
return;
}

config.setSite(site);
configureLeetCodeClientSite(site);

const creds = { session, csrfToken: csrf };
leetcodeClient.setCredentials(creds);

Expand Down
31 changes: 26 additions & 5 deletions src/tui/screens/login/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { AppModel, Cmd, Command, LoginScreenModel, LoginMsg } from '../../types.js';
import { leetcodeClient } from '../../../api/client.js';
import { credentials } from '../../../storage/credentials.js';
import { Cmd, Command, LoginScreenModel, LoginMsg } from '../../types.js';
import { config } from '../../../storage/config.js';
import { DEFAULT_LEETCODE_SITE, normalizeLeetCodeSiteInput } from '../../../utils/site.js';

export { view } from './view.js';

function getConfiguredSite() {
return normalizeLeetCodeSiteInput(config.getSite?.() ?? config.getConfig?.().site ?? '') ?? DEFAULT_LEETCODE_SITE;
}

export function init(): [LoginScreenModel, Command] {
const model: LoginScreenModel = {
step: 'instructions',
site: getConfiguredSite(),
sessionToken: '',
csrfToken: '',
focusedField: 'session',
Expand All @@ -17,6 +22,16 @@ export function init(): [LoginScreenModel, Command] {

export function update(msg: LoginMsg, model: LoginScreenModel): [LoginScreenModel, Command] {
switch (msg.type) {
case 'LOGIN_SWITCH_SITE':
return [
{
...model,
site: model.site === 'leetcode.cn' ? 'leetcode.com' : 'leetcode.cn',
error: null,
},
Cmd.none(),
];

case 'LOGIN_SESSION_INPUT':
return [{ ...model, sessionToken: msg.value }, Cmd.none()];

Expand All @@ -33,18 +48,24 @@ export function update(msg: LoginMsg, model: LoginScreenModel): [LoginScreenMode
return [{ ...model, focusedField: msg.field }, Cmd.none()];

case 'LOGIN_BACK':
if (model.step === 'input' || model.step === 'error' || model.step === 'verifying') {
return [{ ...model, step: 'site', focusedField: 'session', error: null }, Cmd.none()];
}
return [{ ...model, step: 'instructions', focusedField: 'session', error: null }, Cmd.none()];

case 'LOGIN_SUBMIT':
if (model.step === 'instructions') {
return [{ ...model, step: 'input', focusedField: 'session' }, Cmd.none()];
return [{ ...model, step: 'site', focusedField: 'session', error: null }, Cmd.none()];
}
if (model.step === 'site') {
return [{ ...model, step: 'input', focusedField: 'session', error: null }, Cmd.none()];
}
if (!model.sessionToken || !model.csrfToken) {
return [{ ...model, error: 'Both fields are required' }, Cmd.none()];
}
return [
{ ...model, step: 'verifying', error: null },
Cmd.login(model.sessionToken, model.csrfToken),
Cmd.login(model.sessionToken, model.csrfToken, model.site),
];

case 'LOGIN_SUCCESS':
Expand Down
38 changes: 30 additions & 8 deletions src/tui/screens/login/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,9 @@ export function view(model: LoginScreenModel, width: number, height: number): st
const instructions = [
chalk.hex(colors.warning).bold('How to Login:'),
'',
'1. Open https://leetcode.com in your browser',
'2. Login to your account',
'3. Open DevTools (F12) → Application → Cookies → leetcode.com',
'4. Copy the values of ' +
chalk.bold.cyan('LEETCODE_SESSION') +
' and ' +
chalk.bold.cyan('csrftoken'),
'1. Continue to choose your LeetCode site',
'2. We will show the correct cookie domain on the next screen',
'3. Copy the values of ' + chalk.bold.cyan('LEETCODE_SESSION') + ' and ' + chalk.bold.cyan('csrftoken'),
'',
'Default storage: system keychain.',
'Use LEETCODECLI_CREDENTIAL_BACKEND=file + LEETCODECLI_MASTER_KEY for encrypted file mode.',
Expand All @@ -54,10 +50,34 @@ export function view(model: LoginScreenModel, width: number, height: number): st

boxed.forEach((l) => contentLines.push(center(l, width)));
contentLines.push(''); // Spacing
contentLines.push(center(keyHint('Enter', 'Continue to Login'), width));
contentLines.push(center(keyHint('Enter', 'Choose Site'), width));
} else if (model.step === 'site') {
const contentWidth = Math.max(28, Math.min(100, width - 4));
const options = [
model.site === 'leetcode.com'
? chalk.hex(colors.primary)('> leetcode.com (Global)')
: ' leetcode.com (Global)',
model.site === 'leetcode.cn'
? chalk.hex(colors.primary)('> leetcode.cn (China)')
: ' leetcode.cn (China)',
'',
chalk.gray('Use Up/Down/Tab to switch sites.'),
];

const boxed = box(options, contentWidth, {
title: 'Choose Site',
borderColor: colors.primary,
padding: 1,
borderStyle: 'round',
});

boxed.forEach((l) => contentLines.push(center(l, width)));
contentLines.push('');
contentLines.push(center(keyHint('Enter', 'Continue') + ' ' + keyHint('Esc', 'Back'), width));
} else if (model.step === 'input' || model.step === 'verifying' || model.step === 'error') {
const contentWidth = Math.max(28, Math.min(100, width - 4));
const boxLines = [];
const domain = model.site === 'leetcode.cn' ? 'leetcode.cn' : 'leetcode.com';

const isSessionFocused = model.focusedField === 'session';
const isCsrfFocused = model.focusedField === 'csrf';
Expand Down Expand Up @@ -95,6 +115,8 @@ export function view(model: LoginScreenModel, width: number, height: number): st
: ' ' + csrfVal + (model.csrfToken ? chalk.green(' ✔') : '')
);

boxLines.push('');
boxLines.push(chalk.gray(`Cookie source: https://${domain} → DevTools → Application → Cookies → ${domain}`));
boxLines.push('');
if (model.error) {
boxLines.push(center(chalk.red(model.error), contentWidth - 4));
Expand Down
14 changes: 11 additions & 3 deletions src/tui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
ProblemListFilters as RootProblemListFilters,
DailyChallenge,
} from '../types.js';
import type { LeetCodeSite } from '../types.js';

export type Problem = RootProblem;
export type ProblemDetail = RootProblemDetail;
Expand Down Expand Up @@ -133,7 +134,8 @@ export interface HelpScreenModel {
}

export interface LoginScreenModel {
readonly step: 'instructions' | 'input' | 'verifying' | 'success' | 'error';
readonly step: 'instructions' | 'site' | 'input' | 'verifying' | 'success' | 'error';
readonly site: LeetCodeSite;
readonly sessionToken: string;
readonly csrfToken: string;
readonly focusedField: 'session' | 'csrf';
Expand Down Expand Up @@ -356,6 +358,7 @@ export type HelpMsg =
export type LoginMsg =
| { readonly type: 'LOGIN_SESSION_INPUT'; readonly value: string }
| { readonly type: 'LOGIN_CSRF_INPUT'; readonly value: string }
| { readonly type: 'LOGIN_SWITCH_SITE' }
| { readonly type: 'LOGIN_SUBMIT' }
| { readonly type: 'LOGIN_BACK' }
| { readonly type: 'LOGIN_SWITCH_FOCUS' }
Expand Down Expand Up @@ -438,7 +441,7 @@ export type Command =
| { readonly type: 'CMD_SWITCH_WORKSPACE'; readonly name: string }
| { readonly type: 'CMD_FETCH_CHANGELOG' }
| { readonly type: 'CMD_LOGOUT' }
| { readonly type: 'CMD_LOGIN'; readonly session: string; readonly csrf: string };
| { readonly type: 'CMD_LOGIN'; readonly session: string; readonly csrf: string; readonly site: LeetCodeSite };

export const Cmd = {
none: (): Command => ({ type: 'CMD_NONE' }),
Expand Down Expand Up @@ -482,7 +485,12 @@ export const Cmd = {
switchWorkspace: (name: string): Command => ({ type: 'CMD_SWITCH_WORKSPACE', name }),
fetchChangelog: (): Command => ({ type: 'CMD_FETCH_CHANGELOG' }),
logout: (): Command => ({ type: 'CMD_LOGOUT' }),
login: (session: string, csrf: string): Command => ({ type: 'CMD_LOGIN', session, csrf }),
login: (session: string, csrf: string, site: LeetCodeSite): Command => ({
type: 'CMD_LOGIN',
session,
csrf,
site,
}),
};

export function createInitialModel(username?: string): AppModel {
Expand Down
16 changes: 9 additions & 7 deletions src/tui/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,7 @@ export function update(msg: AppMsg, model: AppModel): [AppModel, Command] {
user: null,
screenState: {
screen: 'login',
model: {
step: 'instructions',
sessionToken: '',
csrfToken: '',
focusedField: 'session',
error: null,
},
model: LoginScreen.init()[0],
},
needsRender: true,
},
Expand Down Expand Up @@ -973,6 +967,14 @@ function handleLoginKeyPress(model: AppModel, msg: AppMsg): [AppModel, Command]
} else if (key.name === 'escape') {
return [model, Cmd.exit()];
}
} else if (loginModel.step === 'site') {
if (key.name === 'escape') {
return updateLogin(model, { type: 'LOGIN_BACK' });
} else if (key.name === 'tab' || key.name === 'down' || key.name === 'up' || key.name === 'left' || key.name === 'right') {
return updateLogin(model, { type: 'LOGIN_SWITCH_SITE' });
} else if (key.name === 'return' || key.name === 'enter') {
return updateLogin(model, { type: 'LOGIN_SUBMIT' });
}
} else if (loginModel.step === 'input' || loginModel.step === 'error') {
if (key.name === 'escape') {
return updateLogin(model, { type: 'LOGIN_BACK' });
Expand Down
Loading