diff --git a/app/javascript/components/AmountSelection/AmountSelection.js b/app/javascript/components/AmountSelection/AmountSelection.js index 18c5f09972..e069542f2b 100644 --- a/app/javascript/components/AmountSelection/AmountSelection.js +++ b/app/javascript/components/AmountSelection/AmountSelection.js @@ -72,6 +72,10 @@ export default class AmountSelection extends React.Component { this.props.proceed(); } + processSelection(e) { + this.props.proceed(e); + } + render() { return (
@@ -80,9 +84,10 @@ export default class AmountSelection extends React.Component { ref="donationBands" amounts={this.props.donationBands[this.props.currency]} currency={this.props.currency} - proceed={this.props.proceed} + proceed={this.processSelection.bind(this)} featuredAmount={this.props.donationFeaturedAmount} selectAmount={this.props.selectAmount} + selectCustomAmount={this.props.selectCustomAmount} />

{ + 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 && ( + + ) + ); + } + + render() { + return ( +

+ + .  + + + + {this.selectElement()} +

+ ); + } +} + +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); diff --git a/app/javascript/components/DonationBands/DonationBands.css b/app/javascript/components/DonationBands/DonationBands.css index 198fb8e092..622c07c7f3 100644 --- a/app/javascript/components/DonationBands/DonationBands.css +++ b/app/javascript/components/DonationBands/DonationBands.css @@ -7,6 +7,10 @@ margin-bottom: 12px; } +.DonationBands-container { + margin-top: 12px; +} + .AmountSelection__proceed-button, .AmountSelection__currency-selector { margin-top: 12px; } diff --git a/app/javascript/components/DonationBands/DonationBands.js b/app/javascript/components/DonationBands/DonationBands.js index a457765035..e9f933953c 100644 --- a/app/javascript/components/DonationBands/DonationBands.js +++ b/app/javascript/components/DonationBands/DonationBands.js @@ -21,12 +21,14 @@ type Props = { proceed: () => void, intl: IntlShape, selectAmount: (amount: ?number) => void, + selectCustomAmount: (amount: ?number) => void, featuredAmount?: number, }; type State = { customAmount?: number, }; + export class DonationBands extends Component { constructor(props: Props) { super(props); @@ -47,8 +49,8 @@ export class DonationBands extends Component { const number = value.replace(/\D/g, ''); const amount = number ? parseFloat(number) : undefined; this.setState({ customAmount: amount }); - if (this.props.selectAmount) { - this.props.selectAmount(amount); + if (this.props.selectCustomAmount) { + this.props.selectCustomAmount(amount); } } diff --git a/app/javascript/components/ExpressDonation/ExpressDonation.js b/app/javascript/components/ExpressDonation/ExpressDonation.js index 8070332df6..555ffd2ad0 100644 --- a/app/javascript/components/ExpressDonation/ExpressDonation.js +++ b/app/javascript/components/ExpressDonation/ExpressDonation.js @@ -47,6 +47,18 @@ export class ExpressDonation extends Component { }; } + componentDidMount() { + ee.on('one-click:invoke', () => { + console.log('do it!'); + this.submit(); + }); + } + // bind to custom event + + componentWillUnmount() { + ee.off('one-click:invoke'); + } + oneClickData() { if (!this.state.currentPaymentMethod) return null; diff --git a/app/javascript/components/OneClick/OneClick.js b/app/javascript/components/OneClick/OneClick.js new file mode 100644 index 0000000000..b72b9a8bd0 --- /dev/null +++ b/app/javascript/components/OneClick/OneClick.js @@ -0,0 +1,196 @@ +// @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 Button from '../Button/Button'; +import { + changeAmount, + setSubmitting, + oneClickFailed, +} from '../../state/fundraiser/actions'; + +import type { AppState } from '../../state/reducers'; + +type Props = { + donationBands: any, + currency: string, + donationAmount: number, + selectAmount: (amount: number) => void, +}; + +type State = { + amountConfirmationRequired: boolean, +}; + +class OneClick extends Component { + constructor(props: Props) { + super(props); + this.state = { + amountConfirmationRequired: false, + }; + } + + onSelectCustomAmount = async (amount: number) => { + this.props.selectAmount(amount); + this.setState({ amountConfirmationRequired: true }); + }; + + selectAmount = async (amount: number) => { + await this.props.selectAmount(amount); + this.submit(); + }; + + oneClickData() { + return { + payment: { + currency: this.props.currency, + amount: this.props.donationAmount, + recurring: this.props.recurring, + 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) { + ee.emit( + 'fundraiser:transaction_submitted', + data.payment, + this.props.formData + ); + + 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.props.formData); + return reason; + } + + async onSuccess(data: any): any { + ee.emit('fundraiser:transaction_success', data, this.props.formData); + return data; + } + + donateButton() { + if (!this.state.amountConfirmationRequired) return null; + if (!this.props.donationAmount) return null; + + return ( + this.submit()} + /> + ); + } + + procssingView() { + return ( +
+

+ + Processing +

+

+ Please do not close this tab +
+ or use the back button. +

+
+ ); + } + + paymentOptionsView() { + return ( +
+
+
+ +
+
+

{this.props.title}

+
+
+

+ +

+ + {}} + selectAmount={this.selectAmount} + selectCustomAmount={this.onSelectCustomAmount} + /> + + {this.donateButton()} + +
+
+
+ ); + } + + 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, +}); + +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); diff --git a/app/javascript/components/Payment/Payment.js b/app/javascript/components/Payment/Payment.js index 0515074019..e4b1c30460 100644 --- a/app/javascript/components/Payment/Payment.js +++ b/app/javascript/components/Payment/Payment.js @@ -76,6 +76,7 @@ export class Payment extends Component { static title = ; constructor(props: OwnProps) { + console.log('HELLLLLL'); super(props); this.state = { client: null, @@ -373,7 +374,7 @@ export class Payment extends Component { this.props.setSubmitting(s)} - hidden={this.isExpressHidden()} + hidden={false} onHide={() => this.setState({ expressHidden: true })} /> diff --git a/app/javascript/components/WithExpressPayment/WithExpressPayment.js b/app/javascript/components/WithExpressPayment/WithExpressPayment.js new file mode 100644 index 0000000000..c3733a9d15 --- /dev/null +++ b/app/javascript/components/WithExpressPayment/WithExpressPayment.js @@ -0,0 +1,17 @@ +// @flow + +import React, { Component } from 'react'; + +function withExpressPayment(WrappedComponent, selectData) { + return class extends Component { + render() { + return ; + } + + submit() { + console.log('SUBMIT ME'); + } + }; +} + +export default withExpressPayment; diff --git a/app/javascript/fundraiser/FundraiserView.js b/app/javascript/fundraiser/FundraiserView.js index 6e455fcbdd..f335f6318e 100644 --- a/app/javascript/fundraiser/FundraiserView.js +++ b/app/javascript/fundraiser/FundraiserView.js @@ -9,6 +9,7 @@ import StepWrapper from '../components/Stepper/StepWrapper'; import AmountSelection from '../components/AmountSelection/AmountSelection'; import MemberDetailsForm from '../components/MemberDetailsForm/MemberDetailsForm'; import Payment from '../components/Payment/Payment'; +import OneClick from '../components/OneClick/OneClick'; import { changeAmount, changeCurrency, @@ -70,6 +71,7 @@ export class FundraiserView extends Component { currentStep, outstandingFields, submitting, + oneClickError, }, } = this.props; @@ -92,6 +94,23 @@ export class FundraiserView extends Component { 'fundraiser-bar--freestanding': this.props.fundraiser.freestanding, }); + const oneClickErrorMessage = oneClickError ? ( +
+ +
+ ) : null; + + if (this.props.oneClickDonate) { + return ( +
+ +
+ ); + } + return (
{ changeStep={this.props.changeStep} > - this.selectAmount(amount)} - proceed={this.proceed.bind(this)} - /> +
+ {oneClickErrorMessage} + this.selectAmount(amount)} + proceed={this.proceed.bind(this)} + /> +
{this.showStepTwo() && ( @@ -146,10 +168,16 @@ export class FundraiserView extends Component { } export const mapStateToProps = (state: AppState) => ({ + paymentMethods: state.paymentMethods, features: state.features, fundraiser: state.fundraiser, member: state.member, page: state.page, + oneClickError: state.fundraiser.oneClickError, + oneClickDonate: + state.fundraiser.oneClick && + state.paymentMethods.length > 0 && + !state.fundraiser.disableSavedPayments, }); export const mapDispatchToProps = (dispatch: Dispatch<*>) => ({ diff --git a/app/javascript/state/fundraiser/actions.js b/app/javascript/state/fundraiser/actions.js index a98e2adbe5..83ff5f7947 100644 --- a/app/javascript/state/fundraiser/actions.js +++ b/app/javascript/state/fundraiser/actions.js @@ -8,6 +8,10 @@ export function changeAmount(payload: ?number): FundraiserAction { return { type: 'change_amount', payload }; } +export function oneClickFailed(): FundraiserAction { + return { type: 'one_click_failed', payload: {} }; +} + export function changeCurrency(payload: string): FundraiserAction { ee.emit('fundraiser:change_currency', payload); return { type: 'change_currency', payload }; diff --git a/app/javascript/state/fundraiser/reducer.js b/app/javascript/state/fundraiser/reducer.js index e12a85eb52..d6c183cd11 100644 --- a/app/javascript/state/fundraiser/reducer.js +++ b/app/javascript/state/fundraiser/reducer.js @@ -42,6 +42,7 @@ export const initialState: State = { formId: '', formValues: {}, freestanding: false, + oneClick: false, outstandingFields: [], paymentMethods: [], paymentTypes: ['card', 'paypal'], @@ -50,6 +51,7 @@ export const initialState: State = { recurringDefault: 'one_off', storeInVault: false, submitting: false, + oneClickError: false, title: '', }; @@ -66,6 +68,7 @@ export default (state: State = initialState, action: Action): State => { 'title', 'fields', 'freestanding', + 'oneClick', 'donationAmount' ); initialData.formValues = initialData.formValues || {}; @@ -94,6 +97,8 @@ export default (state: State = initialState, action: Action): State => { case 'change_amount': const donationAmount = action.payload || undefined; return { ...state, donationAmount }; + case 'one_click_failed': + return { ...state, disableSavedPayments: true, oneClickError: true }; case 'change_step': return { ...state, currentStep: action.payload }; case 'update_form': { diff --git a/app/javascript/state/fundraiser/types.js b/app/javascript/state/fundraiser/types.js index 5a32d09352..26009af080 100644 --- a/app/javascript/state/fundraiser/types.js +++ b/app/javascript/state/fundraiser/types.js @@ -29,6 +29,7 @@ export type Fundraiser = { formId: string, formValues: Object, freestanding?: boolean, + oneClickError?: boolean, outstandingFields: string[], paymentMethods: any[], paymentTypes: PaymentType[], @@ -67,6 +68,8 @@ export type FundraiserInitializationOptions = { formValues: { [key: string]: string }, formId: string, freestanding: boolean, + oneClick?: boolean, + oneClickError: boolean, outstandingFields: string[], pageId: string, preselectAmount: boolean, diff --git a/app/liquid/liquid_renderer.rb b/app/liquid/liquid_renderer.rb index 5d19f29632..8d3cb41788 100644 --- a/app/liquid/liquid_renderer.rb +++ b/app/liquid/liquid_renderer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -# rubocop:disable Metrics/ClassLength - class LiquidRenderer include Rails.application.routes.url_helpers @@ -37,8 +35,8 @@ def render_follow_up def personalization_data { url_params: @url_params, - member: member_data, - location: location, + member: member_data, + location: location, form_values: form_values, outstanding_fields: outstanding_fields, donation_bands: donation_bands, @@ -55,10 +53,12 @@ def personalization_data private def render_layout(layout, extra_data = {}) - cache = Cache.new(@page.cache_key, layout.try(:cache_key)) - cache.fetch do - Liquid::Template.parse(layout.content).render(markup_data.merge(extra_data.stringify_keys)).html_safe - end + # content = File.read('./app/liquid/views/layouts/greenpeace.liquid') + # cache = Cache.new(@page.cache_key, layout.try(:cache_key)) + # cache.fetch do + Liquid::Template.parse(layout.content).render(markup_data.merge(extra_data.stringify_keys)).html_safe + # end + # Liquid::Template.parse(content).render(markup_data.merge(extra_data.stringify_keys)).html_safe end # this is all of the data that is needed to render the @@ -66,11 +66,11 @@ def render_layout(layout, extra_data = {}) # are not used when rendering markup def markup_data { - images: images, - named_images: named_images, + images: images, + named_images: named_images, primary_image: image_urls(@page.image_to_display), - shares: Shares.get_all(@page), - locale: @page.language&.code || 'en', + shares: Shares.get_all(@page), + locale: @page.language&.code || 'en', follow_up_url: follow_up_url } .merge(@page.liquid_data) @@ -148,6 +148,7 @@ def isolate_from_plugin_data(field) def location return @location if @location.blank? + country_code = if @member.try(:country) && @member.country.length == 2 @member.country else @@ -155,12 +156,14 @@ def location end return @location.data if country_code.blank? return { country: 'US' } if country_code == 'RD' + currency = Donations::Utils.currency_from_country_code(country_code) @location.data.merge(currency: currency, country: country_code) end def image_urls(img) return { urls: { large: '', small: '', original: '' } } if img.blank? || img.content.blank? + { urls: { large: img.content.url(:large), small: img.content.url(:thumb), original: img.content.url(:original) } } end diff --git a/app/views/plugins/fundraisers/_fundraiser.liquid b/app/views/plugins/fundraisers/_fundraiser.liquid index efeee4f9e1..2530032ab7 100644 --- a/app/views/plugins/fundraisers/_fundraiser.liquid +++ b/app/views/plugins/fundraisers/_fundraiser.liquid @@ -30,7 +30,8 @@ preselectAmount: {{ plugins.fundraiser[ref].preselect_amount }}, fields: {{ plugins.fundraiser[ref].fields | jsonify }}, recurringDefault: global.urlParams.recurring_default || "{{ plugins.fundraiser[ref].recurring_default }}", - freestanding: {% if freestanding %} true {% else %} false {% endif %} + freestanding: {% if freestanding %} true {% else %} false {% endif %}, + oneClick: {% if one_click %} true {% else %} false {% endif %} } }; window.setTimeout(function(){ diff --git a/config/locales/member_facing.en.yml b/config/locales/member_facing.en.yml index c34fb265b8..8648afca50 100644 --- a/config/locales/member_facing.en.yml +++ b/config/locales/member_facing.en.yml @@ -60,6 +60,8 @@ en: target_full_name: '[Select your representative above]' fundraiser: + one_click_warning: "Your donation will be processed immediately." + one_click_failed: "We're sorry but we could not process your donation. Please try again with a different card." payment_methods: gocardless: "Direct Debit" paypal: "PayPal"