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

feat(client): improve SuperBlock cert claiming UX #41147

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
e4987c8
feat(client): improve SuperBlock cert claiming UX
Feb 17, 2021
054d03f
broken: add certCard foundation
Feb 24, 2021
137a516
broken: add TODO comments for scatter-brain
Feb 24, 2021
d6b3c98
restructure stepsToClaimSelector
Feb 26, 2021
4433d53
add api-server verifyCanClaimCert logic
Mar 4, 2021
e1b2fcf
temp: correct verifyCanClaim URL
Mar 6, 2021
182dd0b
rebase and resolve conflicts
Mar 7, 2021
c68a532
move GET logic to CertificationCard, remove console.logs
Mar 7, 2021
4c08e6a
add error handling, and navigation logic
Mar 8, 2021
ea7713f
correct verification logical flow
Mar 12, 2021
40925e6
rebase against upstream/main
Apr 24, 2021
8eff06e
fix completion-epic updates, fix cert verify
Apr 24, 2021
80c00fa
update widget to button, disable button unless verified
Apr 30, 2021
957d23f
working: refactor CertChallenge with hook state
Apr 30, 2021
9929adb
add StepsType
Apr 30, 2021
6880ce6
update Honesty snapshot
Apr 30, 2021
5db0115
add DonationModal to SuperBlockIntro
Apr 30, 2021
68ccdec
disable Claim Cert button unless also isHonest
May 6, 2021
ee64a2f
prevent warning when viewing cert
May 6, 2021
a497eb7
test: use navigate in Modal to return to hash
May 6, 2021
f735638
test: replace gatsby.navigate with reach/router.navigate
May 6, 2021
69ab8c5
rebase against main
ShaunSHamilton Jun 10, 2021
7d5eb7d
add propTypes
ShaunSHamilton Jun 10, 2021
792c210
Merge branch 'main' into feat/claiming-cert
ojeytonwilliams Jun 16, 2021
98f4254
fix: rename propTypes -> prop-types
ojeytonwilliams Jun 16, 2021
1a602ff
rebase against upstream/main
ShaunSHamilton Jul 5, 2021
b11eb9e
use react-scrollable-anchor to squash modal bug
ShaunSHamilton Jul 6, 2021
f7362de
update location parser type
ShaunSHamilton Jul 6, 2021
76f4dc6
open-source Oliver's suggestion
ShaunSHamilton Jul 6, 2021
24ad64f
rebase against upstream/main
ShaunSHamilton Jul 7, 2021
bf92b4a
rebase against upstream/main for cypress
ShaunSHamilton Jul 9, 2021
4966c0c
rebase two-point-oh
ShaunSHamilton Jul 9, 2021
28e74c2
fix superblock title
ShaunSHamilton Jul 9, 2021
48f29c0
add claim-cert-from-learn tests
ShaunSHamilton Jul 9, 2021
74f913f
use larger tests
ShaunSHamilton Jul 13, 2021
53ffb86
fix some cypress stuff
ShaunSHamilton Jul 13, 2021
be1c109
rebase to resolve conflict
ShaunSHamilton Jul 13, 2021
7480507
fix ShowCertification cypress test
ShaunSHamilton Jul 14, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
78 changes: 77 additions & 1 deletion api-server/src/server/boot/certificate.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {
certTypeTitleMap,
certTypeIdMap,
certIds,
oldDataVizId
oldDataVizId,
superBlockCertTypeMap
} from '../../../../config/certification-settings';

const {
Expand Down Expand Up @@ -49,9 +50,11 @@ export default function bootCertificate(app) {
const certTypeIds = createCertTypeIds(getChallenges());
const showCert = createShowCert(app);
const verifyCert = createVerifyCert(certTypeIds, app);
const verifyCanClaimCert = createVerifyCanClaim(certTypeIds, app);

api.put('/certificate/verify', ifNoUser401, ifNoSuperBlock404, verifyCert);
api.get('/certificate/showCert/:username/:certSlug', showCert);
api.get('/certificate/verify-can-claim-cert', verifyCanClaimCert);

app.use(api);
}
Expand Down Expand Up @@ -494,3 +497,76 @@ function createShowCert(app) {
}, next);
};
}

function createVerifyCanClaim(certTypeIds, app) {
const { User } = app.models;

function findUserByUsername$(username, fields) {
return observeQuery(User, 'findOne', {
where: { username },
fields
});
}
return function verifyCert(req, res, next) {
const { superBlock, username } = req.query;
log(superBlock);
let certType = superBlockCertTypeMap[superBlock];
log(certType);

return findUserByUsername$(username, {
isFrontEndCert: true,
isBackEndCert: true,
isFullStackCert: true,
isRespWebDesignCert: true,
isFrontEndLibsCert: true,
isJsAlgoDataStructCert: true,
isDataVisCert: true,
is2018DataVisCert: true,
isApisMicroservicesCert: true,
isInfosecQaCert: true,
isQaCertV7: true,
isInfosecCertV7: true,
isSciCompPyCertV7: true,
isDataAnalysisPyCertV7: true,
isMachineLearningPyCertV7: true,
username: true,
name: true,
isHonest: true,
completedChallenges: true
}).subscribe(user => {
return Observable.of(certTypeIds[certType])
.flatMap(challenge => {
const certName = certTypeTitleMap[certType];
const { tests = [] } = challenge;
const { isHonest, completedChallenges } = user;
const isProjectsCompleted = canClaim(tests, completedChallenges);
let result = 'incomplete-requirements';
let status = false;

if (isHonest && isProjectsCompleted) {
status = true;
result = 'requirements-met';
} else if (isProjectsCompleted) {
result = 'projects-completed';
} else if (isHonest) {
result = 'is-honest';
}
return Observable.just({
type: 'success',
message: { status, result },
variables: { name: certName }
});
})
.subscribe(message => {
return res.status(200).json({
response: message,
isCertMap: getUserIsCertMap(user),
// send back the completed challenges
// NOTE: we could just send back the latest challenge, but this
// ensures the challenges are synced.
completedChallenges: user.completedChallenges
ShaunSHamilton marked this conversation as resolved.
Show resolved Hide resolved
});
}, next);
});
};
}
2 changes: 1 addition & 1 deletion client/i18n/locales/english/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -584,4 +584,4 @@
"add-code-two": "Please leave the ``` line above and the ``` line below,",
"add-code-three": "because they allow your code to properly format in the post."
}
}
}
27 changes: 26 additions & 1 deletion client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"@types/loadable__component": "5.13.4",
"@types/lodash-es": "4.17.4",
"@types/prismjs": "1.16.6",
"@types/reach__router": "^1.3.8",
"@types/react-dom": "17.0.9",
"@types/react-helmet": "6.1.2",
"@types/react-instantsearch-dom": "6.10.2",
Expand All @@ -162,4 +163,4 @@
"webpack": "5.44.0",
"webpack-cli": "4.7.2"
}
}
}
21 changes: 20 additions & 1 deletion client/src/components/Donation/DonationModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import Cup from '../../assets/icons/cup';
import DonateForm from './DonateForm';
import { modalDefaultDonation } from '../../../../config/donation-settings';
import { useTranslation } from 'react-i18next';
import { goToAnchor } from 'react-scrollable-anchor';
import { isLocationSuperBlock } from '../../utils/path-parsers';

import {
closeDonationModal,
Expand Down Expand Up @@ -43,6 +45,10 @@ const propTypes = {
activeDonors: PropTypes.number,
closeDonationModal: PropTypes.func.isRequired,
executeGA: PropTypes.func,
location: PropTypes.shape({
hash: PropTypes.string,
pathname: PropTypes.string
}),
recentlyClaimedBlock: PropTypes.string,
show: PropTypes.bool
};
Expand All @@ -51,6 +57,7 @@ function DonateModal({
show,
closeDonationModal,
executeGA,
location,
recentlyClaimedBlock
}) {
const [closeLabel, setCloseLabel] = React.useState(false);
Expand Down Expand Up @@ -98,6 +105,13 @@ function DonateModal({
}
};

const handleModalHide = () => {
// If modal is open on a SuperBlock page
if (isLocationSuperBlock(location)) {
goToAnchor('claim-cert-block');
}
};

const blockDonationText = (
<div className=' text-center block-modal-text'>
<div className='donation-icon-container'>
Expand Down Expand Up @@ -131,7 +145,12 @@ function DonateModal({
);

return (
<Modal bsSize='lg' className='donation-modal' show={show}>
<Modal
bsSize='lg'
className='donation-modal'
onExited={handleModalHide}
show={show}
>
<Modal.Body>
{recentlyClaimedBlock ? blockDonationText : progressDonationText}
<Spacer />
Expand Down
13 changes: 13 additions & 0 deletions client/src/redux/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,19 @@ export const completedChallengesSelector = state =>
userSelector(state).completedChallenges || [];
export const completionCountSelector = state => state[ns].completionCount;
export const currentChallengeIdSelector = state => state[ns].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 isDonatingSelector = state => userSelector(state).isDonating;
export const isOnlineSelector = state => state[ns].isOnline;
export const isSignedInSelector = state => !!state[ns].appUsername;
Expand Down
7 changes: 7 additions & 0 deletions client/src/redux/prop-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ export const CurrentCertsType = PropTypes.arrayOf(
})
);

export const StepsType = PropTypes.shape({
currentCerts: CurrentCertsType,
isShowCerts: PropTypes.bool,
isShowName: PropTypes.bool,
isShowProfile: PropTypes.bool
});

// TYPESCRIPT TYPES

export type CurrentCertType = {
Expand Down
56 changes: 51 additions & 5 deletions client/src/templates/Challenges/redux/completion-epic.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
catchError,
concat,
filter,
tap
finalize
} from 'rxjs/operators';
import { ofType } from 'redux-observable';
import { navigate } from 'gatsby';
Expand All @@ -24,11 +24,13 @@ import {
isSignedInSelector,
submitComplete,
updateComplete,
updateFailed
updateFailed,
usernameSelector
} from '../../../redux';

import postUpdate$ from '../utils/postUpdate$';
import { challengeTypes, submitTypes } from '../../../../utils/challengeTypes';
import { getVerifyCanClaimCert } from '../../../utils/ajax';

function postChallenge(update, username) {
const saveChallenge = postUpdate$(update).pipe(
Expand Down Expand Up @@ -133,7 +135,7 @@ export default function completionEpic(action$, state$) {
switchMap(({ type }) => {
const state = state$.value;
const meta = challengeMetaSelector(state);
const { nextChallengePath, challengeType } = meta;
const { nextChallengePath, challengeType, superBlock } = meta;
const closeChallengeModal = of(closeModal('completion'));

let submitter = () => of({ type: 'no-user-signed-in' });
Expand All @@ -150,11 +152,55 @@ export default function completionEpic(action$, state$) {
submitter = submitters[submitTypes[challengeType]];
}

const pathToNavigateTo = async () => {
return await findPathToNavigateTo(
nextChallengePath,
superBlock,
state,
challengeType
);
};

return submitter(type, state).pipe(
tap(() => navigate(nextChallengePath)),
concat(closeChallengeModal),
filter(Boolean)
filter(Boolean),
finalize(async () => navigate(await pathToNavigateTo()))
);
})
);
}

async function findPathToNavigateTo(
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, superBlock);
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`;
} else {
pathToNavigateTo = `/learn/${superBlock}/#${superBlock}-projects`;
}
return pathToNavigateTo;
}