mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-10-24 23:49:06 +08:00
feat(client): add google pay (#43117)
* feat: initial button setup client * feat: rename walletsButton to .tsx * chore: typescriptize wallet component * chore: re-add keys to config, env, etc + check in gatsby-node * feat: refactor donate form and wallet component * feat(client): set labels correctly * chore: add stripe package back to server * chore: add stripe back to allowed paths * chore: copy donate.js code from PR #41924 * feat: attempt to make back end work * feat: make redux work * feat: clean up * feat: hokify * feat: add error handling * fix: back-end should be working * fix: type errors * fix: clean up back-end * feat:addd styles * feat: connect the client to the api * feat: display wallets button everywhere * test: add stripe key for cypress action * test: fix for cypress tests * test: cypress tests again * test: maybe? * test: more * test: more * test: more * test * askdfjasklfj * fix: tests finally? * revert: remove space from cypress yaml action * remove logs Co-authored-by: moT01 <20648924+moT01@users.noreply.github.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
co-authored by
moT01
Oliver Eyton-Williams
parent
ad54684dce
commit
b623c340a9
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import {
|
||||
Col,
|
||||
@@ -11,6 +10,8 @@ import {
|
||||
ToggleButton,
|
||||
ToggleButtonGroup
|
||||
} from '@freecodecamp/react-bootstrap';
|
||||
|
||||
import type { Token } from '@stripe/stripe-js';
|
||||
import React, { Component } from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -30,7 +31,8 @@ import {
|
||||
addDonation,
|
||||
updateDonationFormState,
|
||||
defaultDonationFormState,
|
||||
userSelector
|
||||
userSelector,
|
||||
postChargeStripe
|
||||
} from '../../redux';
|
||||
import Spacer from '../helpers/spacer';
|
||||
|
||||
@@ -38,6 +40,7 @@ import DonateCompletion from './DonateCompletion';
|
||||
|
||||
import type { AddDonationData } from './PaypalButton';
|
||||
import PaypalButton from './PaypalButton';
|
||||
import WalletsWrapper from './walletsButton';
|
||||
|
||||
import './Donation.css';
|
||||
|
||||
@@ -55,6 +58,7 @@ type DonateFormState = {
|
||||
|
||||
type DonateFormProps = {
|
||||
addDonation: (data: unknown) => unknown;
|
||||
postChargeStripe: (data: unknown) => unknown;
|
||||
defaultTheme?: string;
|
||||
email: string;
|
||||
handleProcessing: (duration: string, amount: number, action: string) => void;
|
||||
@@ -91,7 +95,8 @@ const mapStateToProps = createSelector(
|
||||
|
||||
const mapDispatchToProps = {
|
||||
addDonation,
|
||||
updateDonationFormState
|
||||
updateDonationFormState,
|
||||
postChargeStripe
|
||||
};
|
||||
|
||||
class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
||||
@@ -126,6 +131,7 @@ class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
||||
this.handleSelectDuration = this.handleSelectDuration.bind(this);
|
||||
this.hideAmountOptionsCB = this.hideAmountOptionsCB.bind(this);
|
||||
this.resetDonation = this.resetDonation.bind(this);
|
||||
this.postStripeDonation = this.postStripeDonation.bind(this);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -180,6 +186,28 @@ class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
||||
this.setState({ donationDuration, donationAmount });
|
||||
}
|
||||
|
||||
postStripeDonation(
|
||||
token: Token,
|
||||
payerEmail: string | undefined,
|
||||
payerName: string | undefined
|
||||
) {
|
||||
const { donationAmount: amount, donationDuration: duration } = this.state;
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
// change the donation modal button label to close
|
||||
// or display the close button for the cert donation section
|
||||
if (this.props.handleProcessing) {
|
||||
this.props.handleProcessing(duration, amount, 'Stripe payment submition');
|
||||
}
|
||||
this.props.postChargeStripe({
|
||||
token,
|
||||
amount,
|
||||
duration,
|
||||
email: payerEmail,
|
||||
name: payerName
|
||||
});
|
||||
}
|
||||
|
||||
handleSelectAmount(donationAmount: number) {
|
||||
this.setState({ donationAmount });
|
||||
}
|
||||
@@ -277,20 +305,31 @@ class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
||||
theme
|
||||
} = this.props;
|
||||
const { donationAmount, donationDuration } = this.state;
|
||||
|
||||
const isOneTime = donationDuration === 'onetime';
|
||||
const formlabel = `${t(
|
||||
isOneTime ? 'donate.confirm-2' : 'donate.confirm-3',
|
||||
{ usd: donationAmount / 100 }
|
||||
)}:`;
|
||||
|
||||
const walletlabel = `${t(
|
||||
isOneTime ? 'donate.wallet-label' : 'donate.wallet-label-1',
|
||||
{ usd: donationAmount / 100 }
|
||||
)}:`;
|
||||
const priorityTheme = defaultTheme ? defaultTheme : theme;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isOneTime ? (
|
||||
<b>
|
||||
{t('donate.confirm-1')} {donationAmount / 100}:
|
||||
</b>
|
||||
) : (
|
||||
<b>{t('donate.confirm-3', { usd: donationAmount / 100 })}:</b>
|
||||
)}
|
||||
<b>{formlabel}</b>
|
||||
<Spacer />
|
||||
<div className='donate-btn-group'>
|
||||
<WalletsWrapper
|
||||
amount={donationAmount}
|
||||
label={walletlabel}
|
||||
onDonationStateChange={this.onDonationStateChange}
|
||||
postStripeDonation={this.postStripeDonation}
|
||||
refreshErrorMessage={t('donate.refresh-needed')}
|
||||
theme={priorityTheme}
|
||||
/>
|
||||
<PaypalButton
|
||||
addDonation={addDonation}
|
||||
donationAmount={donationAmount}
|
||||
@@ -299,7 +338,7 @@ class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
||||
isSubscription={isOneTime ? false : true}
|
||||
onDonationStateChange={this.onDonationStateChange}
|
||||
skipAddDonation={!isSignedIn}
|
||||
theme={defaultTheme ? defaultTheme : theme}
|
||||
theme={priorityTheme}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -322,21 +361,37 @@ class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
||||
|
||||
renderModalForm() {
|
||||
const { donationAmount, donationDuration } = this.state;
|
||||
const { handleProcessing, addDonation, defaultTheme, theme } = this.props;
|
||||
const { handleProcessing, addDonation, defaultTheme, theme, t } =
|
||||
this.props;
|
||||
const priorityTheme = defaultTheme ? defaultTheme : theme;
|
||||
const isOneTime = donationDuration === 'onetime';
|
||||
const walletlabel = `${t(
|
||||
isOneTime ? 'donate.wallet-label' : 'donate.wallet-label-1',
|
||||
{ usd: donationAmount / 100 }
|
||||
)}:`;
|
||||
return (
|
||||
<Row>
|
||||
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<b className='donation-label'>{this.getDonationButtonLabel()}:</b>
|
||||
<Spacer />
|
||||
<b>{this.getDonationButtonLabel()}:</b>
|
||||
<Spacer />
|
||||
<PaypalButton
|
||||
addDonation={addDonation}
|
||||
donationAmount={donationAmount}
|
||||
donationDuration={donationDuration}
|
||||
handleProcessing={handleProcessing}
|
||||
onDonationStateChange={this.onDonationStateChange}
|
||||
theme={defaultTheme ? defaultTheme : theme}
|
||||
/>
|
||||
<div className='donate-btn-group'>
|
||||
<WalletsWrapper
|
||||
amount={donationAmount}
|
||||
label={walletlabel}
|
||||
onDonationStateChange={this.onDonationStateChange}
|
||||
postStripeDonation={this.postStripeDonation}
|
||||
refreshErrorMessage={t('donate.refresh-needed')}
|
||||
theme={priorityTheme}
|
||||
/>
|
||||
<PaypalButton
|
||||
addDonation={addDonation}
|
||||
donationAmount={donationAmount}
|
||||
donationDuration={donationDuration}
|
||||
handleProcessing={handleProcessing}
|
||||
onDonationStateChange={this.onDonationStateChange}
|
||||
theme={defaultTheme ? defaultTheme : theme}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
|
||||
@@ -245,11 +245,13 @@ li.disabled > a {
|
||||
font-weight: 600;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.donation-label,
|
||||
.donation-modal p,
|
||||
.donation-modal b {
|
||||
text-align: center;
|
||||
font-size: 1rem;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.donation-icon-container {
|
||||
@@ -386,31 +388,23 @@ button#confirm-donation-btn:hover {
|
||||
|
||||
.donate-btn-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.donate-btn-group > * {
|
||||
width: 100%;
|
||||
height: 43px;
|
||||
}
|
||||
|
||||
.donate-btn-group button:first-child {
|
||||
margin-bottom: 10px;
|
||||
.wallets-form {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@media (min-width: 500px) {
|
||||
.donate-btn-group {
|
||||
flex-direction: row;
|
||||
}
|
||||
.donate-btn-group > * {
|
||||
width: 49%;
|
||||
}
|
||||
|
||||
.donate-btn-group button:first-child {
|
||||
margin-bottom: 0px;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.donate-page-wrapper .alert.alert-info a:hover {
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
PaymentRequestButtonElement,
|
||||
Elements,
|
||||
ElementsConsumer
|
||||
} from '@stripe/react-stripe-js';
|
||||
import { Stripe, loadStripe } from '@stripe/stripe-js';
|
||||
import type { Token, PaymentRequest } from '@stripe/stripe-js';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import envData from '../../../../config/env.json';
|
||||
import { AddDonationData } from './PaypalButton';
|
||||
|
||||
const { stripePublicKey }: { stripePublicKey: string | null } = envData;
|
||||
|
||||
interface WrapperProps {
|
||||
label: string;
|
||||
amount: number;
|
||||
theme: string;
|
||||
postStripeDonation: (
|
||||
token: Token,
|
||||
payerEmail: string | undefined,
|
||||
payerName: string | undefined
|
||||
) => void;
|
||||
onDonationStateChange: (donationState: AddDonationData) => void;
|
||||
refreshErrorMessage: string;
|
||||
}
|
||||
interface WalletsButtonProps extends WrapperProps {
|
||||
stripe: Stripe | null;
|
||||
}
|
||||
|
||||
const WalletsButton = ({
|
||||
stripe,
|
||||
label,
|
||||
amount,
|
||||
theme,
|
||||
refreshErrorMessage,
|
||||
postStripeDonation,
|
||||
onDonationStateChange
|
||||
}: WalletsButtonProps) => {
|
||||
const [token, setToken] = useState<Token | null>(null);
|
||||
const [paymentRequest, setPaymentRequest] = useState<PaymentRequest | null>(
|
||||
null
|
||||
);
|
||||
const [canMakePayment, checkpaymentPossiblity] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!stripe) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pr = stripe.paymentRequest({
|
||||
country: 'US',
|
||||
currency: 'usd',
|
||||
total: { label, amount },
|
||||
requestPayerName: true,
|
||||
requestPayerEmail: true,
|
||||
disableWallets: ['browserCard']
|
||||
});
|
||||
|
||||
pr.on('token', event => {
|
||||
const { token, payerEmail, payerName } = event;
|
||||
setToken(token);
|
||||
event.complete('success');
|
||||
postStripeDonation(token, payerEmail, payerName);
|
||||
});
|
||||
|
||||
void pr.canMakePayment().then(canMakePaymentRes => {
|
||||
if (canMakePaymentRes) {
|
||||
setPaymentRequest(pr);
|
||||
checkpaymentPossiblity(true);
|
||||
} else {
|
||||
checkpaymentPossiblity(false);
|
||||
}
|
||||
});
|
||||
}, [label, amount, stripe, postStripeDonation]);
|
||||
|
||||
const displayRefreshError = (): void => {
|
||||
onDonationStateChange({
|
||||
redirecting: false,
|
||||
processing: false,
|
||||
success: false,
|
||||
error: refreshErrorMessage
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form className='wallets-form'>
|
||||
{canMakePayment && paymentRequest && (
|
||||
<PaymentRequestButtonElement
|
||||
onClick={() => {
|
||||
if (token) {
|
||||
displayRefreshError();
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
style: {
|
||||
paymentRequestButton: {
|
||||
type: 'default',
|
||||
theme: theme === 'night' ? 'light' : 'dark',
|
||||
height: '43px'
|
||||
}
|
||||
},
|
||||
paymentRequest
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const InjectedCheckoutForm = (props: WrapperProps): JSX.Element => (
|
||||
<ElementsConsumer>
|
||||
{({ stripe }: { stripe: Stripe | null }) => (
|
||||
<WalletsButton stripe={stripe} {...props} />
|
||||
)}
|
||||
</ElementsConsumer>
|
||||
);
|
||||
|
||||
const WalletsWrapper = (props: WrapperProps): JSX.Element | null => {
|
||||
if (!stripePublicKey) {
|
||||
return null;
|
||||
} else {
|
||||
const stripePromise = loadStripe(stripePublicKey);
|
||||
return (
|
||||
<Elements stripe={stripePromise}>
|
||||
<InjectedCheckoutForm {...props} />
|
||||
</Elements>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default WalletsWrapper;
|
||||
@@ -27,7 +27,8 @@ export const actionTypes = createTypes(
|
||||
...createAsyncTypes('fetchProfileForUser'),
|
||||
...createAsyncTypes('acceptTerms'),
|
||||
...createAsyncTypes('showCert'),
|
||||
...createAsyncTypes('reportUser')
|
||||
...createAsyncTypes('reportUser'),
|
||||
...createAsyncTypes('postChargeStripe')
|
||||
],
|
||||
ns
|
||||
);
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { put, select, takeEvery, delay, call, take } from 'redux-saga/effects';
|
||||
import { addDonation } from '../utils/ajax';
|
||||
import {
|
||||
put,
|
||||
select,
|
||||
takeEvery,
|
||||
takeLeading,
|
||||
delay,
|
||||
call,
|
||||
take
|
||||
} from 'redux-saga/effects';
|
||||
import { addDonation, postChargeStripe } from '../utils/ajax';
|
||||
import { actionTypes as appTypes } from './action-types';
|
||||
|
||||
import {
|
||||
@@ -9,7 +17,9 @@ import {
|
||||
preventProgressDonationRequests,
|
||||
recentlyClaimedBlockSelector,
|
||||
addDonationComplete,
|
||||
addDonationError
|
||||
addDonationError,
|
||||
postChargeStripeComplete,
|
||||
postChargeStripeError
|
||||
} from './';
|
||||
|
||||
const defaultDonationError = `Something is not right. Please contact donors@freecodecamp.org`;
|
||||
@@ -44,9 +54,23 @@ function* addDonationSaga({ payload }) {
|
||||
}
|
||||
}
|
||||
|
||||
function* postChargeStripeSaga({ payload }) {
|
||||
try {
|
||||
yield call(postChargeStripe, payload);
|
||||
yield put(postChargeStripeComplete());
|
||||
} catch (error) {
|
||||
const err =
|
||||
error.response && error.response.data
|
||||
? error.response.data.error
|
||||
: defaultDonationError;
|
||||
yield put(postChargeStripeError(err));
|
||||
}
|
||||
}
|
||||
|
||||
export function createDonationSaga(types) {
|
||||
return [
|
||||
takeEvery(types.tryToShowDonationModal, showDonateModalSaga),
|
||||
takeEvery(types.addDonation, addDonationSaga)
|
||||
takeEvery(types.addDonation, addDonationSaga),
|
||||
takeLeading(types.postChargeStripe, postChargeStripeSaga)
|
||||
];
|
||||
}
|
||||
|
||||
@@ -125,6 +125,14 @@ export const addDonationComplete = createAction(
|
||||
);
|
||||
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 fetchProfileForUser = createAction(
|
||||
actionTypes.fetchProfileForUser
|
||||
);
|
||||
@@ -415,6 +423,29 @@ export const reducer = handleActions(
|
||||
...state,
|
||||
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.fetchUser]: state => ({
|
||||
...state,
|
||||
userFetchState: { ...defaultFetchState }
|
||||
|
||||
@@ -104,6 +104,9 @@ export function addDonation(body: Donation): Promise<void> {
|
||||
return post('/donate/add-donation', body);
|
||||
}
|
||||
|
||||
export function postChargeStripe(body: Donation): Promise<void> {
|
||||
return post('/donate/charge-stripe', body);
|
||||
}
|
||||
interface Report {
|
||||
username: string;
|
||||
reportDescription: string;
|
||||
|
||||
Reference in New Issue
Block a user