fix(client): block donation modal (#40899)

Co-authored-by: Kris Koishigawa <scissorsneedfoodtoo@gmail.com>
This commit is contained in:
Ahmad Abdolsaheb
2021-02-08 10:28:36 +03:00
committed by GitHub
parent b1209f5547
commit 3c7979692b
8 changed files with 92 additions and 74 deletions

View File

@ -6,7 +6,6 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { Modal, Button, Col, Row } from '@freecodecamp/react-bootstrap'; import { Modal, Button, Col, Row } from '@freecodecamp/react-bootstrap';
import { Spacer } from '../helpers'; import { Spacer } from '../helpers';
import { blockNameify } from '../../../../utils/block-nameify';
import Heart from '../../assets/icons/Heart'; import Heart from '../../assets/icons/Heart';
import Cup from '../../assets/icons/Cup'; import Cup from '../../assets/icons/Cup';
import DonateForm from './DonateForm'; import DonateForm from './DonateForm';
@ -16,22 +15,18 @@ import { useTranslation } from 'react-i18next';
import { import {
closeDonationModal, closeDonationModal,
isDonationModalOpenSelector, isDonationModalOpenSelector,
isBlockDonationModalSelector, recentlyClaimedBlockSelector,
executeGA executeGA
} from '../../redux'; } from '../../redux';
import { challengeMetaSelector } from '../../templates/Challenges/redux';
import './Donation.css'; import './Donation.css';
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
isDonationModalOpenSelector, isDonationModalOpenSelector,
challengeMetaSelector, recentlyClaimedBlockSelector,
isBlockDonationModalSelector, (show, recentlyClaimedBlock) => ({
(show, { block }, isBlockDonation) => ({
show, show,
block, recentlyClaimedBlock
isBlockDonation
}) })
); );
@ -46,19 +41,17 @@ const mapDispatchToProps = dispatch =>
const propTypes = { const propTypes = {
activeDonors: PropTypes.number, activeDonors: PropTypes.number,
block: PropTypes.string,
closeDonationModal: PropTypes.func.isRequired, closeDonationModal: PropTypes.func.isRequired,
executeGA: PropTypes.func, executeGA: PropTypes.func,
isBlockDonation: PropTypes.bool, recentlyClaimedBlock: PropTypes.string,
show: PropTypes.bool show: PropTypes.bool
}; };
function DonateModal({ function DonateModal({
show, show,
block,
isBlockDonation,
closeDonationModal, closeDonationModal,
executeGA executeGA,
recentlyClaimedBlock
}) { }) {
const [closeLabel, setCloseLabel] = React.useState(false); const [closeLabel, setCloseLabel] = React.useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
@ -87,13 +80,13 @@ function DonateModal({
data: { data: {
category: 'Donation View', category: 'Donation View',
action: `Displayed ${ action: `Displayed ${
isBlockDonation ? 'block' : 'progress' recentlyClaimedBlock ? 'block' : 'progress'
} donation modal`, } donation modal`,
nonInteraction: true nonInteraction: true
} }
}); });
} }
}, [show, isBlockDonation, executeGA]); }, [show, recentlyClaimedBlock, executeGA]);
const getDonationText = () => { const getDonationText = () => {
const donationDuration = modalDefaultDonation.donationDuration; const donationDuration = modalDefaultDonation.donationDuration;
@ -117,7 +110,7 @@ function DonateModal({
<Row> <Row>
{!closeLabel && ( {!closeLabel && (
<Col sm={10} smOffset={1} xs={12}> <Col sm={10} smOffset={1} xs={12}>
<b>{t('donate.nicely-done', { block: blockNameify(block) })}</b> <b>{t('donate.nicely-done', { block: recentlyClaimedBlock })}</b>
<br /> <br />
{getDonationText()} {getDonationText()}
</Col> </Col>
@ -144,7 +137,7 @@ function DonateModal({
return ( return (
<Modal bsSize='lg' className='donation-modal' show={show}> <Modal bsSize='lg' className='donation-modal' show={show}>
<Modal.Body> <Modal.Body>
{isBlockDonation ? blockDonationText : progressDonationText} {recentlyClaimedBlock ? blockDonationText : progressDonationText}
<Spacer /> <Spacer />
<DonateForm handleProcessing={handleProcessing} isMinimalForm={true} /> <DonateForm handleProcessing={handleProcessing} isMinimalForm={true} />
<Spacer /> <Spacer />

View File

@ -4,7 +4,8 @@ import {
takeEvery, takeEvery,
takeLeading, takeLeading,
delay, delay,
call call,
take
} from 'redux-saga/effects'; } from 'redux-saga/effects';
import { import {
@ -12,11 +13,12 @@ import {
preventBlockDonationRequests, preventBlockDonationRequests,
shouldRequestDonationSelector, shouldRequestDonationSelector,
preventProgressDonationRequests, preventProgressDonationRequests,
canRequestBlockDonationSelector, recentlyClaimedBlockSelector,
addDonationComplete, addDonationComplete,
addDonationError, addDonationError,
postChargeStripeComplete, postChargeStripeComplete,
postChargeStripeError postChargeStripeError,
types as appTypes
} from './'; } from './';
import { addDonation, postChargeStripe } from '../utils/ajax'; import { addDonation, postChargeStripe } from '../utils/ajax';
@ -27,9 +29,10 @@ function* showDonateModalSaga() {
let shouldRequestDonation = yield select(shouldRequestDonationSelector); let shouldRequestDonation = yield select(shouldRequestDonationSelector);
if (shouldRequestDonation) { if (shouldRequestDonation) {
yield delay(200); yield delay(200);
const isBlockDonation = yield select(canRequestBlockDonationSelector); const recentlyClaimedBlock = yield select(recentlyClaimedBlockSelector);
yield put(openDonationModal(isBlockDonation)); yield put(openDonationModal());
if (isBlockDonation) { yield take(appTypes.closeDonationModal);
if (recentlyClaimedBlock) {
yield put(preventBlockDonationRequests()); yield put(preventBlockDonationRequests());
} else { } else {
yield put(preventProgressDonationRequests()); yield put(preventProgressDonationRequests());

View File

@ -38,7 +38,7 @@ export const defaultDonationFormState = {
const initialState = { const initialState = {
appUsername: '', appUsername: '',
canRequestBlockDonation: false, recentlyClaimedBlock: null,
canRequestProgressDonation: true, canRequestProgressDonation: true,
completionCount: 0, completionCount: 0,
currentChallengeId: store.get(CURRENT_CHALLENGE_KEY), currentChallengeId: store.get(CURRENT_CHALLENGE_KEY),
@ -55,7 +55,6 @@ const initialState = {
}, },
sessionMeta: { activeDonations: 0 }, sessionMeta: { activeDonations: 0 },
showDonationModal: false, showDonationModal: false,
isBlockDonationModal: false,
isOnline: true, isOnline: true,
donationFormState: { donationFormState: {
...defaultDonationFormState ...defaultDonationFormState
@ -187,10 +186,8 @@ export const isDonatingSelector = state => userSelector(state).isDonating;
export const isOnlineSelector = state => state[ns].isOnline; export const isOnlineSelector = state => state[ns].isOnline;
export const isSignedInSelector = state => !!state[ns].appUsername; export const isSignedInSelector = state => !!state[ns].appUsername;
export const isDonationModalOpenSelector = state => state[ns].showDonationModal; export const isDonationModalOpenSelector = state => state[ns].showDonationModal;
export const canRequestBlockDonationSelector = state => export const recentlyClaimedBlockSelector = state =>
state[ns].canRequestBlockDonation; state[ns].recentlyClaimedBlock;
export const isBlockDonationModalSelector = state =>
state[ns].isBlockDonationModal;
export const donationFormStateSelector = state => state[ns].donationFormState; export const donationFormStateSelector = state => state[ns].donationFormState;
export const signInLoadingSelector = state => export const signInLoadingSelector = state =>
userFetchStateSelector(state).pending; userFetchStateSelector(state).pending;
@ -201,13 +198,13 @@ export const shouldRequestDonationSelector = state => {
const completionCount = completionCountSelector(state); const completionCount = completionCountSelector(state);
const canRequestProgressDonation = state[ns].canRequestProgressDonation; const canRequestProgressDonation = state[ns].canRequestProgressDonation;
const isDonating = isDonatingSelector(state); const isDonating = isDonatingSelector(state);
const canRequestBlockDonation = canRequestBlockDonationSelector(state); const recentlyClaimedBlock = recentlyClaimedBlockSelector(state);
// don't request donation if already donating // don't request donation if already donating
if (isDonating) return false; if (isDonating) return false;
// a block has been completed // a block has been completed
if (canRequestBlockDonation) return true; if (recentlyClaimedBlock) return true;
// a donation has already been requested // a donation has already been requested
if (!canRequestProgressDonation) return false; if (!canRequestProgressDonation) return false;
@ -393,10 +390,12 @@ export const reducer = handleActions(
} }
}; };
}, },
[types.allowBlockDonationRequests]: state => ({ [types.allowBlockDonationRequests]: (state, { payload }) => {
return {
...state, ...state,
canRequestBlockDonation: true recentlyClaimedBlock: payload
}), };
},
[types.updateDonationFormState]: (state, { payload }) => ({ [types.updateDonationFormState]: (state, { payload }) => ({
...state, ...state,
donationFormState: { ...state.donationFormState, ...payload } donationFormState: { ...state.donationFormState, ...payload }
@ -522,14 +521,13 @@ export const reducer = handleActions(
...state, ...state,
showDonationModal: false showDonationModal: false
}), }),
[types.openDonationModal]: (state, { payload }) => ({ [types.openDonationModal]: state => ({
...state, ...state,
showDonationModal: true, showDonationModal: true
isBlockDonationModal: payload
}), }),
[types.preventBlockDonationRequests]: state => ({ [types.preventBlockDonationRequests]: state => ({
...state, ...state,
canRequestBlockDonation: false recentlyClaimedBlock: null
}), }),
[types.preventProgressDonationRequests]: state => ({ [types.preventProgressDonationRequests]: state => ({
...state, ...state,

View File

@ -20,11 +20,14 @@ import {
isCompletionModalOpenSelector, isCompletionModalOpenSelector,
successMessageSelector, successMessageSelector,
challengeFilesSelector, challengeFilesSelector,
challengeMetaSelector, challengeMetaSelector
lastBlockChalSubmitted
} from '../redux'; } from '../redux';
import { isSignedInSelector, executeGA } from '../../../redux'; import {
isSignedInSelector,
executeGA,
allowBlockDonationRequests
} from '../../../redux';
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
challengeFilesSelector, challengeFilesSelector,
@ -57,8 +60,8 @@ const mapDispatchToProps = function(dispatch) {
submitChallenge: () => { submitChallenge: () => {
dispatch(submitChallenge()); dispatch(submitChallenge());
}, },
lastBlockChalSubmitted: () => { allowBlockDonationRequests: block => {
dispatch(lastBlockChalSubmitted()); dispatch(allowBlockDonationRequests(block));
}, },
executeGA executeGA
}; };
@ -66,6 +69,7 @@ const mapDispatchToProps = function(dispatch) {
}; };
const propTypes = { const propTypes = {
allowBlockDonationRequests: PropTypes.func,
blockName: PropTypes.string, blockName: PropTypes.string,
close: PropTypes.func.isRequired, close: PropTypes.func.isRequired,
completedChallengesIds: PropTypes.array, completedChallengesIds: PropTypes.array,
@ -75,7 +79,6 @@ const propTypes = {
id: PropTypes.string, id: PropTypes.string,
isOpen: PropTypes.bool, isOpen: PropTypes.bool,
isSignedIn: PropTypes.bool.isRequired, isSignedIn: PropTypes.bool.isRequired,
lastBlockChalSubmitted: PropTypes.func,
message: PropTypes.string, message: PropTypes.string,
submitChallenge: PropTypes.func.isRequired, submitChallenge: PropTypes.func.isRequired,
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
@ -168,7 +171,7 @@ export class CompletionModalInner extends Component {
this.state.completedPercent === 100 && this.state.completedPercent === 100 &&
!this.props.completedChallengesIds.includes(this.props.id) !this.props.completedChallengesIds.includes(this.props.id)
) { ) {
this.props.lastBlockChalSubmitted(); this.props.allowBlockDonationRequests(this.props.blockName);
} }
} }

View File

@ -4,8 +4,7 @@ import store from 'store';
import { import {
isSignedInSelector, isSignedInSelector,
updateComplete, updateComplete,
updateFailed, updateFailed
allowBlockDonationRequests
} from '../../../redux'; } from '../../../redux';
import { post } from '../../../utils/ajax'; import { post } from '../../../utils/ajax';
@ -38,14 +37,9 @@ export function* updateSuccessMessageSaga() {
yield put(updateSuccessMessage(randomCompliment())); yield put(updateSuccessMessage(randomCompliment()));
} }
export function* allowBlockDonationRequestsSaga() {
yield put(allowBlockDonationRequests());
}
export function createCurrentChallengeSaga(types) { export function createCurrentChallengeSaga(types) {
return [ return [
takeEvery(types.challengeMounted, currentChallengeSaga), takeEvery(types.challengeMounted, currentChallengeSaga),
takeEvery(types.challengeMounted, updateSuccessMessageSaga), takeEvery(types.challengeMounted, updateSuccessMessageSaga)
takeEvery(types.lastBlockChalSubmitted, allowBlockDonationRequestsSaga)
]; ];
} }

View File

@ -1,12 +0,0 @@
/* global expect */
import { allowBlockDonationRequestsSaga } from './current-challenge-saga';
import { types as appTypes } from '../../../redux';
describe('allowBlockDonationRequestsSaga', () => {
it('should call allowBlockDonationRequests', () => {
const gen = allowBlockDonationRequestsSaga();
expect(gen.next().value.payload.action.type).toEqual(
appTypes.allowBlockDonationRequests
);
});
});

View File

@ -88,9 +88,7 @@ export const types = createTypes(
'setEditorFocusability', 'setEditorFocusability',
'toggleVisibleEditor', 'toggleVisibleEditor',
'setAccessibilityMode', 'setAccessibilityMode'
'lastBlockChalSubmitted'
], ],
ns ns
); );
@ -171,10 +169,6 @@ export const setEditorFocusability = createAction(types.setEditorFocusability);
export const toggleVisibleEditor = createAction(types.toggleVisibleEditor); export const toggleVisibleEditor = createAction(types.toggleVisibleEditor);
export const setAccessibilityMode = createAction(types.setAccessibilityMode); export const setAccessibilityMode = createAction(types.setAccessibilityMode);
export const lastBlockChalSubmitted = createAction(
types.lastBlockChalSubmitted
);
export const currentTabSelector = state => state[ns].currentTab; export const currentTabSelector = state => state[ns].currentTab;
export const challengeFilesSelector = state => state[ns].challengeFiles; export const challengeFilesSelector = state => state[ns].challengeFiles;
export const challengeMetaSelector = state => state[ns].challengeMeta; export const challengeMetaSelector = state => state[ns].challengeMeta;

View File

@ -0,0 +1,45 @@
/* global cy */
describe('Donate page', () => {
before(() => {
cy.clearCookies();
cy.exec('npm run seed');
cy.login();
});
after(() => {
cy.exec('npm run seed');
});
const projects = [
'tribute-page',
'survey-form',
'product-landing-page',
'technical-documentation-page',
'personal-portfolio-webpage'
];
it('Should be able to submit projects', () => {
const submitProject = str => {
cy.visit(
`/learn/responsive-web-design/responsive-web-design-projects/build-a-${str}`
);
cy.get('#dynamic-front-end-form')
.get('#solution')
.type('https://codepen.io/camperbot/full/oNvPqqo', {
force: true
});
cy.contains("I've completed this challenge").click();
cy.contains('Submit and go to next challenge').click();
};
projects.forEach(project => submitProject(project));
});
it('Should have a pop up modal', () => {
cy.contains(
'Nicely done. You just completed Responsive Web Design Projects.'
);
});
});