Skip to content
This repository has been archived by the owner on Mar 27, 2023. It is now read-only.

auto one click #1309

Merged
merged 2 commits into from Dec 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion app/controllers/api/payment/braintree_controller.rb
Expand Up @@ -17,7 +17,10 @@ def webhook

def one_click
@result = client::OneClick.new(unsafe_params, cookies.signed[:payment_methods]).run
render status: :unprocessable_entity unless @result.success?
unless @result.success?
@errors = client::ErrorProcessing.new(@result, locale: locale).process
render status: :unprocessable_entity, errors: @errors
end
end

private
Expand Down
2 changes: 2 additions & 0 deletions app/javascript/components/AmountSelection/AmountSelection.js
Expand Up @@ -15,6 +15,7 @@ export type Props = {
currency: string,
nextStepTitle?: React.Element<any>,
selectAmount: (amount: ?number) => void,
selectCustomAmount?: (amount: ?number) => void,
changeCurrency: (currency: string) => void,
proceed: () => void,
};
Expand Down Expand Up @@ -83,6 +84,7 @@ export default class AmountSelection extends React.Component<Props, State> {
proceed={this.props.proceed}
featuredAmount={this.props.donationFeaturedAmount}
selectAmount={this.props.selectAmount}
selectCustomAmount={this.props.selectCustomAmount}
/>
<p>
<FormattedMessage
Expand Down
90 changes: 90 additions & 0 deletions app/javascript/components/CurrencySelector/CurrencySelector.js
@@ -0,0 +1,90 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import { changeCurrency } from '../../state/fundraiser/actions';

import type { AppState } from '../../state/reducers';

type Props = {};

type State = {
currencyDropdVisible: boolean,
};

class CurrencySelector extends Component<Props> {
constructor(props: Props) {
super(props);
this.state = {
currencyDropdVisible: false,
};
}

toggleCurrencyDropd() {
this.setState({
currencyDropdVisible: !this.state.currencyDropdVisible,
});
}

onSelectCurrency(currency: string): void {
this.props.changeCurrency(currency);
}

selectElement() {
return (
this.state.currencyDropdVisible && (
<select
value={this.props.currency}
className="AmountSelection__currency-selector"
onChange={(e: SyntheticEvent<HTMLSelectElement>) =>
this.onSelectCurrency(e.currentTarget.value)
}
>
{Object.keys(this.props.donationBands).map(currency => {
return (
<option key={currency} value={currency}>
{currency}
</option>
);
})}
</select>
)
);
}

render() {
return (
<p>
<FormattedMessage
id="fundraiser.currency_in"
defaultMessage="Values shown in {currency}."
values={{ currency: this.props.currency }}
/>
.&nbsp;
<a
onClick={this.toggleCurrencyDropd.bind(this)}
className="AmountSelection__currency-toggle"
>
<FormattedMessage
id="fundraiser.switch_currency"
defaultMessage="Switch currency"
/>
</a>
{this.selectElement()}
</p>
);
}
}

const mapState = (state: AppState) => ({
currency: state.fundraiser.currency,
donationBands: state.fundraiser.donationBands,
});

const mapDispatch = (dispatch: Dispatch<*>) => ({
changeCurrency: (currency: string) => dispatch(changeCurrency(currency)),
});

export default connect(
mapState,
mapDispatch
)(CurrencySelector);
4 changes: 4 additions & 0 deletions app/javascript/components/DonationBands/DonationBands.css
Expand Up @@ -7,6 +7,10 @@
margin-bottom: 12px;
}

.DonationBands-container {
margin-top: 12px;
}

.AmountSelection__proceed-button, .AmountSelection__currency-selector {
margin-top: 12px;
}
Expand Down
8 changes: 6 additions & 2 deletions app/javascript/components/DonationBands/DonationBands.js
Expand Up @@ -20,13 +20,15 @@ type Props = {
customAmount?: number,
proceed: () => void,
intl: IntlShape,
selectAmount: (amount: ?number) => void,
selectAmount: (amount: ?number) => void | Promise<*>,
selectCustomAmount?: (amount: ?number) => void | Promise<*>,
featuredAmount?: number,
};

type State = {
customAmount?: number,
};

export class DonationBands extends Component<Props, State> {
constructor(props: Props) {
super(props);
Expand All @@ -47,7 +49,9 @@ export class DonationBands extends Component<Props, State> {
const number = value.replace(/\D/g, '');
const amount = number ? parseFloat(number) : undefined;
this.setState({ customAmount: amount });
if (this.props.selectAmount) {
if (this.props.selectCustomAmount) {
this.props.selectCustomAmount(amount);
} else {
this.props.selectAmount(amount);
}
}
Expand Down
Expand Up @@ -151,6 +151,8 @@ exports[`renders all amounts with the currency symbol 1`] = `
"fundraiser.loading": "Loading secure <br> payment portal",
"fundraiser.make_recurring": "Make my donation monthly",
"fundraiser.month": "month",
"fundraiser.one_click_failed": "We're sorry but we could not process your donation. Please try again with a different card.",
"fundraiser.one_click_warning": "Your donation will be processed immediately.",
"fundraiser.oneclick.credit_card_payment_method": "{card_type} ending in {last_four_digits}",
"fundraiser.oneclick.new_payment_method": "Add payment method",
"fundraiser.oneclick.paypal_payment_method": "Paypal ({email})",
Expand Down Expand Up @@ -586,6 +588,8 @@ exports[`renders correctly 1`] = `
"fundraiser.loading": "Loading secure <br> payment portal",
"fundraiser.make_recurring": "Make my donation monthly",
"fundraiser.month": "month",
"fundraiser.one_click_failed": "We're sorry but we could not process your donation. Please try again with a different card.",
"fundraiser.one_click_warning": "Your donation will be processed immediately.",
"fundraiser.oneclick.credit_card_payment_method": "{card_type} ending in {last_four_digits}",
"fundraiser.oneclick.new_payment_method": "Add payment method",
"fundraiser.oneclick.paypal_payment_method": "Paypal ({email})",
Expand Down
206 changes: 206 additions & 0 deletions app/javascript/components/OneClick/OneClick.js
@@ -0,0 +1,206 @@
// @flow

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import DonationBands from '../DonationBands/DonationBands';
import DonateButton from '../DonateButton';
import CurrencySelector from '../CurrencySelector/CurrencySelector';
import ee from '../../shared/pub_sub';

import Button from '../Button/Button';
import {
changeAmount,
setSubmitting,
oneClickFailed,
} from '../../state/fundraiser/actions';

import type { AppState } from '../../state/reducers';
import type { ChampaignPage } from '../../types';
import type { Dispatch } from 'redux';

type Props = {
donationBands: any,
currency: string,
donationAmount: number,
selectAmount: (amount: ?number) => void | Promise<*>,
paymentMethods: any[],
formId: number,
formValues: Object,
form: Object,
title: string,
submitting: boolean,
setSubmitting: boolean => void,
oneClickFailed: () => void,
donationFeaturedAmount?: number,
page: ChampaignPage,
};

type State = {
amountConfirmationRequired: boolean,
submitting: boolean,
};

class OneClick extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
amountConfirmationRequired: false,
submitting: false,
};
}

async onSelectCustomAmount(amount: ?number) {
await this.props.selectAmount(amount);
this.setState({ amountConfirmationRequired: true });
}

async selectAmount(amount: ?number) {
this.props.selectAmount(amount);
this.submit();
}

oneClickData() {
return {
payment: {
currency: this.props.currency,
amount: this.props.donationAmount,
recurring: false,
payment_method_id: this.props.paymentMethods[0].id,
// payment_method_id: 900,
},
user: {
form_id: this.props.formId,
// formValues will have the prefillValues
...this.props.formValues,
// form will have the user's submitted values
...this.props.form,
},
};
}

submit = () => {
const data = this.oneClickData();
if (data) {
this.props.setSubmitting(true);

$.post(
`/api/payment/braintree/pages/${this.props.page.id}/one_click`,
data
).then(this.onSuccess.bind(this), this.onFailure.bind(this));
}
};

async onFailure(reason: any): any {
this.setState({ submitting: false });
this.props.setSubmitting(false);
this.props.oneClickFailed();

ee.emit('fundraiser:transaction_error', reason, this.oneClickData());
return reason;
}

async onSuccess(data: any): any {
ee.emit('fundraiser:transaction_success', data, this.oneClickData());
return data;
}

donateButton() {
if (!this.state.amountConfirmationRequired) return null;
if (!this.props.donationAmount) return null;

return (
<DonateButton
currency={this.props.currency}
amount={this.props.donationAmount || 0}
recurring={false}
submitting={this.state.submitting}
disabled={this.state.submitting}
onClick={() => this.submit()}
/>
);
}

procssingView() {
return (
<div className="submission-interstitial">
<h1 className="submission-interstitial__title">
<i className="fa fa-spin fa-cog" />
Processing
</h1>
<h4>
Please do not close this tab
<br />
or use the back button.
</h4>
</div>
);
}

paymentOptionsView() {
return (
<div className="OneClick">
<div className="StepWrapper-root">
<div className="overlay-toggle__mobile-ui">
<a className="overlay-toggle__close-button">✕</a>
</div>
<div className="Stepper fundraiser-bar__top">
<h2 className="Stepper__header">{this.props.title}</h2>
</div>
<div className="fundraiser-bar__main">
<p>
<FormattedMessage
id="fundraiser.one_click_warning"
defaultMessage="Your donation will be processed immediately."
/>
</p>

<DonationBands
amounts={this.props.donationBands[this.props.currency]}
currency={this.props.currency}
featuredAmount={this.props.donationFeaturedAmount}
proceed={() => {}}
selectAmount={this.selectAmount.bind(this)}
selectCustomAmount={this.onSelectCustomAmount.bind(this)}
/>

{this.donateButton()}
<CurrencySelector />
</div>
</div>
</div>
);
}

render() {
return this.props.submitting
? this.procssingView()
: this.paymentOptionsView();
}
}

const mapState = (state: AppState) => ({
currency: state.fundraiser.currency,
donationAmount: state.fundraiser.donationAmount,
donationBands: state.fundraiser.donationBands,
recurring: state.fundraiser.recurring,
paymentMethods: state.paymentMethods,
formId: state.fundraiser.formId,
formValues: state.fundraiser.formValues,
form: state.fundraiser.form,
page: state.page,
submitting: state.fundraiser.submitting,
title: state.fundraiser.title,
donationFeaturedAmount: state.fundraiser.donationFeaturedAmount,
});

const mapDispatch = (dispatch: Dispatch<*>) => ({
selectAmount: (amount: number) => dispatch(changeAmount(amount)),
setSubmitting: (submitting: boolean) => dispatch(setSubmitting(submitting)),
oneClickFailed: () => dispatch(oneClickFailed()),
});

export default connect(
mapState,
mapDispatch
)(OneClick);
Expand Up @@ -137,6 +137,8 @@ exports[`Snapshots: With default messages object 1`] = `
"fundraiser.loading": "Loading secure <br> payment portal",
"fundraiser.make_recurring": "Make my donation monthly",
"fundraiser.month": "month",
"fundraiser.one_click_failed": "We're sorry but we could not process your donation. Please try again with a different card.",
"fundraiser.one_click_warning": "Your donation will be processed immediately.",
"fundraiser.oneclick.credit_card_payment_method": "{card_type} ending in {last_four_digits}",
"fundraiser.oneclick.new_payment_method": "Add payment method",
"fundraiser.oneclick.paypal_payment_method": "Paypal ({email})",
Expand Down