feat(client): unify GA and add to donation (#37984)

This commit is contained in:
Ahmad Abdolsaheb
2019-12-31 20:59:32 +03:00
committed by mrugesh
parent d07c85151b
commit 78df306707
18 changed files with 304 additions and 109 deletions

View File

@ -15,7 +15,8 @@ import {
showCert, showCert,
userFetchStateSelector, userFetchStateSelector,
usernameSelector, usernameSelector,
isDonatingSelector isDonatingSelector,
executeGA
} from '../redux'; } from '../redux';
import validCertNames from '../../utils/validCertNames'; import validCertNames from '../../utils/validCertNames';
import { createFlashMessage } from '../components/Flash/redux'; import { createFlashMessage } from '../components/Flash/redux';
@ -37,6 +38,7 @@ const propTypes = {
certDashedName: PropTypes.string, certDashedName: PropTypes.string,
certName: PropTypes.string, certName: PropTypes.string,
createFlashMessage: PropTypes.func.isRequired, createFlashMessage: PropTypes.func.isRequired,
executeGA: PropTypes.func,
fetchState: PropTypes.shape({ fetchState: PropTypes.shape({
pending: PropTypes.bool, pending: PropTypes.bool,
complete: PropTypes.bool, complete: PropTypes.bool,
@ -74,19 +76,20 @@ const mapStateToProps = (state, { certName }) => {
}; };
const mapDispatchToProps = dispatch => const mapDispatchToProps = dispatch =>
bindActionCreators({ createFlashMessage, showCert }, dispatch); bindActionCreators({ createFlashMessage, showCert, executeGA }, dispatch);
class ShowCertification extends Component { class ShowCertification extends Component {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
this.state = { this.state = {
closeBtn: false, isDonationSubmitted: false,
donationClosed: false isDonationDisplayed: false,
isDonationClosed: false
}; };
this.hideDonationSection = this.hideDonationSection.bind(this); this.hideDonationSection = this.hideDonationSection.bind(this);
this.showDonationCloseBtn = this.showDonationCloseBtn.bind(this); this.handleProcessing = this.handleProcessing.bind(this);
} }
componentDidMount() { componentDidMount() {
@ -97,12 +100,53 @@ class ShowCertification extends Component {
return null; return null;
} }
hideDonationSection() { shouldComponentUpdate(nextProps) {
this.setState({ donationClosed: true }); const {
userFetchState: { complete: userComplete },
signedInUserName,
isDonating,
cert: { username = '' },
executeGA
} = nextProps;
const { isDonationDisplayed } = this.state;
if (
!isDonationDisplayed &&
userComplete &&
signedInUserName === username &&
!isDonating
) {
this.setState({
isDonationDisplayed: true
});
executeGA({
type: 'event',
data: {
category: 'Donation',
action: 'Displayed Certificate Donation',
nonInteraction: true
}
});
}
return true;
} }
showDonationCloseBtn() { hideDonationSection() {
this.setState({ closeBtn: true }); this.setState({ isDonationDisplayed: false, isDonationClosed: true });
}
handleProcessing(duration, amount) {
this.props.executeGA({
type: 'event',
data: {
category: 'donation',
action: 'certificate stripe form submission',
label: duration,
value: amount
}
});
this.setState({ isDonationSubmitted: true });
} }
render() { render() {
@ -111,13 +155,14 @@ class ShowCertification extends Component {
fetchState, fetchState,
validCertName, validCertName,
createFlashMessage, createFlashMessage,
certName, certName
signedInUserName,
isDonating,
userFetchState
} = this.props; } = this.props;
const { donationClosed, closeBtn } = this.state; const {
isDonationSubmitted,
isDonationDisplayed,
isDonationClosed
} = this.state;
if (!validCertName) { if (!validCertName) {
createFlashMessage(standardErrorMessage); createFlashMessage(standardErrorMessage);
@ -125,7 +170,6 @@ class ShowCertification extends Component {
} }
const { pending, complete, errored } = fetchState; const { pending, complete, errored } = fetchState;
const { complete: userComplete } = userFetchState;
if (pending) { if (pending) {
return <Loader fullScreen={true} />; return <Loader fullScreen={true} />;
@ -149,8 +193,6 @@ class ShowCertification extends Component {
completionTime completionTime
} = cert; } = cert;
let conditionalDonationSection = '';
const donationCloseBtn = ( const donationCloseBtn = (
<div> <div>
<Button <Button
@ -164,43 +206,36 @@ class ShowCertification extends Component {
</div> </div>
); );
if ( let donationSection = (
userComplete && <Grid className='donation-section'>
signedInUserName === username && {!isDonationSubmitted && (
!isDonating &&
!donationClosed
) {
conditionalDonationSection = (
<Grid className='donation-section'>
{!closeBtn && (
<Row>
<Col sm={10} smOffset={1} xs={12}>
<p>
Only you can see this message. Congratulations on earning this
certification. Its no easy task. Running freeCodeCamp isnt
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.
</p>
</Col>
</Row>
)}
<MinimalDonateForm
showCloseBtn={this.showDonationCloseBtn}
defaultTheme='light'
/>
<Row> <Row>
<Col sm={4} smOffset={4} xs={6} xsOffset={3}> <Col sm={10} smOffset={1} xs={12}>
{closeBtn ? donationCloseBtn : ''} <p>
Only you can see this message. Congratulations on earning this
certification. Its no easy task. Running freeCodeCamp isnt
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.
</p>
</Col> </Col>
</Row> </Row>
</Grid> )}
); <MinimalDonateForm
} handleProcessing={this.handleProcessing}
defaultTheme='light'
/>
<Row>
<Col sm={4} smOffset={4} xs={6} xsOffset={3}>
{isDonationSubmitted && donationCloseBtn}
</Col>
</Row>
</Grid>
);
return ( return (
<div className='certificate-outer-wrapper'> <div className='certificate-outer-wrapper'>
{conditionalDonationSection} {isDonationDisplayed && !isDonationClosed ? donationSection : ''}
<Grid className='certificate-wrapper certification-namespace'> <Grid className='certificate-wrapper certification-namespace'>
<Row> <Row>
<header> <header>

View File

@ -34,6 +34,7 @@ const numToCommas = num =>
num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
const propTypes = { const propTypes = {
handleProcessing: PropTypes.func,
isDonating: PropTypes.bool, isDonating: PropTypes.bool,
isSignedIn: PropTypes.bool, isSignedIn: PropTypes.bool,
navigate: PropTypes.func.isRequired, navigate: PropTypes.func.isRequired,
@ -191,7 +192,7 @@ class DonateForm extends Component {
} }
renderDonationOptions() { renderDonationOptions() {
const { stripe } = this.props; const { stripe, handleProcessing } = this.props;
const { donationAmount, donationDuration, paymentType } = this.state; const { donationAmount, donationDuration, paymentType } = this.state;
return ( return (
<div> <div>
@ -203,6 +204,7 @@ class DonateForm extends Component {
donationAmount={donationAmount} donationAmount={donationAmount}
donationDuration={donationDuration} donationDuration={donationDuration}
getDonationButtonLabel={this.getDonationButtonLabel} getDonationButtonLabel={this.getDonationButtonLabel}
handleProcessing={handleProcessing}
hideAmountOptionsCB={this.hideAmountOptionsCB} hideAmountOptionsCB={this.hideAmountOptionsCB}
/> />
</Elements> </Elements>

View File

@ -24,6 +24,7 @@ const propTypes = {
donationDuration: PropTypes.string.isRequired, donationDuration: PropTypes.string.isRequired,
email: PropTypes.string, email: PropTypes.string,
getDonationButtonLabel: PropTypes.func.isRequired, getDonationButtonLabel: PropTypes.func.isRequired,
handleProcessing: PropTypes.func,
isSignedIn: PropTypes.bool, isSignedIn: PropTypes.bool,
showCloseBtn: PropTypes.func, showCloseBtn: PropTypes.func,
stripe: PropTypes.shape({ stripe: PropTypes.shape({
@ -148,8 +149,11 @@ class DonateFormChildViewForHOC extends Component {
// change the donation modal button label to close // change the donation modal button label to close
// or display the close button for the cert donation section // or display the close button for the cert donation section
if (this.props.showCloseBtn) { if (this.props.handleProcessing) {
this.props.showCloseBtn(); this.props.handleProcessing(
this.state.donationDuration,
Math.round(this.state.donationAmount / 100)
);
} }
return postChargeStripe(yearEndGift, { return postChargeStripe(yearEndGift, {

View File

@ -11,11 +11,11 @@ import Heart from '../../assets/icons/Heart';
import Cup from '../../assets/icons/Cup'; import Cup from '../../assets/icons/Cup';
import MinimalDonateForm from './MinimalDonateForm'; import MinimalDonateForm from './MinimalDonateForm';
import ga from '../../analytics';
import { import {
closeDonationModal, closeDonationModal,
isDonationModalOpenSelector, isDonationModalOpenSelector,
isBlockDonationModalSelector isBlockDonationModalSelector,
executeGA
} from '../../redux'; } from '../../redux';
import { challengeMetaSelector } from '../../templates/Challenges/redux'; import { challengeMetaSelector } from '../../templates/Challenges/redux';
@ -36,7 +36,8 @@ const mapStateToProps = createSelector(
const mapDispatchToProps = dispatch => const mapDispatchToProps = dispatch =>
bindActionCreators( bindActionCreators(
{ {
closeDonationModal closeDonationModal,
executeGA
}, },
dispatch dispatch
); );
@ -45,18 +46,44 @@ const propTypes = {
activeDonors: PropTypes.number, activeDonors: PropTypes.number,
block: PropTypes.string, block: PropTypes.string,
closeDonationModal: PropTypes.func.isRequired, closeDonationModal: PropTypes.func.isRequired,
executeGA: PropTypes.func,
isBlockDonation: PropTypes.bool, isBlockDonation: PropTypes.bool,
show: PropTypes.bool show: PropTypes.bool
}; };
function DonateModal({ show, block, isBlockDonation, closeDonationModal }) { function DonateModal({
show,
block,
isBlockDonation,
closeDonationModal,
executeGA
}) {
const [closeLabel, setCloseLabel] = React.useState(false); const [closeLabel, setCloseLabel] = React.useState(false);
const showCloseBtn = () => { const handleProcessing = (duration, amount) => {
executeGA({
type: 'event',
data: {
category: 'donation',
action: 'Modal strip form submission',
label: duration,
value: amount
}
});
setCloseLabel(true); setCloseLabel(true);
}; };
if (show) { if (show) {
ga.modalview('/donation-modal'); executeGA({ type: 'modal', data: '/donation-modal' });
executeGA({
type: 'event',
data: {
category: 'Donation',
action: `Displayed ${
isBlockDonation ? 'block' : 'progress'
} donation modal`,
nonInteraction: true
}
});
} }
const donationText = ( const donationText = (
@ -101,7 +128,7 @@ function DonateModal({ show, block, isBlockDonation, closeDonationModal }) {
<Modal.Body> <Modal.Body>
{isBlockDonation ? blockDonationText : progressDonationText} {isBlockDonation ? blockDonationText : progressDonationText}
<Spacer /> <Spacer />
<MinimalDonateForm showCloseBtn={showCloseBtn} /> <MinimalDonateForm handleProcessing={handleProcessing} />
<Spacer /> <Spacer />
<Row> <Row>
<Col sm={4} smOffset={4} xs={8} xsOffset={2}> <Col sm={4} smOffset={4} xs={8} xsOffset={2}>

View File

@ -19,8 +19,8 @@ import './Donation.css';
const propTypes = { const propTypes = {
defaultTheme: PropTypes.string, defaultTheme: PropTypes.string,
handleProcessing: PropTypes.func,
isDonating: PropTypes.bool, isDonating: PropTypes.bool,
showCloseBtn: PropTypes.func,
stripe: PropTypes.shape({ stripe: PropTypes.shape({
createToken: PropTypes.func.isRequired createToken: PropTypes.func.isRequired
}) })
@ -79,7 +79,7 @@ class MinimalDonateForm extends Component {
render() { render() {
const { donationAmount, donationDuration, stripe } = this.state; const { donationAmount, donationDuration, stripe } = this.state;
const { showCloseBtn, defaultTheme } = this.props; const { handleProcessing, defaultTheme } = this.props;
return ( return (
<Row> <Row>
@ -93,7 +93,7 @@ class MinimalDonateForm extends Component {
getDonationButtonLabel={() => getDonationButtonLabel={() =>
`Confirm your donation of $5 per month` `Confirm your donation of $5 per month`
} }
showCloseBtn={showCloseBtn} handleProcessing={handleProcessing}
/> />
</Elements> </Elements>
</StripeProvider> </StripeProvider>

View File

@ -5,9 +5,8 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { Link } from 'gatsby'; import { Link } from 'gatsby';
import ga from '../../../analytics';
import { makeExpandedBlockSelector, toggleBlock } from '../redux'; import { makeExpandedBlockSelector, toggleBlock } from '../redux';
import { completedChallengesSelector } from '../../../redux'; import { completedChallengesSelector, executeGA } from '../../../redux';
import Caret from '../../../assets/icons/Caret'; import Caret from '../../../assets/icons/Caret';
import { blockNameify } from '../../../../utils/blockNameify'; import { blockNameify } from '../../../../utils/blockNameify';
import GreenPass from '../../../assets/icons/GreenPass'; import GreenPass from '../../../assets/icons/GreenPass';
@ -29,12 +28,13 @@ const mapStateToProps = (state, ownProps) => {
}; };
const mapDispatchToProps = dispatch => const mapDispatchToProps = dispatch =>
bindActionCreators({ toggleBlock }, dispatch); bindActionCreators({ toggleBlock, executeGA }, dispatch);
const propTypes = { const propTypes = {
blockDashedName: PropTypes.string, blockDashedName: PropTypes.string,
challenges: PropTypes.array, challenges: PropTypes.array,
completedChallenges: PropTypes.arrayOf(PropTypes.string), completedChallenges: PropTypes.arrayOf(PropTypes.string),
executeGA: PropTypes.func,
intro: PropTypes.shape({ intro: PropTypes.shape({
fields: PropTypes.shape({ slug: PropTypes.string.isRequired }), fields: PropTypes.shape({ slug: PropTypes.string.isRequired }),
frontmatter: PropTypes.shape({ frontmatter: PropTypes.shape({
@ -58,19 +58,25 @@ export class Block extends Component {
} }
handleBlockClick() { handleBlockClick() {
const { blockDashedName, toggleBlock } = this.props; const { blockDashedName, toggleBlock, executeGA } = this.props;
ga.event({ executeGA({
category: 'Map Block Click', type: 'event',
action: blockDashedName data: {
category: 'Map Block Click',
action: blockDashedName
}
}); });
return toggleBlock(blockDashedName); return toggleBlock(blockDashedName);
} }
handleChallengeClick(slug) { handleChallengeClick(slug) {
return () => { return () => {
return ga.event({ return this.props.executeGA({
category: 'Map Challenge Click', type: 'event',
action: slug data: {
category: 'Map Challenge Click',
action: slug
}
}); });
}; };
} }

View File

@ -44,6 +44,7 @@ test('<Block expanded snapshot', () => {
test('<Block /> should handle toggle clicks correctly', async () => { test('<Block /> should handle toggle clicks correctly', async () => {
const toggleSpy = jest.fn(); const toggleSpy = jest.fn();
const toggleMapSpy = jest.fn(); const toggleMapSpy = jest.fn();
const executeGA = jest.fn();
const props = { const props = {
blockDashedName: 'block-a', blockDashedName: 'block-a',
@ -51,6 +52,7 @@ test('<Block /> should handle toggle clicks correctly', async () => {
completedChallenges: mockCompleted, completedChallenges: mockCompleted,
intro: mockIntroNodes[0], intro: mockIntroNodes[0],
isExpanded: false, isExpanded: false,
executeGA: executeGA,
toggleBlock: toggleSpy, toggleBlock: toggleSpy,
toggleMapModal: toggleMapSpy toggleMapModal: toggleMapSpy
}; };

View File

@ -26,9 +26,10 @@ const numToCommas = num =>
num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
const propTypes = { const propTypes = {
showCloseBtn: PropTypes.func, handleProcessing: PropTypes.func,
defaultTheme: PropTypes.string, defaultTheme: PropTypes.string,
isDonating: PropTypes.bool, isDonating: PropTypes.bool,
executeGA: PropTypes.func,
stripe: PropTypes.shape({ stripe: PropTypes.shape({
createToken: PropTypes.func.isRequired createToken: PropTypes.func.isRequired
}) })
@ -47,6 +48,7 @@ class YearEndDonationForm extends Component {
this.handleSelectAmount = this.handleSelectAmount.bind(this); this.handleSelectAmount = this.handleSelectAmount.bind(this);
this.handleChange = this.handleChange.bind(this); this.handleChange = this.handleChange.bind(this);
this.handleClick = this.handleClick.bind(this); this.handleClick = this.handleClick.bind(this);
this.handlePaypalSubmission = this.handlePaypalSubmission.bind(this);
} }
componentDidMount() { componentDidMount() {
@ -89,14 +91,13 @@ class YearEndDonationForm extends Component {
renderDonationOptions() { renderDonationOptions() {
const { donationAmount, stripe } = this.state; const { donationAmount, stripe } = this.state;
const { handleProcessing, defaultTheme } = this.props;
const { showCloseBtn, defaultTheme } = this.props;
return ( return (
<div> <div>
<StripeProvider stripe={stripe}> <StripeProvider stripe={stripe}>
<Elements> <Elements>
<DonateFormChildViewForHOC <DonateFormChildViewForHOC
showCloseBtn={showCloseBtn} handleProcessing={handleProcessing}
defaultTheme={defaultTheme} defaultTheme={defaultTheme}
donationAmount={donationAmount} donationAmount={donationAmount}
donationDuration='onetime' donationDuration='onetime'
@ -179,12 +180,23 @@ class YearEndDonationForm extends Component {
); );
} }
handlePaypalSubmission() {
this.props.executeGA({
type: 'event',
data: {
category: 'donation',
action: 'year end gift paypal button click'
}
});
}
renderPayPalDonations() { renderPayPalDonations() {
return ( return (
<form <form
action='https://www.paypal.com/cgi-bin/webscr' action='https://www.paypal.com/cgi-bin/webscr'
method='post' method='post'
target='_top' target='_top'
onSubmit={this.handlePaypalSubmission}
> >
<input type='hidden' name='cmd' value='_s-xclick' /> <input type='hidden' name='cmd' value='_s-xclick' />
<input type='hidden' name='hosted_button_id' value='9C73W6CWSLNPW' /> <input type='hidden' name='hosted_button_id' value='9C73W6CWSLNPW' />

View File

@ -2,8 +2,7 @@ import React, { Fragment, Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ga from '../../analytics'; import { fetchUser, isSignedInSelector, executeGA } from '../../redux';
import { fetchUser, isSignedInSelector } from '../../redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
@ -13,7 +12,7 @@ const mapStateToProps = createSelector(
}) })
); );
const mapDispatchToProps = { fetchUser }; const mapDispatchToProps = { fetchUser, executeGA };
class CertificationLayout extends Component { class CertificationLayout extends Component {
componentDidMount() { componentDidMount() {
@ -21,7 +20,7 @@ class CertificationLayout extends Component {
if (!isSignedIn) { if (!isSignedIn) {
fetchUser(); fetchUser();
} }
ga.pageview(pathname); this.props.executeGA({ type: 'page', data: pathname });
} }
render() { render() {
return <Fragment>{this.props.children}</Fragment>; return <Fragment>{this.props.children}</Fragment>;
@ -31,6 +30,7 @@ class CertificationLayout extends Component {
CertificationLayout.displayName = 'CertificationLayout'; CertificationLayout.displayName = 'CertificationLayout';
CertificationLayout.propTypes = { CertificationLayout.propTypes = {
children: PropTypes.any, children: PropTypes.any,
executeGA: PropTypes.func,
fetchUser: PropTypes.func.isRequired, fetchUser: PropTypes.func.isRequired,
isSignedIn: PropTypes.bool, isSignedIn: PropTypes.bool,
pathname: PropTypes.string.isRequired pathname: PropTypes.string.isRequired

View File

@ -6,13 +6,13 @@ import { createSelector } from 'reselect';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import fontawesome from '@fortawesome/fontawesome'; import fontawesome from '@fortawesome/fontawesome';
import ga from '../../analytics';
import { import {
fetchUser, fetchUser,
isSignedInSelector, isSignedInSelector,
onlineStatusChange, onlineStatusChange,
isOnlineSelector, isOnlineSelector,
userSelector userSelector,
executeGA
} from '../../redux'; } from '../../redux';
import { flashMessageSelector, removeFlashMessage } from '../Flash/redux'; import { flashMessageSelector, removeFlashMessage } from '../Flash/redux';
@ -68,6 +68,7 @@ const metaKeywords = [
const propTypes = { const propTypes = {
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
executeGA: PropTypes.func,
fetchUser: PropTypes.func.isRequired, fetchUser: PropTypes.func.isRequired,
flashMessage: PropTypes.shape({ flashMessage: PropTypes.shape({
id: PropTypes.string, id: PropTypes.string,
@ -101,27 +102,27 @@ const mapStateToProps = createSelector(
const mapDispatchToProps = dispatch => const mapDispatchToProps = dispatch =>
bindActionCreators( bindActionCreators(
{ fetchUser, removeFlashMessage, onlineStatusChange }, { fetchUser, removeFlashMessage, onlineStatusChange, executeGA },
dispatch dispatch
); );
class DefaultLayout extends Component { class DefaultLayout extends Component {
componentDidMount() { componentDidMount() {
const { isSignedIn, fetchUser, pathname } = this.props; const { isSignedIn, fetchUser, pathname, executeGA } = this.props;
if (!isSignedIn) { if (!isSignedIn) {
fetchUser(); fetchUser();
} }
ga.pageview(pathname); executeGA({ type: 'page', data: pathname });
window.addEventListener('online', this.updateOnlineStatus); window.addEventListener('online', this.updateOnlineStatus);
window.addEventListener('offline', this.updateOnlineStatus); window.addEventListener('offline', this.updateOnlineStatus);
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { pathname } = this.props; const { pathname, executeGA } = this.props;
const { pathname: prevPathname } = prevProps; const { pathname: prevPathname } = prevProps;
if (pathname !== prevPathname) { if (pathname !== prevPathname) {
ga.pageview(pathname); executeGA({ type: 'page', data: pathname });
} }
} }

View File

@ -1,6 +1,7 @@
import React, { Component, Fragment } from 'react'; import React, { Component, Fragment } from 'react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { Grid, Row, Col } from '@freecodecamp/react-bootstrap'; import { Grid, Row, Col } from '@freecodecamp/react-bootstrap';
@ -9,10 +10,11 @@ import { stripePublicKey } from '../../config/env.json';
import { Spacer, Loader } from '../components/helpers'; import { Spacer, Loader } from '../components/helpers';
import DonateForm from '../components/Donation/DonateForm'; import DonateForm from '../components/Donation/DonateForm';
import DonateText from '../components/Donation/DonateText'; import DonateText from '../components/Donation/DonateText';
import { signInLoadingSelector, userSelector } from '../redux'; import { signInLoadingSelector, userSelector, executeGA } from '../redux';
import { stripeScriptLoader } from '../utils/scriptLoaders'; import { stripeScriptLoader } from '../utils/scriptLoaders';
const propTypes = { const propTypes = {
executeGA: PropTypes.func,
isDonating: PropTypes.bool, isDonating: PropTypes.bool,
showLoading: PropTypes.bool.isRequired showLoading: PropTypes.bool.isRequired
}; };
@ -26,6 +28,14 @@ const mapStateToProps = createSelector(
}) })
); );
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
executeGA
},
dispatch
);
export class DonatePage extends Component { export class DonatePage extends Component {
constructor(...props) { constructor(...props) {
super(...props); super(...props);
@ -33,11 +43,19 @@ export class DonatePage extends Component {
stripe: null, stripe: null,
enableSettings: false enableSettings: false
}; };
this.handleProcessing = this.handleProcessing.bind(this);
this.handleStripeLoad = this.handleStripeLoad.bind(this); this.handleStripeLoad = this.handleStripeLoad.bind(this);
} }
componentDidMount() { componentDidMount() {
this.props.executeGA({
type: 'event',
data: {
category: 'Donation',
action: `Displayed donate page`,
nonInteraction: true
}
});
if (window.Stripe) { if (window.Stripe) {
this.handleStripeLoad(); this.handleStripeLoad();
} else if (document.querySelector('#stripe-js')) { } else if (document.querySelector('#stripe-js')) {
@ -56,6 +74,18 @@ export class DonatePage extends Component {
} }
} }
handleProcessing(duration, amount) {
this.props.executeGA({
type: 'event',
data: {
category: 'donation',
action: 'donate page stripe form submission',
label: duration,
value: amount
}
});
}
handleStripeLoad() { handleStripeLoad() {
// Create Stripe instance once Stripe.js loads // Create Stripe instance once Stripe.js loads
console.info('stripe has loaded'); console.info('stripe has loaded');
@ -88,6 +118,7 @@ export class DonatePage extends Component {
<Col md={6}> <Col md={6}>
<DonateForm <DonateForm
enableDonationSettingsPage={this.enableDonationSettingsPage} enableDonationSettingsPage={this.enableDonationSettingsPage}
handleProcessing={this.handleProcessing}
stripe={stripe} stripe={stripe}
/> />
</Col> </Col>
@ -105,4 +136,7 @@ export class DonatePage extends Component {
DonatePage.displayName = 'DonatePage'; DonatePage.displayName = 'DonatePage';
DonatePage.propTypes = propTypes; DonatePage.propTypes = propTypes;
export default connect(mapStateToProps)(DonatePage); export default connect(
mapStateToProps,
mapDispatchToProps
)(DonatePage);

View File

@ -1,11 +1,48 @@
import React from 'react'; import React, { useEffect } from 'react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import { Grid } from '@freecodecamp/react-bootstrap'; import { Grid } from '@freecodecamp/react-bootstrap';
import { connect } from 'react-redux';
import { executeGA } from '../redux';
import { bindActionCreators } from 'redux';
import PropTypes from 'prop-types';
import { Spacer, FullWidthRow } from '../components/helpers'; import { Spacer, FullWidthRow } from '../components/helpers';
import YearEndDonationForm from '../components/YearEndGift/YearEndDonationForm'; import YearEndDonationForm from '../components/YearEndGift/YearEndDonationForm';
function YearEndGiftPage() { const mapDispatchToProps = dispatch =>
bindActionCreators(
{
executeGA
},
dispatch
);
const propTypes = {
executeGA: PropTypes.func
};
function YearEndGiftPage({ executeGA }) {
useEffect(() => {
executeGA({
type: 'event',
data: {
category: 'Donation',
action: `Displayed year end gift page`,
nonInteraction: true
}
});
}, [executeGA]);
const handleProcessing = (duration, amount) => {
executeGA({
type: 'event',
data: {
category: 'donation',
action: 'year-end-gift strip form submission',
label: duration,
value: amount
}
});
};
return ( return (
<> <>
<Helmet title='Support our nonprofit | freeCodeCamp.org' /> <Helmet title='Support our nonprofit | freeCodeCamp.org' />
@ -13,7 +50,11 @@ function YearEndGiftPage() {
<main> <main>
<Spacer /> <Spacer />
<FullWidthRow> <FullWidthRow>
<YearEndDonationForm defaultTheme='light' /> <YearEndDonationForm
defaultTheme='light'
executeGA={executeGA}
handleProcessing={handleProcessing}
/>
</FullWidthRow> </FullWidthRow>
<Spacer /> <Spacer />
<Spacer /> <Spacer />
@ -24,5 +65,9 @@ function YearEndGiftPage() {
} }
YearEndGiftPage.displayName = 'YearEndGiftPage'; YearEndGiftPage.displayName = 'YearEndGiftPage';
YearEndGiftPage.propTypes = propTypes;
export default YearEndGiftPage; export default connect(
null,
mapDispatchToProps
)(YearEndGiftPage);

View File

@ -0,0 +1,11 @@
import { takeEvery } from 'redux-saga/effects';
import ga from '../analytics';
function* callGaType({ payload: { type, data } }) {
const GaTypes = { event: ga.event, page: ga.pageview, modal: ga.modalview };
GaTypes[type](data);
}
export function createGaSaga(types) {
return [takeEvery(types.executeGA, callGaType)];
}

View File

@ -11,6 +11,7 @@ import { createReportUserSaga } from './report-user-saga';
import { createShowCertSaga } from './show-cert-saga'; import { createShowCertSaga } from './show-cert-saga';
import { createNightModeSaga } from './night-mode-saga'; import { createNightModeSaga } from './night-mode-saga';
import { createDonationSaga } from './donation-saga'; import { createDonationSaga } from './donation-saga';
import { createGaSaga } from './ga-saga';
import hardGoToEpic from './hard-go-to-epic'; import hardGoToEpic from './hard-go-to-epic';
import failedUpdatesEpic from './failed-updates-epic'; import failedUpdatesEpic from './failed-updates-epic';
@ -65,6 +66,7 @@ export const types = createTypes(
'onlineStatusChange', 'onlineStatusChange',
'resetUserData', 'resetUserData',
'tryToShowDonationModal', 'tryToShowDonationModal',
'executeGA',
'submitComplete', 'submitComplete',
'updateComplete', 'updateComplete',
'updateCurrentChallengeId', 'updateCurrentChallengeId',
@ -84,6 +86,7 @@ export const sagas = [
...createAcceptTermsSaga(types), ...createAcceptTermsSaga(types),
...createAppMountSaga(types), ...createAppMountSaga(types),
...createDonationSaga(types), ...createDonationSaga(types),
...createGaSaga(types),
...createFetchUserSaga(types), ...createFetchUserSaga(types),
...createShowCertSaga(types), ...createShowCertSaga(types),
...createReportUserSaga(types), ...createReportUserSaga(types),
@ -95,6 +98,9 @@ export const appMount = createAction(types.appMount);
export const tryToShowDonationModal = createAction( export const tryToShowDonationModal = createAction(
types.tryToShowDonationModal types.tryToShowDonationModal
); );
export const executeGA = createAction(types.executeGA);
export const allowBlockDonationRequests = createAction( export const allowBlockDonationRequests = createAction(
types.allowBlockDonationRequests types.allowBlockDonationRequests
); );

View File

@ -6,10 +6,8 @@ import { createSelector } from 'reselect';
import { Button, Modal } from '@freecodecamp/react-bootstrap'; import { Button, Modal } from '@freecodecamp/react-bootstrap';
import { useStaticQuery, graphql } from 'gatsby'; import { useStaticQuery, graphql } from 'gatsby';
import ga from '../../../analytics';
import Login from '../../../components/Header/components/Login'; import Login from '../../../components/Header/components/Login';
import CompletionModalBody from './CompletionModalBody'; import CompletionModalBody from './CompletionModalBody';
import { dasherize } from '../../../../../utils/slugs'; import { dasherize } from '../../../../../utils/slugs';
import './completion-modal.css'; import './completion-modal.css';
@ -25,7 +23,7 @@ import {
lastBlockChalSubmitted lastBlockChalSubmitted
} from '../redux'; } from '../redux';
import { isSignedInSelector } from '../../../redux'; import { isSignedInSelector, executeGA } from '../../../redux';
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
challengeFilesSelector, challengeFilesSelector,
@ -60,7 +58,8 @@ const mapDispatchToProps = function(dispatch) {
}, },
lastBlockChalSubmitted: () => { lastBlockChalSubmitted: () => {
dispatch(lastBlockChalSubmitted()); dispatch(lastBlockChalSubmitted());
} },
executeGA
}; };
return () => dispatchers; return () => dispatchers;
}; };
@ -70,6 +69,7 @@ const propTypes = {
close: PropTypes.func.isRequired, close: PropTypes.func.isRequired,
completedChallengesIds: PropTypes.array, completedChallengesIds: PropTypes.array,
currentBlockIds: PropTypes.array, currentBlockIds: PropTypes.array,
executeGA: PropTypes.func,
files: PropTypes.object.isRequired, files: PropTypes.object.isRequired,
id: PropTypes.string, id: PropTypes.string,
isOpen: PropTypes.bool, isOpen: PropTypes.bool,
@ -190,7 +190,7 @@ export class CompletionModalInner extends Component {
const { completedPercent } = this.state; const { completedPercent } = this.state;
if (isOpen) { if (isOpen) {
ga.modalview('/completion-modal'); executeGA({ type: 'modal', data: '/completion-modal' });
} }
const dashedName = dasherize(title); const dashedName = dasherize(title);
return ( return (

View File

@ -4,21 +4,22 @@ import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Button, Modal } from '@freecodecamp/react-bootstrap'; import { Button, Modal } from '@freecodecamp/react-bootstrap';
import ga from '../../../analytics';
import { createQuestion, closeModal, isHelpModalOpenSelector } from '../redux'; import { createQuestion, closeModal, isHelpModalOpenSelector } from '../redux';
import { executeGA } from '../../../redux';
import './help-modal.css'; import './help-modal.css';
const mapStateToProps = state => ({ isOpen: isHelpModalOpenSelector(state) }); const mapStateToProps = state => ({ isOpen: isHelpModalOpenSelector(state) });
const mapDispatchToProps = dispatch => const mapDispatchToProps = dispatch =>
bindActionCreators( bindActionCreators(
{ createQuestion, closeHelpModal: () => closeModal('help') }, { createQuestion, executeGA, closeHelpModal: () => closeModal('help') },
dispatch dispatch
); );
const propTypes = { const propTypes = {
closeHelpModal: PropTypes.func.isRequired, closeHelpModal: PropTypes.func.isRequired,
createQuestion: PropTypes.func.isRequired, createQuestion: PropTypes.func.isRequired,
executeGA: PropTypes.func,
isOpen: PropTypes.bool isOpen: PropTypes.bool
}; };
@ -27,9 +28,9 @@ const RSA =
export class HelpModal extends Component { export class HelpModal extends Component {
render() { render() {
const { isOpen, closeHelpModal, createQuestion } = this.props; const { isOpen, closeHelpModal, createQuestion, executeGA } = this.props;
if (isOpen) { if (isOpen) {
ga.modalview('/help-modal'); executeGA({ type: 'modal', data: '/help-modal' });
} }
return ( return (
<Modal dialogClassName='help-modal' onHide={closeHelpModal} show={isOpen}> <Modal dialogClassName='help-modal' onHide={closeHelpModal} show={isOpen}>

View File

@ -5,13 +5,14 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { Button, Modal } from '@freecodecamp/react-bootstrap'; import { Button, Modal } from '@freecodecamp/react-bootstrap';
import ga from '../../../analytics';
import { isResetModalOpenSelector, closeModal, resetChallenge } from '../redux'; import { isResetModalOpenSelector, closeModal, resetChallenge } from '../redux';
import { executeGA } from '../../../redux';
import './reset-modal.css'; import './reset-modal.css';
const propTypes = { const propTypes = {
close: PropTypes.func.isRequired, close: PropTypes.func.isRequired,
executeGA: PropTypes.func,
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
reset: PropTypes.func.isRequired reset: PropTypes.func.isRequired
}; };
@ -25,7 +26,11 @@ const mapStateToProps = createSelector(
const mapDispatchToProps = dispatch => const mapDispatchToProps = dispatch =>
bindActionCreators( bindActionCreators(
{ close: () => closeModal('reset'), reset: () => resetChallenge() }, {
close: () => closeModal('reset'),
executeGA,
reset: () => resetChallenge()
},
dispatch dispatch
); );
@ -35,7 +40,7 @@ function withActions(...fns) {
function ResetModal({ reset, close, isOpen }) { function ResetModal({ reset, close, isOpen }) {
if (isOpen) { if (isOpen) {
ga.modalview('/reset-modal'); executeGA({ type: 'modal', data: '/reset-modal' });
} }
return ( return (
<Modal <Modal

View File

@ -4,26 +4,30 @@ import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Modal } from '@freecodecamp/react-bootstrap'; import { Modal } from '@freecodecamp/react-bootstrap';
import ga from '../../../analytics';
import { closeModal, isVideoModalOpenSelector } from '../redux'; import { closeModal, isVideoModalOpenSelector } from '../redux';
import { executeGA } from '../../../redux';
import './video-modal.css'; import './video-modal.css';
const mapStateToProps = state => ({ isOpen: isVideoModalOpenSelector(state) }); const mapStateToProps = state => ({ isOpen: isVideoModalOpenSelector(state) });
const mapDispatchToProps = dispatch => const mapDispatchToProps = dispatch =>
bindActionCreators({ closeVideoModal: () => closeModal('video') }, dispatch); bindActionCreators(
{ closeVideoModal: () => closeModal('video'), executeGA },
dispatch
);
const propTypes = { const propTypes = {
closeVideoModal: PropTypes.func.isRequired, closeVideoModal: PropTypes.func.isRequired,
executeGA: propTypes.func,
isOpen: PropTypes.bool, isOpen: PropTypes.bool,
videoUrl: PropTypes.string videoUrl: PropTypes.string
}; };
export class VideoModal extends Component { export class VideoModal extends Component {
render() { render() {
const { isOpen, closeVideoModal, videoUrl } = this.props; const { isOpen, closeVideoModal, videoUrl, executeGA } = this.props;
if (isOpen) { if (isOpen) {
ga.modalview('/video-modal'); executeGA({ type: 'modal', data: '/completion-modal' });
} }
return ( return (
<Modal <Modal