mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 18:18:27 -05:00
feat(donation): simplify donation client (#46379)
* feat: unify post payment actions * feat: handle stripe card error and add donation after auth * feat: add donation saga stripe test * feat: add more coverage to stripe tests * feat: add initial stripe card saga test * feat: finalize initial stripe card saga test * feat: add patreon test saga * feat: test clean up * feat: do not show processing for Patreon * feat: normalize donation settings * feat: turn payment provider/contex to enum * feat: remove donation-settings.js * fix: git ignore generated config * fix: ignore the generate config from everything * fix: remove types.js * fix: update linting to include types.js Co-authored-by: Mrugesh Mohapatra <1884376+raisedadead@users.noreply.github.com>
This commit is contained in:
@@ -5,6 +5,7 @@ api-server/src/public/**
|
|||||||
api-server/lib/**
|
api-server/lib/**
|
||||||
config/i18n.js
|
config/i18n.js
|
||||||
config/certification-settings.js
|
config/certification-settings.js
|
||||||
|
config/donation-settings.js
|
||||||
config/superblock-order.js
|
config/superblock-order.js
|
||||||
web/**
|
web/**
|
||||||
docs/**/*.md
|
docs/**/*.md
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -164,6 +164,7 @@ config/client/test-evaluator.json
|
|||||||
config/curriculum.json
|
config/curriculum.json
|
||||||
config/i18n.js
|
config/i18n.js
|
||||||
config/certification-settings.js
|
config/certification-settings.js
|
||||||
|
config/donation-settings.js
|
||||||
config/superblock-order.js
|
config/superblock-order.js
|
||||||
config/superblock-order.test.js
|
config/superblock-order.test.js
|
||||||
|
|
||||||
@@ -212,6 +213,7 @@ client/static/_redirects
|
|||||||
client/static/mobile
|
client/static/mobile
|
||||||
client/static/curriculum-data
|
client/static/curriculum-data
|
||||||
client/i18n/locales/**/trending.json
|
client/i18n/locales/**/trending.json
|
||||||
|
client/src/components/Donation/types.js
|
||||||
|
|
||||||
### UI Components ###
|
### UI Components ###
|
||||||
tools/ui-components/dist
|
tools/ui-components/dist
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ curriculum/challenges/**/*
|
|||||||
config/**/*.json
|
config/**/*.json
|
||||||
config/i18n.js
|
config/i18n.js
|
||||||
config/certification-settings.js
|
config/certification-settings.js
|
||||||
|
config/donation-settings.js
|
||||||
config/superblock-order.js
|
config/superblock-order.js
|
||||||
config/superblock-order.test.js
|
config/superblock-order.test.js
|
||||||
utils/block-nameify.js
|
utils/block-nameify.js
|
||||||
@@ -19,3 +20,4 @@ utils/index.js
|
|||||||
web/.next
|
web/.next
|
||||||
curriculum-server/data/curriculum.json
|
curriculum-server/data/curriculum.json
|
||||||
docs/**/*.md
|
docs/**/*.md
|
||||||
|
client/src/components/Donation/types.js
|
||||||
@@ -217,7 +217,7 @@ export async function createStripeCardDonation(req, res, stripe) {
|
|||||||
* if user is already donating and the donation isn't one time only,
|
* if user is already donating and the donation isn't one time only,
|
||||||
* throw error
|
* throw error
|
||||||
*/
|
*/
|
||||||
if (user.isDonating && duration !== 'onetime') {
|
if (user.isDonating && duration !== 'one-time') {
|
||||||
throw {
|
throw {
|
||||||
message: `User already has active recurring donation(s).`,
|
message: `User already has active recurring donation(s).`,
|
||||||
type: 'AlreadyDonatingError'
|
type: 'AlreadyDonatingError'
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export function validStripeForm(amount, duration, email) {
|
|||||||
return isEmail('' + email) &&
|
return isEmail('' + email) &&
|
||||||
isNumeric('' + amount) &&
|
isNumeric('' + amount) &&
|
||||||
durationKeysConfig.includes(duration) &&
|
durationKeysConfig.includes(duration) &&
|
||||||
duration === 'onetime'
|
duration === 'one-time'
|
||||||
? donationOneTimeConfig.includes(amount)
|
? donationOneTimeConfig.includes(amount)
|
||||||
: donationSubscriptionConfig.plans[duration];
|
: donationSubscriptionConfig.plans[duration];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -361,7 +361,6 @@
|
|||||||
"become-supporter": "Become a Supporter",
|
"become-supporter": "Become a Supporter",
|
||||||
"duration": "Become a one-time supporter of our charity.",
|
"duration": "Become a one-time supporter of our charity.",
|
||||||
"duration-2": "Become a monthly supporter of our charity.",
|
"duration-2": "Become a monthly supporter of our charity.",
|
||||||
"duration-3": "Become an annual supporter of our charity",
|
|
||||||
"duration-4": "Become a supporter of our charity",
|
"duration-4": "Become a supporter of our charity",
|
||||||
"nicely-done": "Nicely done. You just completed {{block}}.",
|
"nicely-done": "Nicely done. You just completed {{block}}.",
|
||||||
"credit-card": "Credit Card",
|
"credit-card": "Credit Card",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import certificateMissingMessage from '../utils/certificate-missing-message';
|
|||||||
import reallyWeirdErrorMessage from '../utils/really-weird-error-message';
|
import reallyWeirdErrorMessage from '../utils/really-weird-error-message';
|
||||||
import standardErrorMessage from '../utils/standard-error-message';
|
import standardErrorMessage from '../utils/standard-error-message';
|
||||||
|
|
||||||
|
import { PaymentContext } from '../../../config/donation-settings';
|
||||||
import ShowProjectLinks from './show-project-links';
|
import ShowProjectLinks from './show-project-links';
|
||||||
|
|
||||||
const { clientLocale } = envData;
|
const { clientLocale } = envData;
|
||||||
@@ -177,20 +178,7 @@ const ShowCertification = (props: ShowCertificationProps): JSX.Element => {
|
|||||||
setIsDonationClosed(true);
|
setIsDonationClosed(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleProcessing = (
|
const handleProcessing = () => {
|
||||||
duration: string,
|
|
||||||
amount: number,
|
|
||||||
action: string
|
|
||||||
) => {
|
|
||||||
props.executeGA({
|
|
||||||
type: 'event',
|
|
||||||
data: {
|
|
||||||
category: 'Donation',
|
|
||||||
action: `certificate ${action}`,
|
|
||||||
label: duration,
|
|
||||||
value: amount
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setIsDonationSubmitted(true);
|
setIsDonationSubmitted(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -270,6 +258,7 @@ const ShowCertification = (props: ShowCertificationProps): JSX.Element => {
|
|||||||
defaultTheme={Themes.Default}
|
defaultTheme={Themes.Default}
|
||||||
handleProcessing={handleProcessing}
|
handleProcessing={handleProcessing}
|
||||||
isMinimalForm={true}
|
isMinimalForm={true}
|
||||||
|
paymentContext={PaymentContext.Certificate}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -9,17 +9,11 @@ import { createSelector } from 'reselect';
|
|||||||
import {
|
import {
|
||||||
amountsConfig,
|
amountsConfig,
|
||||||
durationsConfig,
|
durationsConfig,
|
||||||
defaultAmount,
|
|
||||||
defaultDonation,
|
defaultDonation,
|
||||||
modalDefaultDonation
|
modalDefaultDonation
|
||||||
} from '../../../../config/donation-settings';
|
} from '../../../../config/donation-settings';
|
||||||
import { defaultDonationFormState } from '../../redux';
|
import { defaultDonationFormState } from '../../redux';
|
||||||
import {
|
import { updateDonationFormState, postCharge } from '../../redux/actions';
|
||||||
addDonation,
|
|
||||||
updateDonationFormState,
|
|
||||||
postChargeStripe,
|
|
||||||
postChargeStripeCard
|
|
||||||
} from '../../redux/actions';
|
|
||||||
import {
|
import {
|
||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
userSelector,
|
userSelector,
|
||||||
@@ -32,11 +26,19 @@ import Spacer from '../helpers/spacer';
|
|||||||
import { Themes } from '../settings/theme';
|
import { Themes } from '../settings/theme';
|
||||||
import DonateCompletion from './donate-completion';
|
import DonateCompletion from './donate-completion';
|
||||||
import PatreonButton from './patreon-button';
|
import PatreonButton from './patreon-button';
|
||||||
import type { AddDonationData } from './paypal-button';
|
|
||||||
import PaypalButton from './paypal-button';
|
import PaypalButton from './paypal-button';
|
||||||
import StripeCardForm, { HandleAuthentication } from './stripe-card-form';
|
import StripeCardForm from './stripe-card-form';
|
||||||
import WalletsWrapper from './walletsButton';
|
import WalletsWrapper from './walletsButton';
|
||||||
import SecurityLockIcon from './security-lock-icon';
|
import SecurityLockIcon from './security-lock-icon';
|
||||||
|
import {
|
||||||
|
PaymentProvider,
|
||||||
|
PaymentContext,
|
||||||
|
PostPayment,
|
||||||
|
HandleAuthentication,
|
||||||
|
DonationApprovalData,
|
||||||
|
DonationAmount,
|
||||||
|
DonationConfig
|
||||||
|
} from './types';
|
||||||
|
|
||||||
import './donation.css';
|
import './donation.css';
|
||||||
|
|
||||||
@@ -54,23 +56,26 @@ type DonateFormState = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type DonateFormComponentState = {
|
type DonateFormComponentState = DonationConfig;
|
||||||
donationAmount: number;
|
|
||||||
donationDuration: string;
|
type PostCharge = (data: {
|
||||||
};
|
paymentProvider: PaymentProvider;
|
||||||
|
paymentContext: PaymentContext;
|
||||||
|
amount: number;
|
||||||
|
duration: string;
|
||||||
|
data?: DonationApprovalData;
|
||||||
|
token?: Token;
|
||||||
|
email?: string;
|
||||||
|
name?: string | undefined;
|
||||||
|
paymentMethodId?: string;
|
||||||
|
handleAuthentication?: HandleAuthentication;
|
||||||
|
}) => void;
|
||||||
|
|
||||||
type DonateFormProps = {
|
type DonateFormProps = {
|
||||||
addDonation: (data: unknown) => unknown;
|
postCharge: PostCharge;
|
||||||
postChargeStripe: (data: unknown) => unknown;
|
|
||||||
postChargeStripeCard: (data: {
|
|
||||||
paymentMethodId: string;
|
|
||||||
amount: number;
|
|
||||||
duration: string;
|
|
||||||
handleAuthentication: HandleAuthentication;
|
|
||||||
}) => void;
|
|
||||||
defaultTheme?: Themes;
|
defaultTheme?: Themes;
|
||||||
email: string;
|
email: string;
|
||||||
handleProcessing: (duration: string, amount: number, action: string) => void;
|
handleProcessing?: () => void;
|
||||||
donationFormState: DonateFormState;
|
donationFormState: DonateFormState;
|
||||||
isMinimalForm?: boolean;
|
isMinimalForm?: boolean;
|
||||||
isSignedIn: boolean;
|
isSignedIn: boolean;
|
||||||
@@ -81,8 +86,9 @@ type DonateFormProps = {
|
|||||||
{ usd, hours }?: { usd?: string | number; hours?: string }
|
{ usd, hours }?: { usd?: string | number; hours?: string }
|
||||||
) => string;
|
) => string;
|
||||||
theme: Themes;
|
theme: Themes;
|
||||||
updateDonationFormState: (state: AddDonationData) => unknown;
|
updateDonationFormState: (state: DonationApprovalData) => unknown;
|
||||||
isVariantA: boolean;
|
isVariantA: boolean;
|
||||||
|
paymentContext: PaymentContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
@@ -111,10 +117,8 @@ const mapStateToProps = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
addDonation,
|
postCharge,
|
||||||
updateDonationFormState,
|
updateDonationFormState
|
||||||
postChargeStripe,
|
|
||||||
postChargeStripeCard
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
|
class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
|
||||||
@@ -124,27 +128,20 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
|
|||||||
constructor(props: DonateFormProps) {
|
constructor(props: DonateFormProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.durations = durationsConfig as {
|
this.durations = durationsConfig;
|
||||||
month: 'monthly';
|
|
||||||
onetime: 'one-time';
|
|
||||||
};
|
|
||||||
this.amounts = amountsConfig;
|
this.amounts = amountsConfig;
|
||||||
|
|
||||||
const initialAmountAndDuration = this.props.isMinimalForm
|
const initialAmountAndDuration: DonationConfig = this.props.isMinimalForm
|
||||||
? modalDefaultDonation
|
? modalDefaultDonation
|
||||||
: defaultDonation;
|
: defaultDonation;
|
||||||
|
|
||||||
this.state = { ...initialAmountAndDuration };
|
this.state = { ...initialAmountAndDuration };
|
||||||
|
|
||||||
this.onDonationStateChange = this.onDonationStateChange.bind(this);
|
this.onDonationStateChange = this.onDonationStateChange.bind(this);
|
||||||
this.getActiveDonationAmount = this.getActiveDonationAmount.bind(this);
|
|
||||||
this.getDonationButtonLabel = this.getDonationButtonLabel.bind(this);
|
this.getDonationButtonLabel = this.getDonationButtonLabel.bind(this);
|
||||||
this.handleSelectAmount = this.handleSelectAmount.bind(this);
|
this.handleSelectAmount = this.handleSelectAmount.bind(this);
|
||||||
this.handleSelectDuration = this.handleSelectDuration.bind(this);
|
|
||||||
this.resetDonation = this.resetDonation.bind(this);
|
this.resetDonation = this.resetDonation.bind(this);
|
||||||
this.postStripeDonation = this.postStripeDonation.bind(this);
|
this.postPayment = this.postPayment.bind(this);
|
||||||
this.postStripeCardDonation = this.postStripeCardDonation.bind(this);
|
|
||||||
this.postPatreonRedirect = this.postPatreonRedirect.bind(this);
|
|
||||||
this.handlePaymentButtonLoad = this.handlePaymentButtonLoad.bind(this);
|
this.handlePaymentButtonLoad = this.handlePaymentButtonLoad.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +149,7 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
|
|||||||
this.resetDonation();
|
this.resetDonation();
|
||||||
}
|
}
|
||||||
|
|
||||||
onDonationStateChange(donationState: AddDonationData) {
|
onDonationStateChange(donationState: DonationApprovalData) {
|
||||||
// scroll to top
|
// scroll to top
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
this.props.updateDonationFormState({
|
this.props.updateDonationFormState({
|
||||||
@@ -171,16 +168,6 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// onload
|
|
||||||
getActiveDonationAmount(
|
|
||||||
durationSelected: 'month' | 'onetime',
|
|
||||||
amountSelected: number
|
|
||||||
): number {
|
|
||||||
return this.amounts[durationSelected].includes(amountSelected)
|
|
||||||
? amountSelected
|
|
||||||
: defaultAmount[durationSelected] || this.amounts[durationSelected][0];
|
|
||||||
}
|
|
||||||
|
|
||||||
convertToTimeContributed(amount: number) {
|
convertToTimeContributed(amount: number) {
|
||||||
return numToCommas((amount / 100) * 50);
|
return numToCommas((amount / 100) * 50);
|
||||||
}
|
}
|
||||||
@@ -194,7 +181,7 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
|
|||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
const usd = this.getFormattedAmountLabel(donationAmount);
|
const usd = this.getFormattedAmountLabel(donationAmount);
|
||||||
let donationBtnLabel = t('donate.confirm');
|
let donationBtnLabel = t('donate.confirm');
|
||||||
if (donationDuration === 'onetime') {
|
if (donationDuration === 'one-time') {
|
||||||
donationBtnLabel = t('donate.confirm-2', {
|
donationBtnLabel = t('donate.confirm-2', {
|
||||||
usd: usd
|
usd: usd
|
||||||
});
|
});
|
||||||
@@ -209,62 +196,34 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
|
|||||||
return donationBtnLabel;
|
return donationBtnLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSelectDuration(donationDuration: 'month' | 'onetime') {
|
postPayment = ({
|
||||||
const donationAmount = this.getActiveDonationAmount(donationDuration, 0);
|
paymentProvider,
|
||||||
this.setState({ donationDuration, donationAmount });
|
data,
|
||||||
}
|
payerEmail,
|
||||||
|
payerName,
|
||||||
postStripeDonation(
|
token,
|
||||||
token: Token,
|
paymentMethodId,
|
||||||
payerEmail: string | undefined,
|
handleAuthentication
|
||||||
payerName: string | undefined
|
}: PostPayment): void => {
|
||||||
) {
|
|
||||||
const { email } = this.props;
|
|
||||||
const { donationAmount: amount, donationDuration: duration } = this.state;
|
const { donationAmount: amount, donationDuration: duration } = this.state;
|
||||||
payerEmail = email ? email : payerEmail;
|
const { paymentContext, email } = this.props;
|
||||||
window.scrollTo(0, 0);
|
|
||||||
// change the donation modal button label to close
|
this.props.postCharge({
|
||||||
// or display the close button for the cert donation section
|
paymentProvider,
|
||||||
if (this.props.handleProcessing) {
|
paymentContext,
|
||||||
this.props.handleProcessing(duration, amount, 'Stripe payment submition');
|
amount,
|
||||||
}
|
duration,
|
||||||
this.props.postChargeStripe({
|
data,
|
||||||
token,
|
token,
|
||||||
amount,
|
email: email || payerEmail,
|
||||||
duration,
|
name: payerName,
|
||||||
email: payerEmail,
|
|
||||||
name: payerName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
postStripeCardDonation(
|
|
||||||
paymentMethodId: string,
|
|
||||||
handleAuthentication: HandleAuthentication
|
|
||||||
) {
|
|
||||||
const { donationAmount: amount, donationDuration: duration } = this.state;
|
|
||||||
this.props.handleProcessing(
|
|
||||||
duration,
|
|
||||||
amount,
|
|
||||||
'Stripe card payment submission'
|
|
||||||
);
|
|
||||||
this.props.postChargeStripeCard({
|
|
||||||
paymentMethodId,
|
paymentMethodId,
|
||||||
amount,
|
|
||||||
duration,
|
|
||||||
handleAuthentication
|
handleAuthentication
|
||||||
});
|
});
|
||||||
}
|
if (this.props.handleProcessing) this.props.handleProcessing();
|
||||||
|
};
|
||||||
|
|
||||||
postPatreonRedirect() {
|
handleSelectAmount(donationAmount: DonationAmount) {
|
||||||
const { donationAmount: amount, donationDuration: duration } = this.state;
|
|
||||||
this.props.handleProcessing(
|
|
||||||
duration,
|
|
||||||
amount,
|
|
||||||
'Patreon payment redirection'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSelectAmount(donationAmount: number) {
|
|
||||||
this.setState({ donationAmount });
|
this.setState({ donationAmount });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +235,7 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
|
|||||||
|
|
||||||
let donationDescription = t('donate.your-donation-3', { usd, hours });
|
let donationDescription = t('donate.your-donation-3', { usd, hours });
|
||||||
|
|
||||||
if (donationDuration === 'onetime') {
|
if (donationDuration === 'one-time') {
|
||||||
donationDescription = t('donate.your-donation', { usd, hours });
|
donationDescription = t('donate.your-donation', { usd, hours });
|
||||||
} else if (donationDuration === 'month') {
|
} else if (donationDuration === 'month') {
|
||||||
donationDescription = t('donate.your-donation-2', { usd, hours });
|
donationDescription = t('donate.your-donation-2', { usd, hours });
|
||||||
@@ -316,8 +275,6 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
|
|||||||
const { donationAmount, donationDuration } = this.state;
|
const { donationAmount, donationDuration } = this.state;
|
||||||
const {
|
const {
|
||||||
donationFormState: { loading, processing },
|
donationFormState: { loading, processing },
|
||||||
handleProcessing,
|
|
||||||
addDonation,
|
|
||||||
defaultTheme,
|
defaultTheme,
|
||||||
theme,
|
theme,
|
||||||
t,
|
t,
|
||||||
@@ -327,7 +284,7 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
|
|||||||
isVariantA
|
isVariantA
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const priorityTheme = defaultTheme ? defaultTheme : theme;
|
const priorityTheme = defaultTheme ? defaultTheme : theme;
|
||||||
const isOneTime = donationDuration === 'onetime';
|
const isOneTime = donationDuration === 'one-time';
|
||||||
const walletlabel = `${t(
|
const walletlabel = `${t(
|
||||||
isOneTime ? 'donate.wallet-label' : 'donate.wallet-label-1',
|
isOneTime ? 'donate.wallet-label' : 'donate.wallet-label-1',
|
||||||
{ usd: donationAmount / 100 }
|
{ usd: donationAmount / 100 }
|
||||||
@@ -352,31 +309,29 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
|
|||||||
handlePaymentButtonLoad={this.handlePaymentButtonLoad}
|
handlePaymentButtonLoad={this.handlePaymentButtonLoad}
|
||||||
label={walletlabel}
|
label={walletlabel}
|
||||||
onDonationStateChange={this.onDonationStateChange}
|
onDonationStateChange={this.onDonationStateChange}
|
||||||
postStripeDonation={this.postStripeDonation}
|
postPayment={this.postPayment}
|
||||||
refreshErrorMessage={t('donate.refresh-needed')}
|
refreshErrorMessage={t('donate.refresh-needed')}
|
||||||
theme={priorityTheme}
|
theme={priorityTheme}
|
||||||
/>
|
/>
|
||||||
<PaypalButton
|
<PaypalButton
|
||||||
addDonation={addDonation}
|
|
||||||
donationAmount={donationAmount}
|
donationAmount={donationAmount}
|
||||||
donationDuration={donationDuration}
|
donationDuration={donationDuration}
|
||||||
handlePaymentButtonLoad={this.handlePaymentButtonLoad}
|
handlePaymentButtonLoad={this.handlePaymentButtonLoad}
|
||||||
handleProcessing={handleProcessing}
|
postPayment={this.postPayment}
|
||||||
isMinimalForm={showMinimalPayments}
|
isMinimalForm={showMinimalPayments}
|
||||||
isPaypalLoading={loading.paypal}
|
isPaypalLoading={loading.paypal}
|
||||||
isSignedIn={isSignedIn}
|
|
||||||
onDonationStateChange={this.onDonationStateChange}
|
onDonationStateChange={this.onDonationStateChange}
|
||||||
theme={priorityTheme}
|
theme={priorityTheme}
|
||||||
/>
|
/>
|
||||||
{(!loading.stripe || !loading.paypal) && (
|
{(!loading.stripe || !loading.paypal) && (
|
||||||
<PatreonButton postPatreonRedirect={this.postPatreonRedirect} />
|
<PatreonButton postPayment={this.postPayment} />
|
||||||
)}
|
)}
|
||||||
{showMinimalPayments && (
|
{showMinimalPayments && (
|
||||||
<>
|
<>
|
||||||
<div className='separator'>{t('donate.or-card')}</div>
|
<div className='separator'>{t('donate.or-card')}</div>
|
||||||
<StripeCardForm
|
<StripeCardForm
|
||||||
onDonationStateChange={this.onDonationStateChange}
|
onDonationStateChange={this.onDonationStateChange}
|
||||||
postStripeCardDonation={this.postStripeCardDonation}
|
postPayment={this.postPayment}
|
||||||
processing={processing}
|
processing={processing}
|
||||||
t={t}
|
t={t}
|
||||||
theme={priorityTheme}
|
theme={priorityTheme}
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import { connect } from 'react-redux';
|
|||||||
import { goToAnchor } from 'react-scrollable-anchor';
|
import { goToAnchor } from 'react-scrollable-anchor';
|
||||||
import { bindActionCreators, Dispatch, AnyAction } from 'redux';
|
import { bindActionCreators, Dispatch, AnyAction } from 'redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { modalDefaultDonation } from '../../../../config/donation-settings';
|
import {
|
||||||
|
modalDefaultDonation,
|
||||||
|
PaymentContext
|
||||||
|
} from '../../../../config/donation-settings';
|
||||||
import Cup from '../../assets/icons/cup';
|
import Cup from '../../assets/icons/cup';
|
||||||
import Heart from '../../assets/icons/heart';
|
import Heart from '../../assets/icons/heart';
|
||||||
|
|
||||||
@@ -56,20 +59,7 @@ function DonateModal({
|
|||||||
}: DonateModalProps): JSX.Element {
|
}: DonateModalProps): JSX.Element {
|
||||||
const [closeLabel, setCloseLabel] = React.useState(false);
|
const [closeLabel, setCloseLabel] = React.useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const handleProcessing = (
|
const handleProcessing = () => {
|
||||||
duration: string,
|
|
||||||
amount: number,
|
|
||||||
action: string
|
|
||||||
) => {
|
|
||||||
executeGA({
|
|
||||||
type: 'event',
|
|
||||||
data: {
|
|
||||||
category: 'Donation',
|
|
||||||
action: `Modal ${action}`,
|
|
||||||
label: duration,
|
|
||||||
value: amount
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setCloseLabel(true);
|
setCloseLabel(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,12 +83,10 @@ function DonateModal({
|
|||||||
const getDonationText = () => {
|
const getDonationText = () => {
|
||||||
const donationDuration = modalDefaultDonation.donationDuration;
|
const donationDuration = modalDefaultDonation.donationDuration;
|
||||||
switch (donationDuration) {
|
switch (donationDuration) {
|
||||||
case 'onetime':
|
case 'one-time':
|
||||||
return <b>{t('donate.duration')}</b>;
|
return <b>{t('donate.duration')}</b>;
|
||||||
case 'month':
|
case 'month':
|
||||||
return <b>{t('donate.duration-2')}</b>;
|
return <b>{t('donate.duration-2')}</b>;
|
||||||
case 'year':
|
|
||||||
return <b>{t('donate.duration-3')}</b>;
|
|
||||||
default:
|
default:
|
||||||
return <b>{t('donate.duration-4')}</b>;
|
return <b>{t('donate.duration-4')}</b>;
|
||||||
}
|
}
|
||||||
@@ -158,6 +146,7 @@ function DonateModal({
|
|||||||
<DonateForm
|
<DonateForm
|
||||||
handleProcessing={handleProcessing}
|
handleProcessing={handleProcessing}
|
||||||
isMinimalForm={true}
|
isMinimalForm={true}
|
||||||
|
paymentContext={PaymentContext.Modal}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
donationUrls,
|
donationUrls,
|
||||||
patreonDefaultPledgeAmount
|
patreonDefaultPledgeAmount,
|
||||||
|
PaymentProvider
|
||||||
} from '../../../../config/donation-settings';
|
} from '../../../../config/donation-settings';
|
||||||
import envData from '../../../../config/env.json';
|
import envData from '../../../../config/env.json';
|
||||||
import PatreonLogo from '../../assets/images/components/patreon-logo';
|
import PatreonLogo from '../../assets/images/components/patreon-logo';
|
||||||
|
import { PostPayment } from './types';
|
||||||
|
|
||||||
const { patreonClientId }: { patreonClientId: string | null } = envData as {
|
const { patreonClientId }: { patreonClientId: string | null } = envData as {
|
||||||
patreonClientId: string | null;
|
patreonClientId: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface PatreonButtonProps {
|
interface PatreonButtonProps {
|
||||||
postPatreonRedirect: () => void;
|
postPayment: (arg0: PostPayment) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PatreonButton = ({
|
const PatreonButton = ({
|
||||||
postPatreonRedirect
|
postPayment
|
||||||
}: PatreonButtonProps): JSX.Element | null => {
|
}: PatreonButtonProps): JSX.Element | null => {
|
||||||
if (
|
if (
|
||||||
!patreonClientId ||
|
!patreonClientId ||
|
||||||
@@ -36,7 +38,7 @@ const PatreonButton = ({
|
|||||||
className='patreon-button link-button'
|
className='patreon-button link-button'
|
||||||
data-patreon-widget-type='become-patron-button'
|
data-patreon-widget-type='become-patron-button'
|
||||||
href={href}
|
href={href}
|
||||||
onClick={postPatreonRedirect}
|
onClick={() => postPayment({ paymentProvider: PaymentProvider.Patreon })}
|
||||||
rel='noreferrer'
|
rel='noreferrer'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import ReactDOM from 'react-dom';
|
|||||||
|
|
||||||
import { scriptLoader, scriptRemover } from '../../utils/script-loaders';
|
import { scriptLoader, scriptRemover } from '../../utils/script-loaders';
|
||||||
|
|
||||||
import type { AddDonationData } from './paypal-button';
|
import type { DonationApprovalData } from './types';
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
type PayPalButtonScriptLoaderProps = {
|
type PayPalButtonScriptLoaderProps = {
|
||||||
@@ -30,7 +30,7 @@ type PayPalButtonScriptLoaderProps = {
|
|||||||
) => unknown;
|
) => unknown;
|
||||||
isSubscription: boolean;
|
isSubscription: boolean;
|
||||||
onApprove: (
|
onApprove: (
|
||||||
data: AddDonationData,
|
data: DonationApprovalData,
|
||||||
actions?: { order: { capture: () => Promise<unknown> } }
|
actions?: { order: { capture: () => Promise<unknown> } }
|
||||||
) => unknown;
|
) => unknown;
|
||||||
isPaypalLoading: boolean;
|
isPaypalLoading: boolean;
|
||||||
@@ -66,7 +66,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PayPalButtonScriptLoader extends Component<
|
export default class PayPalButtonScriptLoader extends Component<
|
||||||
PayPalButtonScriptLoaderProps,
|
PayPalButtonScriptLoaderProps,
|
||||||
PayPalButtonScriptLoaderState
|
PayPalButtonScriptLoaderState
|
||||||
> {
|
> {
|
||||||
@@ -188,7 +188,7 @@ export class PayPalButtonScriptLoader extends Component<
|
|||||||
onApprove={
|
onApprove={
|
||||||
isSubscription
|
isSubscription
|
||||||
? (
|
? (
|
||||||
data: AddDonationData,
|
data: DonationApprovalData,
|
||||||
actions: { order: { capture: () => Promise<unknown> } }
|
actions: { order: { capture: () => Promise<unknown> } }
|
||||||
) => onApprove(data, actions)
|
) => onApprove(data, actions)
|
||||||
: (
|
: (
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
import { render } from '@testing-library/react';
|
|
||||||
import React from 'react';
|
|
||||||
import { Themes } from '../settings/theme';
|
|
||||||
|
|
||||||
import { PaypalButton } from './paypal-button';
|
|
||||||
|
|
||||||
const commonProps = {
|
|
||||||
donationAmount: 500,
|
|
||||||
donationDuration: 'month',
|
|
||||||
handleProcessing: () => null,
|
|
||||||
isDonating: false,
|
|
||||||
onDonationStateChange: () => null,
|
|
||||||
isPaypalLoading: true,
|
|
||||||
t: jest.fn(),
|
|
||||||
theme: Themes.Night,
|
|
||||||
handlePaymentButtonLoad: jest.fn(),
|
|
||||||
isMinimalForm: true
|
|
||||||
};
|
|
||||||
|
|
||||||
const donationData = {
|
|
||||||
redirecting: false,
|
|
||||||
processing: false,
|
|
||||||
success: false,
|
|
||||||
error: null
|
|
||||||
};
|
|
||||||
|
|
||||||
jest.mock('../../analytics');
|
|
||||||
|
|
||||||
describe('<Paypal Button/>', () => {
|
|
||||||
it('does not call addDonate api on payment approval when user is not signed ', () => {
|
|
||||||
const ref = React.createRef<PaypalButton>();
|
|
||||||
const isSubscription = true;
|
|
||||||
const addDonation = jest.fn();
|
|
||||||
render(
|
|
||||||
<PaypalButton
|
|
||||||
{...commonProps}
|
|
||||||
addDonation={addDonation}
|
|
||||||
isSignedIn={false}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
ref.current?.handleApproval(donationData, isSubscription);
|
|
||||||
expect(addDonation).toBeCalledTimes(0);
|
|
||||||
});
|
|
||||||
it('calls addDonate api on payment approval when user is signed in', () => {
|
|
||||||
const ref = React.createRef<PaypalButton>();
|
|
||||||
const isSubscription = true;
|
|
||||||
const addDonation = jest.fn();
|
|
||||||
render(
|
|
||||||
<PaypalButton
|
|
||||||
{...commonProps}
|
|
||||||
addDonation={addDonation}
|
|
||||||
isSignedIn={true}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
ref.current?.handleApproval(donationData, isSubscription);
|
|
||||||
expect(addDonation).toBeCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -5,23 +5,23 @@ import { createSelector } from 'reselect';
|
|||||||
import {
|
import {
|
||||||
paypalConfigurator,
|
paypalConfigurator,
|
||||||
paypalConfigTypes,
|
paypalConfigTypes,
|
||||||
defaultDonation
|
defaultDonation,
|
||||||
|
PaymentProvider
|
||||||
} from '../../../../config/donation-settings';
|
} from '../../../../config/donation-settings';
|
||||||
import envData from '../../../../config/env.json';
|
import envData from '../../../../config/env.json';
|
||||||
import { userSelector, signInLoadingSelector } from '../../redux/selectors';
|
import { userSelector, signInLoadingSelector } from '../../redux/selectors';
|
||||||
import { Themes } from '../settings/theme';
|
import { Themes } from '../settings/theme';
|
||||||
import { PayPalButtonScriptLoader } from './paypal-button-script-loader';
|
import {
|
||||||
|
DonationApprovalData,
|
||||||
|
PostPayment,
|
||||||
|
DonationDuration,
|
||||||
|
DonationAmount
|
||||||
|
} from './types';
|
||||||
|
import PayPalButtonScriptLoader from './paypal-button-script-loader';
|
||||||
|
|
||||||
type PaypalButtonProps = {
|
type PaypalButtonProps = {
|
||||||
addDonation: (data: AddDonationData) => void;
|
donationAmount: DonationAmount;
|
||||||
isSignedIn: boolean;
|
donationDuration: DonationDuration;
|
||||||
donationAmount: number;
|
|
||||||
donationDuration: string;
|
|
||||||
handleProcessing: (
|
|
||||||
duration: string,
|
|
||||||
amount: number,
|
|
||||||
action: string
|
|
||||||
) => unknown;
|
|
||||||
isDonating: boolean;
|
isDonating: boolean;
|
||||||
onDonationStateChange: ({
|
onDonationStateChange: ({
|
||||||
redirecting,
|
redirecting,
|
||||||
@@ -35,13 +35,13 @@ type PaypalButtonProps = {
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
}) => void;
|
}) => void;
|
||||||
isPaypalLoading: boolean;
|
isPaypalLoading: boolean;
|
||||||
skipAddDonation?: boolean;
|
|
||||||
t: (label: string) => string;
|
t: (label: string) => string;
|
||||||
ref?: Ref<PaypalButton>;
|
ref?: Ref<PaypalButton>;
|
||||||
theme: Themes;
|
theme: Themes;
|
||||||
isSubscription?: boolean;
|
isSubscription?: boolean;
|
||||||
handlePaymentButtonLoad: (provider: 'stripe' | 'paypal') => void;
|
handlePaymentButtonLoad: (provider: 'stripe' | 'paypal') => void;
|
||||||
isMinimalForm: boolean | undefined;
|
isMinimalForm: boolean | undefined;
|
||||||
|
postPayment: (arg0: PostPayment) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PaypalButtonState = {
|
type PaypalButtonState = {
|
||||||
@@ -50,17 +50,6 @@ type PaypalButtonState = {
|
|||||||
planId: string | null;
|
planId: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface AddDonationData {
|
|
||||||
redirecting: boolean;
|
|
||||||
processing: boolean;
|
|
||||||
success: boolean;
|
|
||||||
error: string | null;
|
|
||||||
loading?: {
|
|
||||||
stripe: boolean;
|
|
||||||
paypal: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
paypalClientId,
|
paypalClientId,
|
||||||
deploymentEnv
|
deploymentEnv
|
||||||
@@ -82,7 +71,6 @@ export class PaypalButton extends Component<
|
|||||||
};
|
};
|
||||||
constructor(props: PaypalButtonProps) {
|
constructor(props: PaypalButtonProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.handleApproval = this.handleApproval.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(
|
static getDerivedStateFromProps(
|
||||||
@@ -90,8 +78,8 @@ export class PaypalButton extends Component<
|
|||||||
): PaypalButtonState {
|
): PaypalButtonState {
|
||||||
const { donationAmount, donationDuration } = props;
|
const { donationAmount, donationDuration } = props;
|
||||||
const configurationObj: {
|
const configurationObj: {
|
||||||
amount: number;
|
amount: DonationAmount;
|
||||||
duration: string;
|
duration: DonationDuration;
|
||||||
planId: string | null;
|
planId: string | null;
|
||||||
} = paypalConfigurator(
|
} = paypalConfigurator(
|
||||||
donationAmount,
|
donationAmount,
|
||||||
@@ -105,30 +93,10 @@ export class PaypalButton extends Component<
|
|||||||
return { ...configurationObj };
|
return { ...configurationObj };
|
||||||
}
|
}
|
||||||
|
|
||||||
handleApproval = (data: AddDonationData, isSubscription: boolean): void => {
|
|
||||||
const { amount, duration } = this.state;
|
|
||||||
const { isSignedIn = false } = this.props;
|
|
||||||
|
|
||||||
// If the user is signed in and the payment is subscritipn call the api
|
|
||||||
if (isSignedIn && isSubscription) {
|
|
||||||
this.props.addDonation(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.handleProcessing(duration, amount, 'Paypal payment submission');
|
|
||||||
|
|
||||||
// Show success anytime because the payment has gone through paypal
|
|
||||||
this.props.onDonationStateChange({
|
|
||||||
redirecting: false,
|
|
||||||
processing: false,
|
|
||||||
success: true,
|
|
||||||
error: data.error ? data.error : null
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render(): JSX.Element | null {
|
render(): JSX.Element | null {
|
||||||
const { duration, planId, amount } = this.state;
|
const { duration, planId, amount } = this.state;
|
||||||
const { t, theme, isPaypalLoading, isMinimalForm } = this.props;
|
const { t, theme, isPaypalLoading, isMinimalForm } = this.props;
|
||||||
const isSubscription = duration !== 'onetime';
|
const isSubscription = duration !== 'one-time';
|
||||||
const buttonColor = theme === Themes.Night ? 'white' : 'gold';
|
const buttonColor = theme === Themes.Night ? 'white' : 'gold';
|
||||||
if (!paypalClientId) {
|
if (!paypalClientId) {
|
||||||
return null;
|
return null;
|
||||||
@@ -177,8 +145,11 @@ export class PaypalButton extends Component<
|
|||||||
isMinimalForm={isMinimalForm}
|
isMinimalForm={isMinimalForm}
|
||||||
isPaypalLoading={isPaypalLoading}
|
isPaypalLoading={isPaypalLoading}
|
||||||
isSubscription={isSubscription}
|
isSubscription={isSubscription}
|
||||||
onApprove={(data: AddDonationData) => {
|
onApprove={(data: DonationApprovalData) => {
|
||||||
this.handleApproval(data, isSubscription);
|
this.props.postPayment({
|
||||||
|
paymentProvider: PaymentProvider.Paypal,
|
||||||
|
data
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
this.props.onDonationStateChange({
|
this.props.onDonationStateChange({
|
||||||
|
|||||||
@@ -9,29 +9,21 @@ import {
|
|||||||
import { loadStripe } from '@stripe/stripe-js';
|
import { loadStripe } from '@stripe/stripe-js';
|
||||||
import type {
|
import type {
|
||||||
StripeCardNumberElementChangeEvent,
|
StripeCardNumberElementChangeEvent,
|
||||||
StripeCardExpiryElementChangeEvent,
|
StripeCardExpiryElementChangeEvent
|
||||||
PaymentIntentResult
|
|
||||||
} from '@stripe/stripe-js';
|
} from '@stripe/stripe-js';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { PaymentProvider } from '../../../../config/donation-settings';
|
||||||
import envData from '../../../../config/env.json';
|
import envData from '../../../../config/env.json';
|
||||||
import { Themes } from '../settings/theme';
|
import { Themes } from '../settings/theme';
|
||||||
import { AddDonationData } from './paypal-button';
|
import { DonationApprovalData, PostPayment } from './types';
|
||||||
import SecurityLockIcon from './security-lock-icon';
|
import SecurityLockIcon from './security-lock-icon';
|
||||||
|
|
||||||
const { stripePublicKey }: { stripePublicKey: string | null } = envData;
|
const { stripePublicKey }: { stripePublicKey: string | null } = envData;
|
||||||
|
|
||||||
export type HandleAuthentication = (
|
|
||||||
clientSecret: string,
|
|
||||||
paymentMethod: string
|
|
||||||
) => Promise<PaymentIntentResult | { error: { type: string } }>;
|
|
||||||
|
|
||||||
interface FormPropTypes {
|
interface FormPropTypes {
|
||||||
onDonationStateChange: (donationState: AddDonationData) => void;
|
onDonationStateChange: (donationState: DonationApprovalData) => void;
|
||||||
postStripeCardDonation: (
|
postPayment: (arg0: PostPayment) => void;
|
||||||
paymentMethodId: string,
|
|
||||||
handleAuthentication: HandleAuthentication
|
|
||||||
) => void;
|
|
||||||
t: (label: string) => string;
|
t: (label: string) => string;
|
||||||
theme: Themes;
|
theme: Themes;
|
||||||
processing: boolean;
|
processing: boolean;
|
||||||
@@ -50,7 +42,7 @@ const StripeCardForm = ({
|
|||||||
theme,
|
theme,
|
||||||
t,
|
t,
|
||||||
onDonationStateChange,
|
onDonationStateChange,
|
||||||
postStripeCardDonation,
|
postPayment,
|
||||||
processing,
|
processing,
|
||||||
isVariantA
|
isVariantA
|
||||||
}: FormPropTypes): JSX.Element => {
|
}: FormPropTypes): JSX.Element => {
|
||||||
@@ -124,7 +116,11 @@ const StripeCardForm = ({
|
|||||||
error: t('donate.went-wrong')
|
error: t('donate.went-wrong')
|
||||||
});
|
});
|
||||||
} else if (paymentMethod)
|
} else if (paymentMethod)
|
||||||
postStripeCardDonation(paymentMethod.id, handleAuthentication);
|
postPayment({
|
||||||
|
paymentProvider: PaymentProvider.StripeCard,
|
||||||
|
paymentMethodId: paymentMethod.id,
|
||||||
|
handleAuthentication
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return setTokenizing(false);
|
return setTokenizing(false);
|
||||||
|
|||||||
37
client/src/components/Donation/types.ts
Normal file
37
client/src/components/Donation/types.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type { Token, PaymentIntentResult } from '@stripe/stripe-js';
|
||||||
|
|
||||||
|
export type PaymentContext = 'modal' | 'donate page' | 'certificate';
|
||||||
|
export type PaymentProvider = 'patreon' | 'paypal' | 'stripe' | 'stripe card';
|
||||||
|
|
||||||
|
export type HandleAuthentication = (
|
||||||
|
clientSecret: string,
|
||||||
|
paymentMethod: string
|
||||||
|
) => Promise<PaymentIntentResult | { error: { type: string } }>;
|
||||||
|
|
||||||
|
export type DonationAmount = 500 | 1000 | 2000 | 3000 | 4000 | 5000;
|
||||||
|
export type DonationDuration = 'one-time' | 'month';
|
||||||
|
export interface DonationConfig {
|
||||||
|
donationAmount: DonationAmount;
|
||||||
|
donationDuration: DonationDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostPayment {
|
||||||
|
paymentProvider: PaymentProvider;
|
||||||
|
data?: DonationApprovalData;
|
||||||
|
token?: Token;
|
||||||
|
payerEmail?: string | undefined;
|
||||||
|
payerName?: string | undefined;
|
||||||
|
paymentMethodId?: string;
|
||||||
|
handleAuthentication?: HandleAuthentication;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DonationApprovalData {
|
||||||
|
redirecting: boolean;
|
||||||
|
processing: boolean;
|
||||||
|
success: boolean;
|
||||||
|
error: string | null;
|
||||||
|
loading?: {
|
||||||
|
stripe: boolean;
|
||||||
|
paypal: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -8,7 +8,8 @@ import type { Token, PaymentRequest } from '@stripe/stripe-js';
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import envData from '../../../../config/env.json';
|
import envData from '../../../../config/env.json';
|
||||||
import { Themes } from '../settings/theme';
|
import { Themes } from '../settings/theme';
|
||||||
import { AddDonationData } from './paypal-button';
|
import { PaymentProvider } from '../../../../config/donation-settings';
|
||||||
|
import { DonationApprovalData, PostPayment } from './types';
|
||||||
|
|
||||||
const { stripePublicKey }: { stripePublicKey: string | null } = envData;
|
const { stripePublicKey }: { stripePublicKey: string | null } = envData;
|
||||||
|
|
||||||
@@ -16,12 +17,8 @@ interface WrapperProps {
|
|||||||
label: string;
|
label: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
theme: Themes;
|
theme: Themes;
|
||||||
postStripeDonation: (
|
postPayment: (arg0: PostPayment) => void;
|
||||||
token: Token,
|
onDonationStateChange: (donationState: DonationApprovalData) => void;
|
||||||
payerEmail: string | undefined,
|
|
||||||
payerName: string | undefined
|
|
||||||
) => void;
|
|
||||||
onDonationStateChange: (donationState: AddDonationData) => void;
|
|
||||||
refreshErrorMessage: string;
|
refreshErrorMessage: string;
|
||||||
handlePaymentButtonLoad: (provider: 'stripe' | 'paypal') => void;
|
handlePaymentButtonLoad: (provider: 'stripe' | 'paypal') => void;
|
||||||
}
|
}
|
||||||
@@ -35,7 +32,7 @@ const WalletsButton = ({
|
|||||||
amount,
|
amount,
|
||||||
theme,
|
theme,
|
||||||
refreshErrorMessage,
|
refreshErrorMessage,
|
||||||
postStripeDonation,
|
postPayment,
|
||||||
onDonationStateChange,
|
onDonationStateChange,
|
||||||
handlePaymentButtonLoad
|
handlePaymentButtonLoad
|
||||||
}: WalletsButtonProps) => {
|
}: WalletsButtonProps) => {
|
||||||
@@ -63,7 +60,12 @@ const WalletsButton = ({
|
|||||||
const { token, payerEmail, payerName } = event;
|
const { token, payerEmail, payerName } = event;
|
||||||
setToken(token);
|
setToken(token);
|
||||||
event.complete('success');
|
event.complete('success');
|
||||||
postStripeDonation(token, payerEmail, payerName);
|
postPayment({
|
||||||
|
paymentProvider: PaymentProvider.Stripe,
|
||||||
|
token,
|
||||||
|
payerEmail,
|
||||||
|
payerName
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
void pr.canMakePayment().then(canMakePaymentRes => {
|
void pr.canMakePayment().then(canMakePaymentRes => {
|
||||||
@@ -74,7 +76,7 @@ const WalletsButton = ({
|
|||||||
checkpaymentPossiblity(false);
|
checkpaymentPossiblity(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [label, amount, stripe, postStripeDonation, handlePaymentButtonLoad]);
|
}, [label, amount, stripe, postPayment, handlePaymentButtonLoad]);
|
||||||
|
|
||||||
const displayRefreshError = (): void => {
|
const displayRefreshError = (): void => {
|
||||||
onDonationStateChange({
|
onDonationStateChange({
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { Spacer, Loader } from '../components/helpers';
|
|||||||
import CampersImage from '../components/landing/components/campers-image';
|
import CampersImage from '../components/landing/components/campers-image';
|
||||||
import { executeGA } from '../redux/actions';
|
import { executeGA } from '../redux/actions';
|
||||||
import { signInLoadingSelector, userSelector } from '../redux/selectors';
|
import { signInLoadingSelector, userSelector } from '../redux/selectors';
|
||||||
|
import { PaymentContext } from '../../../config/donation-settings';
|
||||||
|
|
||||||
export interface ExecuteGaArg {
|
export interface ExecuteGaArg {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -68,18 +69,6 @@ function DonatePage({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function handleProcessing(duration: string, amount: number, action: string) {
|
|
||||||
executeGA({
|
|
||||||
type: 'event',
|
|
||||||
data: {
|
|
||||||
category: 'Donation',
|
|
||||||
action: `donate page ${action}`,
|
|
||||||
label: duration,
|
|
||||||
value: amount
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return showLoading ? (
|
return showLoading ? (
|
||||||
<Loader fullScreen={true} />
|
<Loader fullScreen={true} />
|
||||||
) : (
|
) : (
|
||||||
@@ -110,7 +99,7 @@ function DonatePage({
|
|||||||
<DonationText />
|
<DonationText />
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs={12}>
|
<Col xs={12}>
|
||||||
<DonateForm handleProcessing={handleProcessing} />
|
<DonateForm paymentContext={PaymentContext.DonatePage} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Spacer size={3} />
|
<Spacer size={3} />
|
||||||
|
|||||||
@@ -27,15 +27,13 @@ export const actionTypes = createTypes(
|
|||||||
'updateFailed',
|
'updateFailed',
|
||||||
'updateDonationFormState',
|
'updateDonationFormState',
|
||||||
'updateUserToken',
|
'updateUserToken',
|
||||||
|
'postChargeProcessing',
|
||||||
...createAsyncTypes('fetchUser'),
|
...createAsyncTypes('fetchUser'),
|
||||||
...createAsyncTypes('addDonation'),
|
...createAsyncTypes('postCharge'),
|
||||||
...createAsyncTypes('createStripeSession'),
|
|
||||||
...createAsyncTypes('postChargeStripe'),
|
|
||||||
...createAsyncTypes('fetchProfileForUser'),
|
...createAsyncTypes('fetchProfileForUser'),
|
||||||
...createAsyncTypes('acceptTerms'),
|
...createAsyncTypes('acceptTerms'),
|
||||||
...createAsyncTypes('showCert'),
|
...createAsyncTypes('showCert'),
|
||||||
...createAsyncTypes('reportUser'),
|
...createAsyncTypes('reportUser'),
|
||||||
...createAsyncTypes('postChargeStripeCard'),
|
|
||||||
...createAsyncTypes('deleteUserToken'),
|
...createAsyncTypes('deleteUserToken'),
|
||||||
...createAsyncTypes('saveChallenge')
|
...createAsyncTypes('saveChallenge')
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -53,28 +53,12 @@ export const fetchUser = createAction(actionTypes.fetchUser);
|
|||||||
export const fetchUserComplete = createAction(actionTypes.fetchUserComplete);
|
export const fetchUserComplete = createAction(actionTypes.fetchUserComplete);
|
||||||
export const fetchUserError = createAction(actionTypes.fetchUserError);
|
export const fetchUserError = createAction(actionTypes.fetchUserError);
|
||||||
|
|
||||||
export const addDonation = createAction(actionTypes.addDonation);
|
export const postCharge = createAction(actionTypes.postCharge);
|
||||||
export const addDonationComplete = createAction(
|
export const postChargeProcessing = createAction(
|
||||||
actionTypes.addDonationComplete
|
actionTypes.postChargeProcessing
|
||||||
);
|
|
||||||
export const addDonationError = createAction(actionTypes.addDonationError);
|
|
||||||
|
|
||||||
export const postChargeStripe = createAction(actionTypes.postChargeStripe);
|
|
||||||
export const postChargeStripeComplete = createAction(
|
|
||||||
actionTypes.postChargeStripeComplete
|
|
||||||
);
|
|
||||||
export const postChargeStripeError = createAction(
|
|
||||||
actionTypes.postChargeStripeError
|
|
||||||
);
|
|
||||||
export const postChargeStripeCard = createAction(
|
|
||||||
actionTypes.postChargeStripeCard
|
|
||||||
);
|
|
||||||
export const postChargeStripeCardComplete = createAction(
|
|
||||||
actionTypes.postChargeStripeCardComplete
|
|
||||||
);
|
|
||||||
export const postChargeStripeCardError = createAction(
|
|
||||||
actionTypes.postChargeStripeCardError
|
|
||||||
);
|
);
|
||||||
|
export const postChargeComplete = createAction(actionTypes.postChargeComplete);
|
||||||
|
export const postChargeError = createAction(actionTypes.postChargeError);
|
||||||
|
|
||||||
export const fetchProfileForUser = createAction(
|
export const fetchProfileForUser = createAction(
|
||||||
actionTypes.fetchProfileForUser
|
actionTypes.fetchProfileForUser
|
||||||
|
|||||||
@@ -14,22 +14,23 @@ import {
|
|||||||
postChargeStripe,
|
postChargeStripe,
|
||||||
postChargeStripeCard
|
postChargeStripeCard
|
||||||
} from '../utils/ajax';
|
} from '../utils/ajax';
|
||||||
|
import { stringifyDonationEvents } from '../utils/analyticsStrings';
|
||||||
|
import { PaymentProvider } from '../../../config/donation-settings';
|
||||||
import { actionTypes as appTypes } from './action-types';
|
import { actionTypes as appTypes } from './action-types';
|
||||||
import {
|
import {
|
||||||
addDonationComplete,
|
|
||||||
addDonationError,
|
|
||||||
openDonationModal,
|
openDonationModal,
|
||||||
postChargeStripeCardComplete,
|
postChargeComplete,
|
||||||
postChargeStripeCardError,
|
postChargeProcessing,
|
||||||
postChargeStripeComplete,
|
postChargeError,
|
||||||
postChargeStripeError,
|
|
||||||
preventBlockDonationRequests,
|
preventBlockDonationRequests,
|
||||||
preventProgressDonationRequests
|
preventProgressDonationRequests,
|
||||||
|
executeGA
|
||||||
} from './actions';
|
} from './actions';
|
||||||
import {
|
import {
|
||||||
isDonatingSelector,
|
isDonatingSelector,
|
||||||
recentlyClaimedBlockSelector,
|
recentlyClaimedBlockSelector,
|
||||||
shouldRequestDonationSelector
|
shouldRequestDonationSelector,
|
||||||
|
isSignedInSelector
|
||||||
} from './selectors';
|
} from './selectors';
|
||||||
|
|
||||||
const defaultDonationErrorMessage = i18next.t('donate.error-2');
|
const defaultDonationErrorMessage = i18next.t('donate.error-2');
|
||||||
@@ -49,33 +50,77 @@ function* showDonateModalSaga() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function* addDonationSaga({ payload }) {
|
export function* postChargeSaga({
|
||||||
try {
|
payload,
|
||||||
yield call(addDonation, payload);
|
payload: {
|
||||||
yield put(addDonationComplete());
|
paymentProvider,
|
||||||
yield call(setDonationCookie);
|
paymentContext,
|
||||||
} catch (error) {
|
amount,
|
||||||
const data =
|
duration,
|
||||||
error.response && error.response.data
|
handleAuthentication,
|
||||||
? error.response.data
|
paymentMethodId
|
||||||
: {
|
|
||||||
message: defaultDonationErrorMessage
|
|
||||||
};
|
|
||||||
yield put(addDonationError(data.message));
|
|
||||||
}
|
}
|
||||||
}
|
}) {
|
||||||
|
|
||||||
function* postChargeStripeSaga({ payload }) {
|
|
||||||
try {
|
try {
|
||||||
yield call(postChargeStripe, payload);
|
if (paymentProvider !== PaymentProvider.Patreon) {
|
||||||
yield put(postChargeStripeComplete());
|
yield put(postChargeProcessing());
|
||||||
yield call(setDonationCookie);
|
}
|
||||||
|
|
||||||
|
if (paymentProvider === PaymentProvider.Stripe) {
|
||||||
|
yield call(postChargeStripe, payload);
|
||||||
|
} else if (paymentProvider === PaymentProvider.StripeCard) {
|
||||||
|
const optimizedPayload = { paymentMethodId, amount, duration };
|
||||||
|
const response = yield call(postChargeStripeCard, optimizedPayload);
|
||||||
|
const error = response?.data?.error;
|
||||||
|
if (error) {
|
||||||
|
yield stripeCardErrorHandler(
|
||||||
|
error,
|
||||||
|
handleAuthentication,
|
||||||
|
error.client_secret,
|
||||||
|
response.paymentMethodId,
|
||||||
|
optimizedPayload
|
||||||
|
);
|
||||||
|
|
||||||
|
//if the authentication does not throw an error, add a donation
|
||||||
|
yield call(addDonation, { amount, duration });
|
||||||
|
}
|
||||||
|
} else if (paymentProvider === PaymentProvider.Paypal) {
|
||||||
|
// If the user is signed in and the payment goes through call api
|
||||||
|
let isSignedIn = yield select(isSignedInSelector);
|
||||||
|
// look into skip add donation
|
||||||
|
// what to do with "data" that comes throug
|
||||||
|
if (isSignedIn) yield call(addDonation, { amount, duration });
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
PaymentProvider.Paypal,
|
||||||
|
PaymentProvider.Stripe,
|
||||||
|
PaymentProvider.StripeCard
|
||||||
|
].includes(paymentProvider)
|
||||||
|
) {
|
||||||
|
yield put(postChargeComplete());
|
||||||
|
yield call(setDonationCookie);
|
||||||
|
}
|
||||||
|
yield put(
|
||||||
|
executeGA({
|
||||||
|
type: 'event',
|
||||||
|
data: {
|
||||||
|
category:
|
||||||
|
paymentProvider === PaymentProvider.Patreon
|
||||||
|
? 'Donation Related'
|
||||||
|
: 'Donation',
|
||||||
|
action: stringifyDonationEvents(paymentContext, paymentProvider),
|
||||||
|
label: duration,
|
||||||
|
value: amount
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err =
|
const err =
|
||||||
error.response && error.response.data
|
error.response && error.response.data
|
||||||
? error.response.data.error
|
? error.response.data.error
|
||||||
: defaultDonationErrorMessage;
|
: defaultDonationErrorMessage;
|
||||||
yield put(postChargeStripeError(err));
|
yield put(postChargeError(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,40 +144,16 @@ function* stripeCardErrorHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function* postChargeStripeCardSaga({
|
export function* setDonationCookie() {
|
||||||
payload: { paymentMethodId, amount, duration, handleAuthentication }
|
if (document?.cookie) {
|
||||||
}) {
|
const isDonating = yield select(isDonatingSelector);
|
||||||
try {
|
const isDonorCookieSet = document.cookie
|
||||||
const optimizedPayload = { paymentMethodId, amount, duration };
|
.split(';')
|
||||||
const {
|
.some(item => item.trim().startsWith('isDonor=true'));
|
||||||
data: { error }
|
if (isDonating) {
|
||||||
} = yield call(postChargeStripeCard, optimizedPayload);
|
if (!isDonorCookieSet) {
|
||||||
if (error) {
|
document.cookie = 'isDonor=true';
|
||||||
yield stripeCardErrorHandler(
|
}
|
||||||
error,
|
|
||||||
handleAuthentication,
|
|
||||||
error.client_secret,
|
|
||||||
paymentMethodId,
|
|
||||||
optimizedPayload
|
|
||||||
);
|
|
||||||
}
|
|
||||||
yield call(addDonation, optimizedPayload);
|
|
||||||
yield put(postChargeStripeCardComplete());
|
|
||||||
yield call(setDonationCookie);
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error.message || defaultDonationErrorMessage;
|
|
||||||
yield put(postChargeStripeCardError(errorMessage));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function* setDonationCookie() {
|
|
||||||
const isDonating = yield select(isDonatingSelector);
|
|
||||||
const isDonorCookieSet = document.cookie
|
|
||||||
.split(';')
|
|
||||||
.some(item => item.trim().startsWith('isDonor=true'));
|
|
||||||
if (isDonating) {
|
|
||||||
if (!isDonorCookieSet) {
|
|
||||||
document.cookie = 'isDonor=true';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,9 +161,7 @@ function* setDonationCookie() {
|
|||||||
export function createDonationSaga(types) {
|
export function createDonationSaga(types) {
|
||||||
return [
|
return [
|
||||||
takeEvery(types.tryToShowDonationModal, showDonateModalSaga),
|
takeEvery(types.tryToShowDonationModal, showDonateModalSaga),
|
||||||
takeEvery(types.addDonation, addDonationSaga),
|
takeLeading(types.postCharge, postChargeSaga),
|
||||||
takeLeading(types.postChargeStripe, postChargeStripeSaga),
|
|
||||||
takeLeading(types.postChargeStripeCard, postChargeStripeCardSaga),
|
|
||||||
takeEvery(types.fetchUserComplete, setDonationCookie)
|
takeEvery(types.fetchUserComplete, setDonationCookie)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
130
client/src/redux/donation-saga.test.js
Normal file
130
client/src/redux/donation-saga.test.js
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { expectSaga } from 'redux-saga-test-plan';
|
||||||
|
import {
|
||||||
|
postChargeStripe,
|
||||||
|
postChargeStripeCard,
|
||||||
|
addDonation
|
||||||
|
} from '../utils/ajax';
|
||||||
|
import { postChargeSaga, setDonationCookie } from './donation-saga.js';
|
||||||
|
import { postChargeComplete, postChargeProcessing, executeGA } from './actions';
|
||||||
|
|
||||||
|
jest.mock('../utils/ajax');
|
||||||
|
jest.mock('../analytics');
|
||||||
|
|
||||||
|
const postChargeDataMock = {
|
||||||
|
payload: {
|
||||||
|
paymentProvider: 'stripe',
|
||||||
|
paymentContext: 'donate page',
|
||||||
|
amount: '500',
|
||||||
|
duration: 'monthly',
|
||||||
|
handleAuthentication: jest.fn(),
|
||||||
|
paymentMethodId: '123456'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const analyticsDataMock = {
|
||||||
|
type: 'event',
|
||||||
|
data: {
|
||||||
|
category: 'Donation',
|
||||||
|
action: 'Donate Page Stripe Payment Submission',
|
||||||
|
label: 'monthly',
|
||||||
|
value: '500'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('donation-saga', () => {
|
||||||
|
it('calls postChargeStrip for Stripe', () => {
|
||||||
|
return expectSaga(postChargeSaga, postChargeDataMock)
|
||||||
|
.put(postChargeProcessing())
|
||||||
|
.call(postChargeStripe, postChargeDataMock.payload)
|
||||||
|
.put(postChargeComplete())
|
||||||
|
.call(setDonationCookie)
|
||||||
|
.put(executeGA(analyticsDataMock))
|
||||||
|
.run();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls postChargeStripCard for Stripe Card', () => {
|
||||||
|
const stripeCardDataMock = {
|
||||||
|
payload: { ...postChargeDataMock.payload, paymentProvider: 'stripe card' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripeCardAnalyticsDataMock = analyticsDataMock;
|
||||||
|
stripeCardAnalyticsDataMock.data.action =
|
||||||
|
'Donate Page Stripe Card Payment Submission';
|
||||||
|
|
||||||
|
const { paymentMethodId, amount, duration } = stripeCardDataMock.payload;
|
||||||
|
const optimizedPayload = { paymentMethodId, amount, duration };
|
||||||
|
return expectSaga(postChargeSaga, stripeCardDataMock)
|
||||||
|
.put(postChargeProcessing())
|
||||||
|
.call(postChargeStripeCard, optimizedPayload)
|
||||||
|
.put(postChargeComplete())
|
||||||
|
.call(setDonationCookie)
|
||||||
|
.put(executeGA(stripeCardAnalyticsDataMock))
|
||||||
|
.run();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls addDonate for Paypal if user signed in', () => {
|
||||||
|
const paypalDataMock = {
|
||||||
|
payload: { ...postChargeDataMock.payload, paymentProvider: 'paypal' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const paypalAnalyticsDataMock = analyticsDataMock;
|
||||||
|
paypalAnalyticsDataMock.data.action =
|
||||||
|
'Donate Page Paypal Payment Submission';
|
||||||
|
|
||||||
|
const storeMock = {
|
||||||
|
app: {
|
||||||
|
appUsername: 'devuser'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { amount, duration } = paypalDataMock.payload;
|
||||||
|
return expectSaga(postChargeSaga, paypalDataMock)
|
||||||
|
.withState(storeMock)
|
||||||
|
.put(postChargeProcessing())
|
||||||
|
.call(addDonation, { amount, duration })
|
||||||
|
.put(postChargeComplete())
|
||||||
|
.call(setDonationCookie)
|
||||||
|
.put(executeGA(paypalAnalyticsDataMock))
|
||||||
|
.run();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not call addDonate for Paypal if user not signed in', () => {
|
||||||
|
const paypalDataMock = {
|
||||||
|
payload: { ...postChargeDataMock.payload, paymentProvider: 'paypal' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const paypalAnalyticsDataMock = analyticsDataMock;
|
||||||
|
paypalAnalyticsDataMock.data.action =
|
||||||
|
'Donate Page Paypal Payment Submission';
|
||||||
|
|
||||||
|
const storeMock = {
|
||||||
|
app: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
return expectSaga(postChargeSaga, paypalDataMock)
|
||||||
|
.withState(storeMock)
|
||||||
|
.put(postChargeProcessing())
|
||||||
|
.not.call.fn(addDonation)
|
||||||
|
.put(postChargeComplete())
|
||||||
|
.call(setDonationCookie)
|
||||||
|
.put(executeGA(paypalAnalyticsDataMock))
|
||||||
|
.run();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not call api for Patreon', () => {
|
||||||
|
const patreonDataMock = {
|
||||||
|
payload: { ...postChargeDataMock.payload, paymentProvider: 'patreon' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const patreonAnalyticsDataMock = analyticsDataMock;
|
||||||
|
patreonAnalyticsDataMock.data.action =
|
||||||
|
'Donate Page Patreon Payment Redirection';
|
||||||
|
patreonAnalyticsDataMock.data.category = 'Donation Related';
|
||||||
|
return expectSaga(postChargeSaga, patreonDataMock)
|
||||||
|
.not.call.fn(addDonation)
|
||||||
|
.not.call.fn(postChargeStripeCard)
|
||||||
|
.not.call.fn(postChargeStripe)
|
||||||
|
.put(executeGA(patreonAnalyticsDataMock))
|
||||||
|
.run();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -130,11 +130,11 @@ export const reducer = handleActions(
|
|||||||
...state,
|
...state,
|
||||||
donationFormState: { ...state.donationFormState, ...payload }
|
donationFormState: { ...state.donationFormState, ...payload }
|
||||||
}),
|
}),
|
||||||
[actionTypes.addDonation]: state => ({
|
[actionTypes.postChargeProcessing]: state => ({
|
||||||
...state,
|
...state,
|
||||||
donationFormState: { ...defaultDonationFormState, processing: true }
|
donationFormState: { ...defaultDonationFormState, processing: true }
|
||||||
}),
|
}),
|
||||||
[actionTypes.addDonationComplete]: state => {
|
[actionTypes.postChargeComplete]: state => {
|
||||||
const { appUsername } = state;
|
const { appUsername } = state;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@@ -149,56 +149,11 @@ export const reducer = handleActions(
|
|||||||
donationFormState: { ...defaultDonationFormState, success: true }
|
donationFormState: { ...defaultDonationFormState, success: true }
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[actionTypes.addDonationError]: (state, { payload }) => ({
|
[actionTypes.postChargeError]: (state, { payload }) => ({
|
||||||
...state,
|
...state,
|
||||||
donationFormState: { ...defaultDonationFormState, error: payload }
|
donationFormState: { ...defaultDonationFormState, error: payload }
|
||||||
}),
|
}),
|
||||||
[actionTypes.postChargeStripe]: state => ({
|
|
||||||
...state,
|
|
||||||
donationFormState: { ...defaultDonationFormState, processing: true }
|
|
||||||
}),
|
|
||||||
[actionTypes.postChargeStripeComplete]: state => {
|
|
||||||
const { appUsername } = state;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
user: {
|
|
||||||
...state.user,
|
|
||||||
[appUsername]: {
|
|
||||||
...state.user[appUsername],
|
|
||||||
isDonating: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
donationFormState: { ...defaultDonationFormState, success: true }
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[actionTypes.postChargeStripeError]: (state, { payload }) => ({
|
|
||||||
...state,
|
|
||||||
donationFormState: { ...defaultDonationFormState, error: payload }
|
|
||||||
}),
|
|
||||||
[actionTypes.postChargeStripeCard]: state => ({
|
|
||||||
...state,
|
|
||||||
donationFormState: { ...defaultDonationFormState, processing: true }
|
|
||||||
}),
|
|
||||||
[actionTypes.postChargeStripeCardComplete]: state => {
|
|
||||||
const { appUsername } = state;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
user: {
|
|
||||||
...state.user,
|
|
||||||
[appUsername]: {
|
|
||||||
...state.user[appUsername],
|
|
||||||
isDonating: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
donationFormState: { ...defaultDonationFormState, success: true }
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[actionTypes.postChargeStripeCardError]: (state, { payload }) => ({
|
|
||||||
...state,
|
|
||||||
donationFormState: { ...defaultDonationFormState, error: payload }
|
|
||||||
}),
|
|
||||||
[actionTypes.fetchUser]: state => ({
|
[actionTypes.fetchUser]: state => ({
|
||||||
...state,
|
...state,
|
||||||
userFetchState: { ...defaultFetchState }
|
userFetchState: { ...defaultFetchState }
|
||||||
|
|||||||
14
client/src/utils/analyticsStrings.test.ts
Normal file
14
client/src/utils/analyticsStrings.test.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { stringifyDonationEvents } from './analyticsStrings';
|
||||||
|
|
||||||
|
describe('Analytics donation strings', () => {
|
||||||
|
it('Should return correct string for modal patreon payment', () => {
|
||||||
|
expect(stringifyDonationEvents('modal', 'patreon')).toEqual(
|
||||||
|
'Modal Patreon Payment Redirection'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('Should return correct string for modal donate page stripe card payment', () => {
|
||||||
|
expect(stringifyDonationEvents('donate page', 'stripe card')).toEqual(
|
||||||
|
'Donate Page Stripe Card Payment Submission'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
15
client/src/utils/analyticsStrings.ts
Normal file
15
client/src/utils/analyticsStrings.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { PaymentContext, PaymentProvider } from '../components/Donation/types';
|
||||||
|
|
||||||
|
export function stringifyDonationEvents(
|
||||||
|
paymentContext: PaymentContext,
|
||||||
|
paymentProvider: PaymentProvider
|
||||||
|
): string {
|
||||||
|
const donationString = `${paymentContext} ${paymentProvider} payment ${
|
||||||
|
paymentProvider === 'patreon' ? 'redirection' : 'submission'
|
||||||
|
}`;
|
||||||
|
|
||||||
|
// return title case
|
||||||
|
return donationString.replace(/(^\w{1})|(\s+\w{1})/g, letter =>
|
||||||
|
letter.toUpperCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,26 +1,32 @@
|
|||||||
// Configuration for client side
|
// Configuration for client side
|
||||||
const durationsConfig = {
|
import { DonationConfig } from '../client/src/components/Donation/types';
|
||||||
|
|
||||||
|
export const durationsConfig: {
|
||||||
|
month: 'monthly';
|
||||||
|
onetime: 'one-time';
|
||||||
|
} = {
|
||||||
month: 'monthly',
|
month: 'monthly',
|
||||||
onetime: 'one-time'
|
onetime: 'one-time'
|
||||||
};
|
};
|
||||||
const amountsConfig = {
|
|
||||||
|
export const amountsConfig = {
|
||||||
month: [1000, 2000, 3000, 4000, 5000],
|
month: [1000, 2000, 3000, 4000, 5000],
|
||||||
onetime: [2500, 5000, 7500, 10000, 15000]
|
onetime: [2500, 5000, 7500, 10000, 15000]
|
||||||
};
|
};
|
||||||
const defaultAmount = {
|
export const defaultAmount: { month: 500; onetime: 7500 } = {
|
||||||
month: 500,
|
month: 500,
|
||||||
onetime: 7500
|
onetime: 7500
|
||||||
};
|
};
|
||||||
const defaultDonation = {
|
export const defaultDonation: DonationConfig = {
|
||||||
donationAmount: defaultAmount['month'],
|
donationAmount: defaultAmount.month,
|
||||||
donationDuration: 'month'
|
donationDuration: 'month'
|
||||||
};
|
};
|
||||||
const modalDefaultDonation = {
|
export const modalDefaultDonation: DonationConfig = {
|
||||||
donationAmount: 500,
|
donationAmount: 500,
|
||||||
donationDuration: 'month'
|
donationDuration: 'month'
|
||||||
};
|
};
|
||||||
|
|
||||||
const onetimeSKUConfig = {
|
export const onetimeSKUConfig = {
|
||||||
live: [
|
live: [
|
||||||
{ amount: '15000', id: 'sku_IElisJHup0nojP' },
|
{ amount: '15000', id: 'sku_IElisJHup0nojP' },
|
||||||
{ amount: '10000', id: 'sku_IEliodY88lglPk' },
|
{ amount: '10000', id: 'sku_IEliodY88lglPk' },
|
||||||
@@ -38,9 +44,9 @@ const onetimeSKUConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Configuration for server side
|
// Configuration for server side
|
||||||
const durationKeysConfig = ['month', 'onetime'];
|
export const durationKeysConfig = ['month', 'one-time'];
|
||||||
const donationOneTimeConfig = [100000, 25000, 6000];
|
export const donationOneTimeConfig = [100000, 25000, 6000];
|
||||||
const donationSubscriptionConfig = {
|
export const donationSubscriptionConfig = {
|
||||||
duration: {
|
duration: {
|
||||||
month: 'Monthly'
|
month: 'Monthly'
|
||||||
},
|
},
|
||||||
@@ -51,7 +57,7 @@ const donationSubscriptionConfig = {
|
|||||||
|
|
||||||
// Shared paypal configuration
|
// Shared paypal configuration
|
||||||
// keep the 5 dollars for the modal
|
// keep the 5 dollars for the modal
|
||||||
const paypalConfigTypes = {
|
export const paypalConfigTypes = {
|
||||||
live: {
|
live: {
|
||||||
month: {
|
month: {
|
||||||
500: { planId: 'P-1L11422374370240ULZKX3PA' },
|
500: { planId: 'P-1L11422374370240ULZKX3PA' },
|
||||||
@@ -74,42 +80,51 @@ const paypalConfigTypes = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const paypalConfigurator = (donationAmount, donationDuration, paypalConfig) => {
|
export const paypalConfigurator = (
|
||||||
if (donationDuration === 'onetime') {
|
donationAmount: 500 | 1000 | 2000 | 3000 | 4000 | 5000,
|
||||||
|
donationDuration: 'one-time' | 'month',
|
||||||
|
paypalConfig: {
|
||||||
|
month: {
|
||||||
|
500: { planId: string };
|
||||||
|
1000: { planId: string };
|
||||||
|
2000: { planId: string };
|
||||||
|
3000: { planId: string };
|
||||||
|
4000: { planId: string };
|
||||||
|
5000: { planId: string };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
if (donationDuration === 'one-time') {
|
||||||
return { amount: donationAmount, duration: donationDuration, planId: null };
|
return { amount: donationAmount, duration: donationDuration, planId: null };
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
amount: donationAmount,
|
amount: donationAmount,
|
||||||
duration: donationDuration,
|
duration: donationDuration,
|
||||||
planId: paypalConfig[donationDuration]['' + donationAmount].planId
|
planId: paypalConfig[donationDuration][donationAmount].planId
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const donationUrls = {
|
export const donationUrls = {
|
||||||
successUrl: 'https://www.freecodecamp.org/news/thank-you-for-donating/',
|
successUrl: 'https://www.freecodecamp.org/news/thank-you-for-donating/',
|
||||||
cancelUrl: 'https://freecodecamp.org/donate'
|
cancelUrl: 'https://freecodecamp.org/donate'
|
||||||
};
|
};
|
||||||
|
|
||||||
const patreonDefaultPledgeAmount = 500;
|
export const patreonDefaultPledgeAmount = 500;
|
||||||
|
|
||||||
const aBTestConfig = {
|
export const aBTestConfig = {
|
||||||
isTesting: true,
|
isTesting: true,
|
||||||
type: 'secureIconButtonOnly'
|
type: 'secureIconButtonOnly'
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
export enum PaymentContext {
|
||||||
durationsConfig,
|
Modal = 'modal',
|
||||||
amountsConfig,
|
DonatePage = 'donate page',
|
||||||
defaultAmount,
|
Certificate = 'certificate'
|
||||||
defaultDonation,
|
}
|
||||||
durationKeysConfig,
|
|
||||||
donationOneTimeConfig,
|
export enum PaymentProvider {
|
||||||
donationSubscriptionConfig,
|
Paypal = 'paypal',
|
||||||
modalDefaultDonation,
|
Patreon = 'patreon',
|
||||||
onetimeSKUConfig,
|
Stripe = 'stripe',
|
||||||
paypalConfigTypes,
|
StripeCard = 'stripe card'
|
||||||
paypalConfigurator,
|
}
|
||||||
donationUrls,
|
|
||||||
patreonDefaultPledgeAmount,
|
|
||||||
aBTestConfig
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user