diff --git a/client/src/assets/icons/Heart.js b/client/src/assets/icons/Heart.js
new file mode 100644
index 0000000000..a2706d8d6e
--- /dev/null
+++ b/client/src/assets/icons/Heart.js
@@ -0,0 +1,38 @@
+import React, { Fragment } from 'react';
+
+const propTypes = {};
+
+function Heart(props) {
+ return (
+
+ Heart
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+Heart.displayName = 'Heart';
+Heart.propTypes = propTypes;
+
+export default Heart;
diff --git a/client/src/client-only-routes/ShowCertification.js b/client/src/client-only-routes/ShowCertification.js
index 6455f5df8a..41267ef498 100644
--- a/client/src/client-only-routes/ShowCertification.js
+++ b/client/src/client-only-routes/ShowCertification.js
@@ -8,7 +8,12 @@ import { Grid, Row, Col, Image } from '@freecodecamp/react-bootstrap';
import {
showCertSelector,
showCertFetchStateSelector,
- showCert
+ showCert,
+ userFetchStateSelector,
+ usernameSelector,
+ isDonatingSelector,
+ isDonationRequestedSelector,
+ preventDonationRequests
} from '../redux';
import validCertNames from '../../utils/validCertNames';
import { createFlashMessage } from '../components/Flash/redux';
@@ -16,7 +21,7 @@ import standardErrorMessage from '../utils/standardErrorMessage';
import reallyWeirdErrorMessage from '../utils/reallyWeirdErrorMessage';
import RedirectHome from '../components/RedirectHome';
-import { Loader } from '../components/helpers';
+import { Loader, Link } from '../components/helpers';
const propTypes = {
cert: PropTypes.shape({
@@ -35,8 +40,15 @@ const propTypes = {
complete: PropTypes.bool,
errored: PropTypes.bool
}),
+ isDonating: PropTypes.bool,
+ isDonationRequested: PropTypes.bool,
issueDate: PropTypes.string,
+ preventDonationRequests: PropTypes.func,
showCert: PropTypes.func.isRequired,
+ signedInUserName: PropTypes.string,
+ userFetchState: PropTypes.shape({
+ complete: PropTypes.bool
+ }),
userFullName: PropTypes.string,
username: PropTypes.string,
validCertName: PropTypes.bool
@@ -47,16 +59,34 @@ const mapStateToProps = (state, { certName }) => {
return createSelector(
showCertSelector,
showCertFetchStateSelector,
- (cert, fetchState) => ({
+ usernameSelector,
+ userFetchStateSelector,
+ isDonatingSelector,
+ isDonationRequestedSelector,
+ (
cert,
fetchState,
- validCertName
+ signedInUserName,
+ userFetchState,
+ isDonating,
+ isDonationRequested
+ ) => ({
+ cert,
+ fetchState,
+ validCertName,
+ signedInUserName,
+ userFetchState,
+ isDonating,
+ isDonationRequested
})
);
};
const mapDispatchToProps = dispatch =>
- bindActionCreators({ createFlashMessage, showCert }, dispatch);
+ bindActionCreators(
+ { createFlashMessage, showCert, preventDonationRequests },
+ dispatch
+ );
class ShowCertification extends Component {
componentDidMount() {
@@ -72,7 +102,12 @@ class ShowCertification extends Component {
fetchState,
validCertName,
createFlashMessage,
- certName
+ certName,
+ preventDonationRequests,
+ signedInUserName,
+ isDonating,
+ isDonationRequested,
+ userFetchState
} = this.props;
if (!validCertName) {
@@ -81,6 +116,7 @@ class ShowCertification extends Component {
}
const { pending, complete, errored } = fetchState;
+ const { complete: userComplete } = userFetchState;
if (pending) {
return ;
@@ -103,8 +139,40 @@ class ShowCertification extends Component {
certTitle,
completionTime
} = cert;
+
+ let conditionalDonationMessage = '';
+
+ if (
+ userComplete &&
+ signedInUserName === username &&
+ !isDonating &&
+ !isDonationRequested
+ ) {
+ conditionalDonationMessage = (
+
+
+
+ Only you can see this message. Congratulations on earning this
+ certification. It’s no easy task. Running freeCodeCamp isn’t easy
+ either. Nor is it cheap. Help us help you and many other people
+ around the world. Make a tax-deductible supporting donation to our
+ nonprofit today.
+
+
+ Check out our donation dashboard
+
+
+
+ );
+ }
+
return (
+ {conditionalDonationMessage}
diff --git a/client/src/components/Donation/Donation.css b/client/src/components/Donation/Donation.css
index 09e14ef9fb..f557552acc 100644
--- a/client/src/components/Donation/Donation.css
+++ b/client/src/components/Donation/Donation.css
@@ -162,3 +162,59 @@ li.disabled > a {
.servicebot-embed-panel .panel {
padding: 20px;
}
+
+.heart-icon-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ margin: 40px;
+}
+
+.heart-icon {
+ width: 150px;
+ height: auto;
+ transform: scale(1.5);
+ opacity: 0;
+ animation: heart-icon-animation 1s linear 100ms forwards;
+}
+
+@keyframes heart-icon-animation {
+ 33% {
+ transform: scale(1.2);
+ }
+ 66% {
+ transform: scale(1.25);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+.donation-modal p {
+ margin: 0;
+ text-align: center;
+ font-weight: 700;
+ font-size: 1.2rem;
+}
+
+.donation-modal .modal-title {
+ text-align: center;
+ font-weight: 700;
+ font-size: 1.5rem;
+}
+
+@media screen and (max-width: 991px) {
+ .heart-icon-container {
+ margin: 30px;
+ }
+ .donation-modal p {
+ font-weight: 400;
+ font-size: 1rem;
+ }
+ .donation-modal .modal-title {
+ font-weight: 600;
+ font-size: 1.2rem;
+ }
+}
diff --git a/client/src/components/Donation/components/DonationModal.js b/client/src/components/Donation/components/DonationModal.js
new file mode 100644
index 0000000000..400063ed93
--- /dev/null
+++ b/client/src/components/Donation/components/DonationModal.js
@@ -0,0 +1,98 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { Modal, Button } from '@freecodecamp/react-bootstrap';
+import { Link } from '../../../components/helpers';
+import { blockNameify } from '../../../../utils/blockNameify';
+import Heart from '../../../assets/icons/Heart';
+
+import ga from '../../../analytics';
+import {
+ closeDonationModal,
+ isDonationModalOpenSelector
+} from '../../../redux';
+
+import { challengeMetaSelector } from '../../../templates/Challenges/redux';
+
+import '../Donation.css';
+
+const mapStateToProps = createSelector(
+ isDonationModalOpenSelector,
+ challengeMetaSelector,
+ (show, { block }) => ({
+ show,
+ block
+ })
+);
+
+const mapDispatchToProps = dispatch =>
+ bindActionCreators(
+ {
+ closeDonationModal
+ },
+ dispatch
+ );
+
+const propTypes = {
+ block: PropTypes.string,
+ closeDonationModal: PropTypes.func.isRequired,
+ show: PropTypes.bool
+};
+
+class DonateModal extends Component {
+ render() {
+ const { show, block } = this.props;
+ if (show) {
+ ga.modalview('/donation-modal');
+ }
+
+ return (
+
+
+
+ Support freeCodeCamp.org
+
+
+
+
+ Nicely done. You just completed {blockNameify(block)}.
+
+
+
+
+
+ Help us create even more learning resources like this.
+
+
+
+
+ Support our nonprofit
+
+
+ Ask me later
+
+
+
+ );
+ }
+}
+
+DonateModal.displayName = 'DonateModal';
+DonateModal.propTypes = propTypes;
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(DonateModal);
diff --git a/client/src/components/layouts/Certification.js b/client/src/components/layouts/Certification.js
index 720c2ad6ac..fc113724d9 100644
--- a/client/src/components/layouts/Certification.js
+++ b/client/src/components/layouts/Certification.js
@@ -1,11 +1,42 @@
-import React, { Fragment } from 'react';
+import React, { Fragment, Component } from 'react';
import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
-function CertificationLayout({ children }) {
- return {children} ;
+import ga from '../../analytics';
+import { fetchUser, isSignedInSelector } from '../../redux';
+import { createSelector } from 'reselect';
+
+const mapStateToProps = createSelector(
+ isSignedInSelector,
+ isSignedIn => ({
+ isSignedIn
+ })
+);
+
+const mapDispatchToProps = { fetchUser };
+
+class CertificationLayout extends Component {
+ componentDidMount() {
+ const { isSignedIn, fetchUser, pathname } = this.props;
+ if (!isSignedIn) {
+ fetchUser();
+ }
+ ga.pageview(pathname);
+ }
+ render() {
+ return {this.props.children} ;
+ }
}
CertificationLayout.displayName = 'CertificationLayout';
-CertificationLayout.propTypes = { children: PropTypes.any };
+CertificationLayout.propTypes = {
+ children: PropTypes.any,
+ fetchUser: PropTypes.func.isRequired,
+ isSignedIn: PropTypes.bool,
+ pathname: PropTypes.string.isRequired
+};
-export default CertificationLayout;
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(CertificationLayout);
diff --git a/client/src/components/layouts/Learn.js b/client/src/components/layouts/Learn.js
index 89f86c8695..945d3172e0 100644
--- a/client/src/components/layouts/Learn.js
+++ b/client/src/components/layouts/Learn.js
@@ -1,4 +1,4 @@
-import React, { Fragment } from 'react';
+import React, { Fragment, Component } from 'react';
import PropTypes from 'prop-types';
import { createSelector } from 'reselect';
import { connect } from 'react-redux';
@@ -7,9 +7,11 @@ import { Loader } from '../../components/helpers';
import {
userSelector,
userFetchStateSelector,
- isSignedInSelector
+ isSignedInSelector,
+ tryToShowDonationModal
} from '../../redux';
import createRedirect from '../../components/createRedirect';
+import DonateModal from '../Donation/components/DonationModal';
import 'prismjs/themes/prism.css';
import './prism.css';
@@ -28,27 +30,40 @@ const mapStateToProps = createSelector(
})
);
+const mapDispatchToProps = {
+ tryToShowDonationModal
+};
+
const RedirectAcceptPrivacyTerm = createRedirect('/accept-privacy-terms');
-function LearnLayout({
- fetchState: { pending, complete },
- isSignedIn,
- user: { acceptedPrivacyTerms },
- children
-}) {
- if (pending && !complete) {
- return ;
+class LearnLayout extends Component {
+ componentDidMount() {
+ this.props.tryToShowDonationModal();
}
- if (isSignedIn && !acceptedPrivacyTerms) {
- return ;
- }
+ render() {
+ const {
+ fetchState: { pending, complete },
+ isSignedIn,
+ user: { acceptedPrivacyTerms },
+ children
+ } = this.props;
- return (
-
- {children}
-
- );
+ if (pending && !complete) {
+ return ;
+ }
+
+ if (isSignedIn && !acceptedPrivacyTerms) {
+ return ;
+ }
+
+ return (
+
+ {children}
+
+
+ );
+ }
}
LearnLayout.displayName = 'LearnLayout';
@@ -60,9 +75,13 @@ LearnLayout.propTypes = {
errored: PropTypes.bool
}),
isSignedIn: PropTypes.bool,
+ tryToShowDonationModal: PropTypes.func.isRequired,
user: PropTypes.shape({
acceptedPrivacyTerms: PropTypes.bool
})
};
-export default connect(mapStateToProps)(LearnLayout);
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(LearnLayout);
diff --git a/client/src/components/layouts/global.css b/client/src/components/layouts/global.css
index a809ad24c6..d4d8970b76 100644
--- a/client/src/components/layouts/global.css
+++ b/client/src/components/layouts/global.css
@@ -208,18 +208,19 @@ fieldset[disabled] .btn-primary.focus {
}
.btn-cta {
- background-color: #ffac33;
- background-image: linear-gradient(#ffcc4d, #ffac33);
- border-color: #f1a02a;
+ background-color: #feac32;
+ background-image: linear-gradient(#fecc4c, #ffac33);
+ border-width: 3px;
+ border-color: #feac32;
color: #0a0a23 !important;
- text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
- border: none;
}
.btn-cta:hover,
.btn-cta:focus,
.btn-cta:active:hover {
- background-color: #e99110;
- background-image: linear-gradient(#ffcc4d, #e99110);
+ background-color: #fecc4c;
+ border-width: 3px;
+ border-color: #f1a02a;
+ background-image: none;
color: #0a0a23 !important;
}
.btn-cta:active {
diff --git a/client/src/components/layouts/variables.css b/client/src/components/layouts/variables.css
index 5ee0bb4211..529c6dfe99 100644
--- a/client/src/components/layouts/variables.css
+++ b/client/src/components/layouts/variables.css
@@ -19,6 +19,8 @@
--green-dark: #00471b;
--red-light: #ffadad;
--red-dark: #850000;
+ --love-light: #f8577c;
+ --love-dark: #f82153;
}
.dark-palette {
@@ -36,6 +38,7 @@
--success-background: var(--green-dark);
--danger-color: var(--red-light);
--danger-background: var(--red-dark);
+ --love-color: var(--love-light);
}
.light-palette {
@@ -53,4 +56,5 @@
--success-background: var(--green-light);
--danger-color: var(--red-dark);
--danger-background: var(--red-light);
+ --love-color: var(--love-dark);
}
diff --git a/client/src/pages/certification.css b/client/src/pages/certification.css
index 5f4c3fab52..351fcb3b98 100644
--- a/client/src/pages/certification.css
+++ b/client/src/pages/certification.css
@@ -35,6 +35,23 @@
display: flex;
align-items: center;
min-height: 100vh;
+ flex-direction: column;
+}
+
+.certificate-outer-wrapper .certification-donation {
+ padding: 15px 0px;
+}
+
+.certificate-outer-wrapper .certification-donation .btn {
+ background-color: var(--gray-15);
+ border-color: var(--gray-85);
+ color: var(--gray-85);
+}
+
+.certificate-outer-wrapper .certification-donation .btn:hover {
+ border-color: var(--gray-85);
+ background-color: var(--gray-85);
+ color: var(--gray-05);
}
.certification-namespace header {
diff --git a/client/src/redux/donation-saga.js b/client/src/redux/donation-saga.js
new file mode 100644
index 0000000000..f486c5e56d
--- /dev/null
+++ b/client/src/redux/donation-saga.js
@@ -0,0 +1,20 @@
+import { put, select, takeEvery, delay } from 'redux-saga/effects';
+
+import {
+ openDonationModal,
+ shouldRequestDonationSelector,
+ preventDonationRequests
+} from './';
+
+function* showDonateModalSaga() {
+ let shouldRequestDonation = yield select(shouldRequestDonationSelector);
+ if (shouldRequestDonation) {
+ yield delay(200);
+ yield put(openDonationModal());
+ yield put(preventDonationRequests());
+ }
+}
+
+export function createDonationSaga(types) {
+ return [takeEvery(types.tryToShowDonationModal, showDonateModalSaga)];
+}
diff --git a/client/src/redux/index.js b/client/src/redux/index.js
index 07349644f7..ed21b69934 100644
--- a/client/src/redux/index.js
+++ b/client/src/redux/index.js
@@ -10,6 +10,7 @@ import { createAppMountSaga } from './app-mount-saga';
import { createReportUserSaga } from './report-user-saga';
import { createShowCertSaga } from './show-cert-saga';
import { createNightModeSaga } from './night-mode-saga';
+import { createDonationSaga } from './donation-saga';
import hardGoToEpic from './hard-go-to-epic';
import failedUpdatesEpic from './failed-updates-epic';
@@ -31,6 +32,7 @@ export const defaultFetchState = {
const initialState = {
appUsername: '',
+ canRequestDonation: false,
completionCount: 0,
currentChallengeId: store.get(CURRENT_CHALLENGE_KEY),
donationRequested: false,
@@ -53,12 +55,14 @@ const initialState = {
export const types = createTypes(
[
'appMount',
- 'closeDonationModal',
- 'donationRequested',
'hardGoTo',
+ 'allowDonationRequests',
+ 'closeDonationModal',
+ 'preventDonationRequests',
'openDonationModal',
'onlineStatusChange',
'resetUserData',
+ 'tryToShowDonationModal',
'submitComplete',
'updateComplete',
'updateCurrentChallengeId',
@@ -77,6 +81,7 @@ export const epics = [hardGoToEpic, failedUpdatesEpic, updateCompleteEpic];
export const sagas = [
...createAcceptTermsSaga(types),
...createAppMountSaga(types),
+ ...createDonationSaga(types),
...createFetchUserSaga(types),
...createShowCertSaga(types),
...createReportUserSaga(types),
@@ -85,9 +90,15 @@ export const sagas = [
export const appMount = createAction(types.appMount);
+export const tryToShowDonationModal = createAction(
+ types.tryToShowDonationModal
+);
+export const allowDonationRequests = createAction(types.allowDonationRequests);
export const closeDonationModal = createAction(types.closeDonationModal);
export const openDonationModal = createAction(types.openDonationModal);
-export const donationRequested = createAction(types.donationRequested);
+export const preventDonationRequests = createAction(
+ types.preventDonationRequests
+);
export const onlineStatusChange = createAction(types.onlineStatusChange);
@@ -134,7 +145,8 @@ export const completedChallengesSelector = state =>
userSelector(state).completedChallenges || [];
export const completionCountSelector = state => state[ns].completionCount;
export const currentChallengeIdSelector = state => state[ns].currentChallengeId;
-export const donationRequestedSelector = state => state[ns].donationRequested;
+export const isDonationRequestedSelector = state => state[ns].donationRequested;
+export const isDonatingSelector = state => userSelector(state).isDonating;
export const isOnlineSelector = state => state[ns].isOnline;
export const isSignedInSelector = state => !!state[ns].appUsername;
@@ -145,21 +157,14 @@ export const signInLoadingSelector = state =>
export const showCertSelector = state => state[ns].showCert;
export const showCertFetchStateSelector = state => state[ns].showCertFetchState;
-export const showDonationSelector = state => {
- const completedChallenges = completedChallengesSelector(state);
- const completionCount = completionCountSelector(state);
- const currentCompletedLength = completedChallenges.length;
- const donationRequested = donationRequestedSelector(state);
- // the user has not completed 9 challenges in total yet
- if (currentCompletedLength < 9) {
- return false;
- }
- // this will mean we are on the 10th submission in total for the user
- if (completedChallenges.length === 9 && donationRequested === false) {
- return true;
- }
- // this will mean we are on the 3rd submission for this browser session
- if (completionCount === 2 && donationRequested === false) {
+export const shouldRequestDonationSelector = state => {
+ const isDonationRequested = isDonationRequestedSelector(state);
+ const isDonating = isDonatingSelector(state);
+ if (
+ isDonationRequested === false &&
+ isDonating === false &&
+ state[ns].canRequestDonation
+ ) {
return true;
}
return false;
@@ -209,6 +214,10 @@ function spreadThePayloadOnUser(state, payload) {
export const reducer = handleActions(
{
+ [types.allowDonationRequests]: state => ({
+ ...state,
+ canRequestDonation: true
+ }),
[types.fetchUser]: state => ({
...state,
userFetchState: { ...defaultFetchState }
@@ -288,7 +297,7 @@ export const reducer = handleActions(
...state,
showDonationModal: true
}),
- [types.donationRequested]: state => ({
+ [types.preventDonationRequests]: state => ({
...state,
donationRequested: true
}),
diff --git a/client/src/templates/Challenges/components/CompletionModal.js b/client/src/templates/Challenges/components/CompletionModal.js
index 91d77ff5f4..e94684fb5a 100644
--- a/client/src/templates/Challenges/components/CompletionModal.js
+++ b/client/src/templates/Challenges/components/CompletionModal.js
@@ -21,7 +21,8 @@ import {
isCompletionModalOpenSelector,
successMessageSelector,
challengeFilesSelector,
- challengeMetaSelector
+ challengeMetaSelector,
+ lastBlockChalSubmitted
} from '../redux';
import { isSignedInSelector } from '../../../redux';
@@ -54,17 +55,11 @@ const mapStateToProps = createSelector(
const mapDispatchToProps = function(dispatch) {
const dispatchers = {
close: () => dispatch(closeModal('completion')),
- handleKeypress: e => {
- if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
- e.preventDefault();
- // Since Hotkeys also listens to Ctrl + Enter we have to stop this event
- // getting to it.
- e.stopPropagation();
- dispatch(submitChallenge());
- }
- },
submitChallenge: () => {
dispatch(submitChallenge());
+ },
+ lastBlockChalSubmitted: () => {
+ dispatch(lastBlockChalSubmitted());
}
};
return () => dispatchers;
@@ -76,18 +71,18 @@ const propTypes = {
completedChallengesIds: PropTypes.array,
currentBlockIds: PropTypes.array,
files: PropTypes.object.isRequired,
- handleKeypress: PropTypes.func.isRequired,
id: PropTypes.string,
isOpen: PropTypes.bool,
isSignedIn: PropTypes.bool.isRequired,
+ lastBlockChalSubmitted: PropTypes.func,
message: PropTypes.string,
submitChallenge: PropTypes.func.isRequired,
title: PropTypes.string
};
export function getCompletedPercent(
- completedChallengesIds,
- currentBlockIds,
+ completedChallengesIds = [],
+ currentBlockIds = [],
currentChallengeId
) {
completedChallengesIds = completedChallengesIds.includes(currentChallengeId)
@@ -106,8 +101,15 @@ export function getCompletedPercent(
}
export class CompletionModalInner extends Component {
+ constructor(props) {
+ super(props);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleKeypress = this.handleKeypress.bind(this);
+ }
+
state = {
- downloadURL: null
+ downloadURL: null,
+ completedPercent: 0
};
static getDerivedStateFromProps(props, state) {
@@ -135,7 +137,37 @@ export class CompletionModalInner extends Component {
});
newURL = URL.createObjectURL(blob);
}
- return { downloadURL: newURL };
+
+ const { completedChallengesIds, currentBlockIds, id, isSignedIn } = props;
+ let completedPercent = isSignedIn
+ ? getCompletedPercent(completedChallengesIds, currentBlockIds, id)
+ : 0;
+ return { downloadURL: newURL, completedPercent: completedPercent };
+ }
+
+ handleKeypress(e) {
+ if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
+ e.preventDefault();
+ // Since Hotkeys also listens to Ctrl + Enter we have to stop this event
+ // getting to it.
+ e.stopPropagation();
+ this.handleSubmit();
+ }
+ }
+
+ handleSubmit() {
+ this.props.submitChallenge();
+ this.checkBlockCompletion();
+ }
+
+ // check block completion for donation
+ checkBlockCompletion() {
+ if (
+ this.state.completedPercent === 100 &&
+ !this.props.completedChallengesIds.includes(this.props.id)
+ ) {
+ this.props.lastBlockChalSubmitted();
+ }
}
componentWillUnmount() {
@@ -149,20 +181,13 @@ export class CompletionModalInner extends Component {
const {
blockName = '',
close,
- completedChallengesIds = [],
- currentBlockIds = [],
- id = '',
isOpen,
- isSignedIn,
- submitChallenge,
- handleKeypress,
message,
- title
+ title,
+ isSignedIn
} = this.props;
- const completedPercent = !isSignedIn
- ? 0
- : getCompletedPercent(completedChallengesIds, currentBlockIds, id);
+ const { completedPercent } = this.state;
if (isOpen) {
ga.modalview('/completion-modal');
@@ -175,7 +200,7 @@ export class CompletionModalInner extends Component {
dialogClassName='challenge-success-modal'
keyboard={true}
onHide={close}
- onKeyDown={isOpen ? handleKeypress : noop}
+ onKeyDown={isOpen ? this.handleKeypress : noop}
show={isOpen}
>
{isSignedIn ? 'Submit and g' : 'G'}o to next challenge{' '}
(Ctrl + Enter)
diff --git a/client/src/templates/Challenges/redux/completion-epic.js b/client/src/templates/Challenges/redux/completion-epic.js
index 0c9ee9dada..40822eebd7 100644
--- a/client/src/templates/Challenges/redux/completion-epic.js
+++ b/client/src/templates/Challenges/redux/completion-epic.js
@@ -132,6 +132,7 @@ export default function completionEpic(action$, state$) {
const meta = challengeMetaSelector(state);
const { nextChallengePath, introPath, challengeType } = meta;
const closeChallengeModal = of(closeModal('completion'));
+
let submitter = () => of({ type: 'no-user-signed-in' });
if (
!(challengeType in submitTypes) ||
diff --git a/client/src/templates/Challenges/redux/current-challenge-saga.js b/client/src/templates/Challenges/redux/current-challenge-saga.js
index 685cad5191..92cbeb35a3 100644
--- a/client/src/templates/Challenges/redux/current-challenge-saga.js
+++ b/client/src/templates/Challenges/redux/current-challenge-saga.js
@@ -4,7 +4,8 @@ import store from 'store';
import {
isSignedInSelector,
updateComplete,
- updateFailed
+ updateFailed,
+ allowDonationRequests
} from '../../../redux';
import { post } from '../../../utils/ajax';
@@ -33,13 +34,18 @@ export function* currentChallengeSaga({ payload: id }) {
}
}
-function* updateSuccessMessageSaga() {
+export function* updateSuccessMessageSaga() {
yield put(updateSuccessMessage(randomCompliment()));
}
+export function* allowDonationRequestsSaga() {
+ yield put(allowDonationRequests());
+}
+
export function createCurrentChallengeSaga(types) {
return [
takeEvery(types.challengeMounted, currentChallengeSaga),
- takeEvery(types.challengeMounted, updateSuccessMessageSaga)
+ takeEvery(types.challengeMounted, updateSuccessMessageSaga),
+ takeEvery(types.lastBlockChalSubmitted, allowDonationRequestsSaga)
];
}
diff --git a/client/src/templates/Challenges/redux/current-challenge-saga.test.js b/client/src/templates/Challenges/redux/current-challenge-saga.test.js
new file mode 100644
index 0000000000..0e4d415ee1
--- /dev/null
+++ b/client/src/templates/Challenges/redux/current-challenge-saga.test.js
@@ -0,0 +1,12 @@
+/* global expect */
+import { allowDonationRequestsSaga } from './current-challenge-saga';
+import { types as appTypes } from '../../../redux';
+
+describe('allowDonationRequestsSaga', () => {
+ it('should call allowDonationRequests', () => {
+ const gen = allowDonationRequestsSaga();
+ expect(gen.next().value.payload.action.type).toEqual(
+ appTypes.allowDonationRequests
+ );
+ });
+});
diff --git a/client/src/templates/Challenges/redux/index.js b/client/src/templates/Challenges/redux/index.js
index 7c12537257..00d7381dbc 100644
--- a/client/src/templates/Challenges/redux/index.js
+++ b/client/src/templates/Challenges/redux/index.js
@@ -30,6 +30,7 @@ const initialState = {
},
challengeTests: [],
consoleOut: '',
+ hasCompletedBlock: false,
inAccessibilityMode: false,
isCodeLocked: false,
isBuildEnabled: true,
@@ -81,7 +82,9 @@ export const types = createTypes(
'moveToTab',
'setEditorFocusability',
- 'setAccessibilityMode'
+ 'setAccessibilityMode',
+
+ 'lastBlockChalSubmitted'
],
ns
);
@@ -157,6 +160,10 @@ export const moveToTab = createAction(types.moveToTab);
export const setEditorFocusability = createAction(types.setEditorFocusability);
export const setAccessibilityMode = createAction(types.setAccessibilityMode);
+export const lastBlockChalSubmitted = createAction(
+ types.lastBlockChalSubmitted
+);
+
export const currentTabSelector = state => state[ns].currentTab;
export const challengeFilesSelector = state => state[ns].challengeFiles;
export const challengeMetaSelector = state => state[ns].challengeMeta;
diff --git a/client/utils/gatsby/layoutSelector.js b/client/utils/gatsby/layoutSelector.js
index 455d11c227..91b98f3814 100644
--- a/client/utils/gatsby/layoutSelector.js
+++ b/client/utils/gatsby/layoutSelector.js
@@ -15,7 +15,9 @@ export default function layoutSelector({ element, props }) {
return {element} ;
}
if (/^\/certification(\/.*)*/.test(pathname)) {
- return {element} ;
+ return (
+ {element}
+ );
}
if (/^\/guide(\/.*)*/.test(pathname)) {
console.log('Hitting guide for some reason. Need a redirect.');