chore: remove verify-can-claim-cert logic (#44574)

* chore: remove verify-can-claim-cert logic

* remove extraneous

* remove console log before Nich wakes up

* add api route back with flash

* remove unnecessary logic in completion-epic

* change tests for new layout

* dynamically use api location

* rename file

* fix Cypress api location

* fix(test): anchor does not have disabled class

* fix(tests): change js test to claim from /settings

* chore: change status to 410 (gone)

* update testing again

* oliver is nitpicky

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>

* make oliver happy

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Shaun Hamilton
2022-01-18 16:52:49 +02:00
committed by GitHub
parent 9672c92a19
commit 9cb87d0257
12 changed files with 72 additions and 491 deletions

View File

@@ -63,7 +63,8 @@
"submit-and-go": "Submit and go to next challenge",
"go-to-next": "Go to next challenge",
"ask-later": "Ask me later",
"start-coding": "Start coding!"
"start-coding": "Start coding!",
"go-to-settings": "Go to settings to claim your certification"
},
"landing": {
"big-heading-1": "Learn to code — for free.",

View File

@@ -189,19 +189,7 @@ export const completedChallengesSelector = state =>
export const completionCountSelector = state => state[MainApp].completionCount;
export const currentChallengeIdSelector = state =>
state[MainApp].currentChallengeId;
export const stepsToClaimSelector = state => {
const user = userSelector(state);
const currentCerts = certificatesByNameSelector(user.username)(
state
).currentCerts;
return {
currentCerts: currentCerts,
isHonest: user?.isHonest,
isShowName: user?.profileUI?.showName,
isShowCerts: user?.profileUI?.showCerts,
isShowProfile: !user?.profileUI?.isLocked
};
};
export const emailSelector = state => userSelector(state).email;
export const isAVariantSelector = state => {
const email = emailSelector(state);
@@ -271,6 +259,9 @@ export const userByNameSelector = username => state => {
return user[username] ?? initialState.user;
};
export const currentCertsSelector = state =>
certificatesByNameSelector(state[MainApp]?.appUsername)(state)?.currentCerts;
export const certificatesByNameSelector = username => state => {
const {
isRespWebDesignCert,

View File

@@ -17,11 +17,9 @@ import {
isSignedInSelector,
submitComplete,
updateComplete,
updateFailed,
usernameSelector
updateFailed
} from '../../../redux';
import { getVerifyCanClaimCert } from '../../../utils/ajax';
import postUpdate$ from '../utils/postUpdate$';
import { actionTypes } from './action-types';
import {
@@ -151,8 +149,7 @@ export default function completionEpic(action$, state$) {
switchMap(({ type }) => {
const state = state$.value;
const meta = challengeMetaSelector(state);
const { nextChallengePath, challengeType, superBlock, certification } =
meta;
const { nextChallengePath, challengeType, superBlock } = meta;
const closeChallengeModal = of(closeModal('completion'));
let submitter = () => of({ type: 'no-user-signed-in' });
@@ -170,13 +167,7 @@ export default function completionEpic(action$, state$) {
}
const pathToNavigateTo = async () => {
return await findPathToNavigateTo(
certification,
nextChallengePath,
superBlock,
state,
challengeType
);
return await findPathToNavigateTo(nextChallengePath, superBlock);
};
return submitter(type, state).pipe(
@@ -188,38 +179,10 @@ export default function completionEpic(action$, state$) {
);
}
async function findPathToNavigateTo(
certification,
nextChallengePath,
superBlock,
state,
challengeType
) {
let canClaimCert = false;
const isProjectSubmission = [
challengeTypes.frontEndProject,
challengeTypes.backEndProject,
challengeTypes.pythonProject
].includes(challengeType);
if (isProjectSubmission) {
const username = usernameSelector(state);
try {
const response = await getVerifyCanClaimCert(username, certification);
if (response.status === 200) {
canClaimCert = response.data?.response?.message === 'can-claim-cert';
}
} catch (err) {
console.error('failed to verify if user can claim certificate', err);
}
}
let pathToNavigateTo;
if (nextChallengePath.includes(superBlock) && !canClaimCert) {
pathToNavigateTo = nextChallengePath;
} else if (canClaimCert) {
pathToNavigateTo = `/learn/${superBlock}/#claim-cert-block`;
async function findPathToNavigateTo(nextChallengePath, superBlock) {
if (nextChallengePath.includes(superBlock)) {
return nextChallengePath;
} else {
pathToNavigateTo = `/learn/${superBlock}/#${superBlock}-projects`;
return `/learn/${superBlock}/#${superBlock}-projects`;
}
return pathToNavigateTo;
}

View File

@@ -1,6 +1,6 @@
import { Button } from '@freecodecamp/react-bootstrap';
import { navigate } from 'gatsby-link';
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, MouseEvent } from 'react';
import { TFunction, withTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
@@ -13,14 +13,12 @@ import { createFlashMessage } from '../../../components/Flash/redux';
import { FlashMessages } from '../../../components/Flash/redux/flash-messages';
import {
userFetchStateSelector,
stepsToClaimSelector,
isSignedInSelector
isSignedInSelector,
currentCertsSelector
} from '../../../redux';
import { User, Steps } from '../../../redux/prop-types';
import { verifyCert } from '../../../redux/settings';
import { certMap } from '../../../resources/cert-and-project-map';
import { getVerifyCanClaimCert } from '../../../utils/ajax';
import CertificationCard from './certification-card';
interface CertChallengeProps {
// TODO: create enum/reuse SuperBlocks enum somehow
@@ -33,7 +31,7 @@ interface CertChallengeProps {
error: null | string;
};
isSignedIn: boolean;
steps: Steps;
currentCerts: Steps['currentCerts'];
superBlock: SuperBlocks;
t: TFunction;
title: typeof certMap[number]['title'];
@@ -48,11 +46,16 @@ const honestyInfoMessage = {
const mapStateToProps = (state: unknown) => {
return createSelector(
stepsToClaimSelector,
currentCertsSelector,
userFetchStateSelector,
isSignedInSelector,
(steps, fetchState: CertChallengeProps['fetchState'], isSignedIn) => ({
steps,
(
currentCerts,
fetchState: CertChallengeProps['fetchState'],
isSignedIn
) => ({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
currentCerts,
fetchState,
isSignedIn
})
@@ -65,9 +68,8 @@ const mapDispatchToProps = {
};
const CertChallenge = ({
certification,
createFlashMessage,
steps = {},
currentCerts,
superBlock,
t,
verifyCert,
@@ -76,45 +78,8 @@ const CertChallenge = ({
isSignedIn,
user: { isHonest, username }
}: CertChallengeProps): JSX.Element => {
const [canClaimCert, setCanClaimCert] = useState(false);
const [certVerificationMessage, setCertVerificationMessage] = useState('');
const [isCertified, setIsCertified] = useState(false);
const [userLoaded, setUserLoaded] = useState(false);
const [verificationComplete, setVerificationComplete] = useState(false);
const [stepState, setStepState] = useState({
numberOfSteps: 0,
completedCount: 0
});
const [hasCompletedRequiredSteps, setHasCompletedRequiredSteps] =
useState(false);
const [isProjectsCompleted, setIsProjectsCompleted] = useState(false);
useEffect(() => {
if (username) {
void (async () => {
try {
const data = await getVerifyCanClaimCert(username, certification);
if (data?.message) {
setCanClaimCert(false);
createFlashMessage(data.message);
} else {
const { status, result } = data?.response?.message;
setCanClaimCert(status);
setCertVerificationMessage(result);
}
} catch (e) {
console.error(e);
createFlashMessage({
type: 'danger',
message: FlashMessages.ReallyWeird
});
} finally {
setVerificationComplete(true);
}
})();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [username]);
// @ts-expect-error Typescript is confused
const certSlug = certMap.find(x => x.title === title).certSlug;
@@ -133,40 +98,19 @@ const CertChallenge = ({
useEffect(() => {
setIsCertified(
steps?.currentCerts?.find(
currentCerts?.find(
(cert: { certSlug: string }) =>
certSlugTypeMapTyped[cert.certSlug] ===
superBlockCertTypeMapTyped[superBlock]
)?.show ?? false
);
const projectsCompleted =
canClaimCert || certVerificationMessage === 'projects-completed';
const projectsCompletedNumber = projectsCompleted ? 1 : 0;
const completedCount =
Object.values(steps).filter(
stepVal => typeof stepVal === 'boolean' && stepVal
).length + projectsCompletedNumber;
const numberOfSteps = Object.keys(steps).length;
setHasCompletedRequiredSteps(completedCount === numberOfSteps);
setStepState({ numberOfSteps, completedCount });
setIsProjectsCompleted(projectsCompleted);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [steps, canClaimCert, certVerificationMessage]);
}, [currentCerts]);
const certLocation = `/certification/${username}/${certSlug}`;
const i18nSuperBlock = t(`intro:${superBlock}.title`);
const i18nCertText = t(`intro:misc-text.certification`, {
cert: i18nSuperBlock
});
const showCertificationCard =
userLoaded &&
isSignedIn &&
(!isCertified || (!hasCompletedRequiredSteps && verificationComplete));
const createClickHandler =
(certSlug: string | undefined) => (e: { preventDefault: () => void }) => {
(certSlug: string | undefined) => (e: MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
if (isCertified) {
return navigate(certLocation);
@@ -177,33 +121,19 @@ const CertChallenge = ({
};
return (
<div className='block'>
{showCertificationCard && (
<CertificationCard
i18nCertText={i18nCertText}
isProjectsCompleted={isProjectsCompleted}
steps={steps}
stepState={stepState}
superBlock={superBlock}
/>
{isSignedIn && (
<Button
block={true}
bsStyle='primary'
className='cert-btn'
href={isCertified ? certLocation : `/settings#certification-settings`}
onClick={() => (isCertified ? createClickHandler(certSlug) : false)}
>
{isCertified && userLoaded
? t('buttons.show-cert')
: t('buttons.go-to-settings')}
</Button>
)}
<>
{isSignedIn && (
<Button
block={true}
bsStyle='primary'
className='cert-btn'
disabled={
!canClaimCert || (isCertified && !hasCompletedRequiredSteps)
}
href={certLocation}
onClick={createClickHandler(certSlug)}
>
{isCertified && userLoaded
? t('buttons.show-cert')
: t('buttons.claim-cert')}
</Button>
)}
</>
</div>
);
};

View File

@@ -1,92 +0,0 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import ScrollableAnchor from 'react-scrollable-anchor';
import { SuperBlocks } from '../../../../../config/certification-settings';
import Caret from '../../../assets/icons/caret';
import GreenNotCompleted from '../../../assets/icons/green-not-completed';
import GreenPass from '../../../assets/icons/green-pass';
import { Steps } from '../../../redux/prop-types';
import ClaimCertSteps from './claim-cert-steps';
interface CertificationCardProps {
i18nCertText: string;
isProjectsCompleted: boolean;
stepState: {
numberOfSteps: number;
completedCount: number;
};
steps: Steps;
superBlock: SuperBlocks;
}
const mapIconStyle = { height: '15px', marginRight: '10px', width: '15px' };
const CertificationCard = ({
isProjectsCompleted,
superBlock,
i18nCertText,
stepState: { completedCount, numberOfSteps },
steps
}: CertificationCardProps): JSX.Element => {
const { t } = useTranslation();
const [isExpanded, setIsExpanded] = useState(true);
const handleBlockClick = () => {
setIsExpanded(!isExpanded);
};
const {
expand: expandText,
collapse: collapseText
}: {
expand: string;
collapse: string;
} = t('intro:misc-text');
return (
<ScrollableAnchor id='claim-cert-block'>
<div className={`block ${isExpanded ? 'open' : ''}`}>
<div className='block-title-wrapper'>
<a className='block-link' href='#claim-cert-block'>
<h3 className='big-block-title'>
{t('certification-card.title')}
<span className='block-link-icon'>#</span>
</h3>
</a>
</div>
<div className='block-description'>
{t('certification-card.intro', { i18nCertText })}
</div>
<button
aria-expanded={isExpanded}
className='map-title'
onClick={handleBlockClick}
>
<Caret />
<h4 className='course-title'>
{`${isExpanded ? collapseText : expandText}`}
</h4>
<div className='map-title-completed course-title'>
{completedCount === numberOfSteps ? (
<GreenPass style={mapIconStyle} />
) : (
<GreenNotCompleted style={mapIconStyle} />
)}
<span className='map-completed-count'>{`${completedCount}/${numberOfSteps}`}</span>
</div>
</button>
{isExpanded && (
<ClaimCertSteps
i18nCertText={i18nCertText}
isProjectsCompleted={isProjectsCompleted}
steps={steps}
superBlock={superBlock}
/>
)}
</div>
</ScrollableAnchor>
);
};
CertificationCard.displayName = 'CertStatus';
export default CertificationCard;

View File

@@ -1,88 +0,0 @@
import { Link } from 'gatsby';
import React from 'react';
import { withTranslation, useTranslation } from 'react-i18next';
import { SuperBlocks } from '../../../../../config/certification-settings';
import GreenNotCompleted from '../../../assets/icons/green-not-completed';
import GreenPass from '../../../assets/icons/green-pass';
import { Steps } from '../../../redux/prop-types';
const mapIconStyle = { height: '15px', marginRight: '10px', width: '15px' };
interface ClaimCertStepsProps {
i18nCertText: string;
isProjectsCompleted: boolean;
steps: Steps;
superBlock: SuperBlocks;
}
const ClaimCertSteps = ({
isProjectsCompleted,
i18nCertText,
steps,
superBlock
}: ClaimCertStepsProps): JSX.Element => {
const { t } = useTranslation();
const renderCheckMark = (isCompleted: boolean) => {
return isCompleted ? (
<GreenPass style={mapIconStyle} />
) : (
<GreenNotCompleted style={mapIconStyle} />
);
};
const settingsLink = '/settings#privacy-settings';
const honestyPolicyAnchor = '/settings#honesty-policy';
const {
isHonest = false,
isShowName = false,
isShowCerts = false,
isShowProfile = false
} = steps;
return (
<ul className='map-challenges-ul' data-cy='claim-cert-steps'>
<li className='map-challenge-title map-challenge-wrap'>
<Link to={honestyPolicyAnchor}>
<span className='badge map-badge'>{renderCheckMark(isHonest)}</span>
{t('certification-card.accept-honesty')}
</Link>
</li>
<li className='map-challenge-title map-challenge-wrap'>
<a href={`#${superBlock}-projects`}>
<span className='badge map-badge'>
{renderCheckMark(isProjectsCompleted)}
</span>
{t('certification-card.complete-project', {
i18nCertText
})}
</a>
</li>
<li className='map-challenge-title map-challenge-wrap'>
<Link to={settingsLink}>
<span className='badge map-badge'>
{renderCheckMark(isShowProfile)}
</span>
{t('certification-card.set-profile-public')}
</Link>
</li>
<li className='map-challenge-title map-challenge-wrap'>
<Link to={settingsLink}>
<span className='badge map-badge'>
{renderCheckMark(isShowCerts)}
</span>
{t('certification-card.set-certs-public')}
</Link>
</li>
<li className='map-challenge-title map-challenge-wrap'>
<Link to={settingsLink}>
<span className='badge map-badge'>{renderCheckMark(isShowName)}</span>
{t('certification-card.set-name')}
</Link>
</li>
</ul>
);
};
ClaimCertSteps.displayName = 'ClaimCertSteps';
export default withTranslation()(ClaimCertSteps);

View File

@@ -1,10 +1,8 @@
import cookies from 'browser-cookies';
import envData from '../../../config/env.json';
import { FlashMessageArg } from '../components/Flash/redux';
import type {
ChallengeFile,
ClaimedCertifications,
CompletedChallenge,
User
} from '../redux/prop-types';
@@ -25,6 +23,8 @@ function getCSRFToken() {
return token ?? '';
}
// TODO: Might want to handle flash messages as close to the request as possible
// to make use of the Response object (message, status, etc)
async function get<T>(path: string): Promise<T> {
return fetch(`${base}${path}`, defaultOptions).then<T>(res => res.json());
}
@@ -160,31 +160,6 @@ export function getUsernameExists(username: string): Promise<boolean> {
return get(`/api/users/exists?username=${username}`);
}
export interface GetVerifyCanClaimCert {
response: {
type: string;
message: {
status: boolean;
result: string;
};
variables: {
name: string;
};
};
isCertMap: ClaimedCertifications;
completedChallenges: CompletedChallenge[];
message?: FlashMessageArg;
}
export function getVerifyCanClaimCert(
username: string,
certification: string
): Promise<GetVerifyCanClaimCert> {
return get(
`/certificate/verify-can-claim-cert?username=${username}&superBlock=${certification}`
);
}
/** POST **/
interface Donation {