mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(client): exam token UI (#55687)
Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
"first-lesson": "Go to the first lesson",
|
||||
"close": "Close",
|
||||
"edit": "Edit",
|
||||
"copy": "Copy",
|
||||
"view": "View",
|
||||
"view-code": "View Code",
|
||||
"view-project": "View Project",
|
||||
@@ -1111,6 +1112,16 @@
|
||||
"no-thanks": "No thanks, I would like to keep my token",
|
||||
"yes-please": "Yes please, I would like to delete my token"
|
||||
},
|
||||
"exam-token": {
|
||||
"exam-token": "Exam Token",
|
||||
"note": "Your exam token is a secret key that allows you to access exams. Do not share this token with anyone.",
|
||||
"invalidation": "If you generate a new token, your old token will be invalidated.",
|
||||
"generate-exam-token": "Generate Exam Token",
|
||||
"error": "There was an error generating your token, please try again in a moment.",
|
||||
"your-exam-token": "Your Exam Token is: {{token}}",
|
||||
"copied": "Token copied to clipboard",
|
||||
"copy-error": "Error copying token to clipboard"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Keyboard shortcuts",
|
||||
"table-header-action": "Action",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { Callout, Container } from '@freecodecamp/ui';
|
||||
import { useFeatureIsOn } from '@growthbook/growthbook-react';
|
||||
|
||||
import store from 'store';
|
||||
import envData from '../../config/env.json';
|
||||
@@ -18,6 +19,7 @@ import Honesty from '../components/settings/honesty';
|
||||
import Privacy from '../components/settings/privacy';
|
||||
import { type ThemeProps, Themes } from '../components/settings/theme';
|
||||
import UserToken from '../components/settings/user-token';
|
||||
import ExamToken from '../components/settings/exam-token';
|
||||
import { hardGoTo as navigate } from '../redux/actions';
|
||||
import {
|
||||
signInLoadingSelector,
|
||||
@@ -35,6 +37,7 @@ import {
|
||||
updateMyKeyboardShortcuts,
|
||||
verifyCert
|
||||
} from '../redux/settings/actions';
|
||||
|
||||
const { apiLocation } = envData;
|
||||
|
||||
// TODO: update types for actions
|
||||
@@ -126,6 +129,8 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
|
||||
} = props;
|
||||
const isSignedInRef = useRef(isSignedIn);
|
||||
|
||||
const examTokenFlag = useFeatureIsOn('exam-token-widget');
|
||||
|
||||
if (showLoading) {
|
||||
return <Loader fullScreen={true} />;
|
||||
}
|
||||
@@ -172,6 +177,7 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
|
||||
<Spacer size='medium' />
|
||||
<Honesty isHonest={isHonest} updateIsHonest={updateIsHonest} />
|
||||
<Spacer size='medium' />
|
||||
{examTokenFlag && <ExamToken />}
|
||||
<Certification
|
||||
completedChallenges={completedChallenges}
|
||||
createFlashMessage={createFlashMessage}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Panel, Modal, Spacer } from '@freecodecamp/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FullWidthRow } from '../helpers';
|
||||
import { generateExamToken } from '../../utils/ajax';
|
||||
|
||||
function ExamToken(): JSX.Element {
|
||||
const [examToken, setExamToken] = useState<string | null>(null);
|
||||
const [examTokenError, setExamTokenError] = useState<string | null>(null);
|
||||
|
||||
const [recentlyGenerated, setRecentlyGenerated] = useState(false);
|
||||
|
||||
const [copySuccess, setCopySuccess] = useState<string | null>(null);
|
||||
const [copyError, setCopyError] = useState<string | null>(null);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getToken = async () => {
|
||||
try {
|
||||
const response = await generateExamToken();
|
||||
|
||||
const {
|
||||
data: {
|
||||
data: { examEnvironmentAuthorizationToken }
|
||||
}
|
||||
} = response;
|
||||
setExamToken(examEnvironmentAuthorizationToken);
|
||||
setExamTokenError('');
|
||||
} catch (e) {
|
||||
setExamTokenError(t('exam-token.error'));
|
||||
}
|
||||
|
||||
setRecentlyGenerated(true);
|
||||
setTimeout(() => setRecentlyGenerated(false), 10000);
|
||||
};
|
||||
|
||||
return (
|
||||
<FullWidthRow>
|
||||
<Modal
|
||||
open={!!examToken}
|
||||
onClose={() => {
|
||||
setExamToken(null);
|
||||
setCopySuccess(null);
|
||||
setCopyError(null);
|
||||
}}
|
||||
>
|
||||
<Modal.Header>{t('exam-token.exam-token')}</Modal.Header>
|
||||
<Modal.Body>
|
||||
{examToken && (
|
||||
<p style={{ wordBreak: 'break-word' }}>
|
||||
{t('exam-token.your-exam-token', {
|
||||
token: examToken
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
{examTokenError && <p style={{ color: 'red' }}>{examTokenError}</p>}
|
||||
{copySuccess && <p style={{ color: 'green' }}>{copySuccess}</p>}
|
||||
{copyError && <p style={{ color: 'red' }}>{copyError}</p>}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(examToken ?? '').then(
|
||||
() => {
|
||||
setCopySuccess(t('exam-token.copied'));
|
||||
setCopyError(null);
|
||||
},
|
||||
() => {
|
||||
setCopyError(t('exam-token.copy-error'));
|
||||
setCopySuccess(null);
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t('buttons.copy')}
|
||||
</Button>
|
||||
<Spacer size='s' />
|
||||
<Button
|
||||
onClick={() => {
|
||||
setExamToken(null);
|
||||
setCopySuccess(null);
|
||||
setCopyError(null);
|
||||
}}
|
||||
>
|
||||
{t('buttons.close')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
<Panel variant='info' id='exam-environment-authorization-token'>
|
||||
<Panel.Heading>{t('exam-token.exam-token')}</Panel.Heading>
|
||||
<Panel.Body>
|
||||
<p>{t('exam-token.note')}</p>
|
||||
<strong>{t('exam-token.invalidation')}</strong>
|
||||
<Spacer size='s' />
|
||||
<Button
|
||||
block={true}
|
||||
disabled={recentlyGenerated}
|
||||
onClick={() => void getToken()}
|
||||
>
|
||||
{t('exam-token.generate-exam-token')}
|
||||
</Button>
|
||||
</Panel.Body>
|
||||
</Panel>
|
||||
</FullWidthRow>
|
||||
);
|
||||
}
|
||||
|
||||
ExamToken.displayName = 'ExamToken';
|
||||
|
||||
export default ExamToken;
|
||||
@@ -469,6 +469,11 @@ export interface GenerateExamResponseWithData {
|
||||
data: GenerateExamResponse;
|
||||
}
|
||||
|
||||
export interface ExamTokenResponse {
|
||||
data: {
|
||||
examEnvironmentAuthorizationToken: string;
|
||||
};
|
||||
}
|
||||
// User Exam (null until they answer the question)
|
||||
interface UserExamAnswer {
|
||||
id: string | null;
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
ChallengeFile,
|
||||
ChallengeFiles,
|
||||
CompletedChallenge,
|
||||
ExamTokenResponse,
|
||||
GenerateExamResponseWithData,
|
||||
SavedChallenge,
|
||||
SavedChallengeFile,
|
||||
@@ -258,6 +259,12 @@ export function postChargeStripeCard(
|
||||
return post('/donate/charge-stripe-card', body);
|
||||
}
|
||||
|
||||
export function generateExamToken(): Promise<
|
||||
ResponseWithData<ExamTokenResponse>
|
||||
> {
|
||||
return post('/user/exam-environment/token', {});
|
||||
}
|
||||
|
||||
type PaymentIntentResponse = Promise<
|
||||
ResponseWithData<
|
||||
| {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// TO ENABLE THESE TESTS GROWTHBOOK HAS TO BE SET IN THE ENVIRONMENT VARIABLES`
|
||||
// UNCOMMENT WHEN NEW API IS READY.
|
||||
|
||||
// import { test, expect } from '@playwright/test';
|
||||
|
||||
// test.use({ storageState: 'playwright/.auth/certified-user.json' });
|
||||
|
||||
// test.beforeEach(async ({ page }) => {
|
||||
// await page.goto('/settings');
|
||||
// });
|
||||
|
||||
// test.describe('Exam Token Widget', () => {
|
||||
// test('should tell you to not share the token with anyone', async ({
|
||||
// page
|
||||
// }) => {
|
||||
// await expect(
|
||||
// page.getByText(
|
||||
// 'Your exam token is a secret key that allows you to access the exam. Do not share this token with anyone'
|
||||
// )
|
||||
// ).toBeVisible();
|
||||
// await expect(
|
||||
// page.getByText(
|
||||
// 'If you generate a new token, your old token will be invalidated.'
|
||||
// )
|
||||
// ).toBeVisible();
|
||||
|
||||
// await expect(
|
||||
// page.getByRole('button', { name: 'Generate Exam Token' })
|
||||
// ).toBeVisible();
|
||||
// });
|
||||
|
||||
// test('should be able to generate a token', async ({ page }) => {
|
||||
// await page.getByRole('button', { name: 'Generate Exam Token' }).click();
|
||||
// await expect(page.getByText('Your Exam Token is:')).toBeVisible();
|
||||
|
||||
// await expect(page.getByRole('button', { name: 'Copy' })).toBeVisible();
|
||||
|
||||
// await expect(
|
||||
// page.getByRole('button', { name: 'Close' }).nth(1)
|
||||
// ).toBeVisible();
|
||||
// });
|
||||
// });
|
||||
Reference in New Issue
Block a user