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,15 +206,9 @@ class ShowCertification extends Component {
</div> </div>
); );
if ( let donationSection = (
userComplete &&
signedInUserName === username &&
!isDonating &&
!donationClosed
) {
conditionalDonationSection = (
<Grid className='donation-section'> <Grid className='donation-section'>
{!closeBtn && ( {!isDonationSubmitted && (
<Row> <Row>
<Col sm={10} smOffset={1} xs={12}> <Col sm={10} smOffset={1} xs={12}>
<p> <p>
@ -186,21 +222,20 @@ class ShowCertification extends Component {
</Row> </Row>
)} )}
<MinimalDonateForm <MinimalDonateForm
showCloseBtn={this.showDonationCloseBtn} handleProcessing={this.handleProcessing}
defaultTheme='light' defaultTheme='light'
/> />
<Row> <Row>
<Col sm={4} smOffset={4} xs={6} xsOffset={3}> <Col sm={4} smOffset={4} xs={6} xsOffset={3}>
{closeBtn ? donationCloseBtn : ''} {isDonationSubmitted && donationCloseBtn}
</Col> </Col>
</Row> </Row>
</Grid> </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({
type: 'event',
data: {
category: 'Map Block Click', category: 'Map Block Click',
action: blockDashedName action: blockDashedName
}
}); });
return toggleBlock(blockDashedName); return toggleBlock(blockDashedName);
} }
handleChallengeClick(slug) { handleChallengeClick(slug) {
return () => { return () => {
return ga.event({ return this.props.executeGA({
type: 'event',
data: {
category: 'Map Challenge Click', category: 'Map Challenge Click',
action: slug 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