From 50c900c3addb8bb57a1b1ec4934fe3d546b2ff0c Mon Sep 17 00:00:00 2001 From: Vince Martinez Fasoro Date: Mon, 9 Sep 2019 12:04:09 +0100 Subject: [PATCH 1/8] Get ReCAPTCHA token just before submitting transaction --- app/javascript/components/Payment/Payment.js | 32 +++--------- app/javascript/shared/recaptcha/index.ts | 53 ++++++++++++++++++++ app/javascript/window.d.ts | 6 ++- 3 files changed, 65 insertions(+), 26 deletions(-) create mode 100644 app/javascript/shared/recaptcha/index.ts diff --git a/app/javascript/components/Payment/Payment.js b/app/javascript/components/Payment/Payment.js index 2f78ec1ed..6e8a2dbb6 100644 --- a/app/javascript/components/Payment/Payment.js +++ b/app/javascript/components/Payment/Payment.js @@ -6,6 +6,7 @@ import braintreeClient from 'braintree-web/client'; import dataCollector from 'braintree-web/data-collector'; import { isEmpty } from 'lodash'; import ee from '../../shared/pub_sub'; +import captcha from '../../shared/recaptcha'; import PayPal from '../Braintree/PayPal'; import BraintreeCardFields from '../Braintree/BraintreeCardFields'; @@ -26,7 +27,6 @@ import ExpressDonation from '../ExpressDonation/ExpressDonation'; // Styles import './Payment.css'; -const RECAPTCHA_SITE_KEY = window.champaign.configuration.recaptcha3.siteKey; const BRAINTREE_TOKEN_URL = process.env.BRAINTREE_TOKEN_URL || '/api/payment/braintree/token'; @@ -48,7 +48,6 @@ export class Payment extends Component { }, errors: [], waitingForGoCardless: false, - recaptacha_token: null, }; } @@ -85,27 +84,8 @@ export class Payment extends Component { console.warn('could not fetch Braintree token'); }); this.bindGlobalEvents(); - if (RECAPTCHA_SITE_KEY) this.loadReCaptcha(); } - loadReCaptcha() { - try { - grecaptcha - .execute(RECAPTCHA_SITE_KEY, { action: `donate/${this.props.page.id}` }) - .then( - token => { - this.setState({ - recaptacha_token: token, - }); - }, - error => { - console.warn('Error fetching recaptcha token ', error); - } - ); - } catch (error) { - console.warn('Error trying to execute grecaptcha.', error); - } - } bindGlobalEvents() { ee.on('fundraiser:actions:make_payment', this.makePayment); } @@ -252,13 +232,16 @@ export class Payment extends Component { } }; - submit = data => { + submit = async data => { + const recaptcha_action = `donate/${this.props.page.id}`; + const recaptcha_token = await captcha.execute({ action: recaptcha_action }); + const payload = { ...this.donationData(), payment_method_nonce: data.nonce, device_data: this.state.deviceData, - recaptacha_token: this.state.recaptacha_token, - recaptacha_action: `donate/${this.props.page.id}`, + recaptacha_token, + recaptacha_action, }; this.emitTransactionSubmitted(); @@ -327,7 +310,6 @@ export class Payment extends Component { } else { errors = []; } - if (RECAPTCHA_SITE_KEY) this.loadReCaptcha(); this.setState({ errors: errors }); this.onError(response); }; diff --git a/app/javascript/shared/recaptcha/index.ts b/app/javascript/shared/recaptcha/index.ts new file mode 100644 index 000000000..f6024bc38 --- /dev/null +++ b/app/javascript/shared/recaptcha/index.ts @@ -0,0 +1,53 @@ +const MAX_RETRIES = 4; +export interface IReCaptchaInstance { + ready: (cb: () => any) => void; + execute: (siteKey: string, options: IExecuteOptions) => Promise; +} +interface IExecuteOptions { + action: string; +} + +export const isReady = () => { + return window.grecaptcha != null && window.grecaptcha.execute != null; +}; + +export const load = (): Promise => { + if (isReady()) { + return Promise.resolve(window.grecaptcha as IReCaptchaInstance); + } + + const siteKey = window.champaign.configuration.recaptcha3.siteKey; + + const src = `https://www.google.com/recaptcha/api.js?render=${siteKey}`; + const script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = src; + script.async = true; + script.defer = true; + document.body.appendChild(script); + + return new Promise((resolve, reject) => { + let retries = 0; + const id = setTimeout(() => { + if (window.grecaptcha) { + resolve(window.grecaptcha); + } else if (retries >= MAX_RETRIES) { + reject(new Error('Could not load reCAPTCHA')); + } else { + retries = retries + 1; + } + }, 400); + }); +}; + +export const execute = async (options: IExecuteOptions): Promise => { + const siteKey = window.champaign.configuration.recaptcha3.siteKey; + const captcha = await load(); + return captcha.execute(siteKey, options); +}; + +export default { + load, + isReady, + execute, +}; diff --git a/app/javascript/window.d.ts b/app/javascript/window.d.ts index 594f2efcf..ce04a2993 100644 --- a/app/javascript/window.d.ts +++ b/app/javascript/window.d.ts @@ -4,22 +4,26 @@ import { Store } from 'redux'; import { Fundraiser } from './plugins/fundraiser'; import { Petition } from './plugins/petition'; import Plugin, { IPluginConfig } from './plugins/plugin'; +import { IReCaptchaInstance } from './shared/recaptcha'; import { IAppState, IFormField } from './types'; declare global { // tslint:disable-next-line:interface-name interface Window { + grecaptcha?: IReCaptchaInstance; champaign: IChampaignGlobalObject; I18n: II18n & typeof I18n; store: Store; ee: EventEmitter; } } - interface IChampaignGlobalObject { configuration: { environment: string; defaultCurrency: string; + recaptcha3: { + siteKey: string; + }; [key: string]: any; }; page: IChampaignPage; From 5a945e9cde49425ec8d782ea5127f2772df83fbd Mon Sep 17 00:00:00 2001 From: Vince Martinez Fasoro Date: Mon, 9 Sep 2019 12:08:32 +0100 Subject: [PATCH 2/8] Clear timeout if captcha.load resolves or rejects --- app/javascript/shared/recaptcha/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/javascript/shared/recaptcha/index.ts b/app/javascript/shared/recaptcha/index.ts index f6024bc38..bc53c2f74 100644 --- a/app/javascript/shared/recaptcha/index.ts +++ b/app/javascript/shared/recaptcha/index.ts @@ -30,9 +30,11 @@ export const load = (): Promise => { let retries = 0; const id = setTimeout(() => { if (window.grecaptcha) { - resolve(window.grecaptcha); + clearTimeout(id); + return resolve(window.grecaptcha); } else if (retries >= MAX_RETRIES) { - reject(new Error('Could not load reCAPTCHA')); + clearTimeout(id); + return reject(new Error('Could not load reCAPTCHA')); } else { retries = retries + 1; } From c6ec06cffbd8f26a83bddcceaf7a827b052bcfcc Mon Sep 17 00:00:00 2001 From: Vince Martinez Fasoro Date: Mon, 9 Sep 2019 12:49:03 +0100 Subject: [PATCH 3/8] Fix variable name typo --- app/controllers/api/payment/braintree_controller.rb | 2 +- app/javascript/components/Payment/Payment.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/payment/braintree_controller.rb b/app/controllers/api/payment/braintree_controller.rb index 484e81d5d..abbdd9f0c 100644 --- a/app/controllers/api/payment/braintree_controller.rb +++ b/app/controllers/api/payment/braintree_controller.rb @@ -92,7 +92,7 @@ def valid_user?(user) def verify_bot action = 'donate/' + params[:page_id] - @captcha = Recaptcha3.new(token: params[:recaptacha_token], action: action) + @captcha = Recaptcha3.new(token: params[:recaptcha_token], action: action) unless @captcha.human? msg = @captcha.errors.present? ? @captcha.errors : 'Invalid request' diff --git a/app/javascript/components/Payment/Payment.js b/app/javascript/components/Payment/Payment.js index 6e8a2dbb6..c198f150f 100644 --- a/app/javascript/components/Payment/Payment.js +++ b/app/javascript/components/Payment/Payment.js @@ -240,8 +240,8 @@ export class Payment extends Component { ...this.donationData(), payment_method_nonce: data.nonce, device_data: this.state.deviceData, - recaptacha_token, - recaptacha_action, + recaptcha_token, + recaptcha_action, }; this.emitTransactionSubmitted(); From 1ca6a57676e85d0306f9aa860dda01276d165227 Mon Sep 17 00:00:00 2001 From: Vince Martinez Fasoro Date: Mon, 9 Sep 2019 13:20:06 +0100 Subject: [PATCH 4/8] Make g-recaptcha logo visible --- app/javascript/shared/recaptcha/index.ts | 2 ++ app/javascript/shared/recaptcha/recaptcha.css | 3 +++ 2 files changed, 5 insertions(+) create mode 100644 app/javascript/shared/recaptcha/recaptcha.css diff --git a/app/javascript/shared/recaptcha/index.ts b/app/javascript/shared/recaptcha/index.ts index bc53c2f74..59418b490 100644 --- a/app/javascript/shared/recaptcha/index.ts +++ b/app/javascript/shared/recaptcha/index.ts @@ -1,3 +1,5 @@ +import './recaptcha.css'; + const MAX_RETRIES = 4; export interface IReCaptchaInstance { ready: (cb: () => any) => void; diff --git a/app/javascript/shared/recaptcha/recaptcha.css b/app/javascript/shared/recaptcha/recaptcha.css new file mode 100644 index 000000000..c7d0c3299 --- /dev/null +++ b/app/javascript/shared/recaptcha/recaptcha.css @@ -0,0 +1,3 @@ +.grecaptcha-badge { + z-index: 1000; +} From 21b2056e94b73aa99c7e851253c707cfdfa8cdf1 Mon Sep 17 00:00:00 2001 From: Vince Martinez Fasoro Date: Mon, 9 Sep 2019 13:54:15 +0100 Subject: [PATCH 5/8] Hide reCAPTCHA logo, display branding text --- app/javascript/components/Payment/Payment.js | 3 +++ app/javascript/shared/recaptcha/recaptcha.css | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/javascript/components/Payment/Payment.js b/app/javascript/components/Payment/Payment.js index c198f150f..6b62253d8 100644 --- a/app/javascript/components/Payment/Payment.js +++ b/app/javascript/components/Payment/Payment.js @@ -15,6 +15,7 @@ import WelcomeMember from '../WelcomeMember/WelcomeMember'; import DonateButton from '../DonateButton'; import Checkbox from '../Checkbox/Checkbox'; import ShowIf from '../ShowIf'; +import ReCaptchaBranding from '../ReCaptchaBranding'; import { resetMember } from '../../state/member/reducer'; import { changeStep, @@ -455,6 +456,8 @@ export class Payment extends Component { DIRECT Debit )} + + ); } diff --git a/app/javascript/shared/recaptcha/recaptcha.css b/app/javascript/shared/recaptcha/recaptcha.css index c7d0c3299..545f6b084 100644 --- a/app/javascript/shared/recaptcha/recaptcha.css +++ b/app/javascript/shared/recaptcha/recaptcha.css @@ -1,3 +1,3 @@ .grecaptcha-badge { - z-index: 1000; + visibility: hidden; } From 6c16728fc47b4e0413d0883556bc0cc580c85a72 Mon Sep 17 00:00:00 2001 From: Vince Martinez Fasoro Date: Mon, 9 Sep 2019 14:41:02 +0100 Subject: [PATCH 6/8] Localise reCAPTCHA branding message --- app/javascript/components/Payment/Payment.css | 8 +++++-- app/javascript/components/Payment/Payment.js | 3 +-- .../components/ReCaptchaBranding.tsx | 21 +++++++++++++++++++ config/locales/member_facing.de.yml | 1 + config/locales/member_facing.en.yml | 1 + config/locales/member_facing.es.yml | 1 + config/locales/member_facing.fr.yml | 1 + 7 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 app/javascript/components/ReCaptchaBranding.tsx diff --git a/app/javascript/components/Payment/Payment.css b/app/javascript/components/Payment/Payment.css index f8726034b..3ffeb5b68 100644 --- a/app/javascript/components/Payment/Payment.css +++ b/app/javascript/components/Payment/Payment.css @@ -46,6 +46,10 @@ padding-bottom: 15px; } +.Payment__fine-print .ReCaptchaBranding { + margin-top: 5px; +} + .Payment .gpay-button { width: 100%; height: 60px; @@ -91,8 +95,8 @@ box-shadow: none; } -@media(max-width: 700px) { +@media (max-width: 700px) { .PaymentExpressDonationConflict { width: 100%; } -} \ No newline at end of file +} diff --git a/app/javascript/components/Payment/Payment.js b/app/javascript/components/Payment/Payment.js index 6b62253d8..38b413221 100644 --- a/app/javascript/components/Payment/Payment.js +++ b/app/javascript/components/Payment/Payment.js @@ -449,6 +449,7 @@ export class Payment extends Component { For further information, please contact info@sumofus.org. `} /> + {this.props.showDirectDebit && ( @@ -456,8 +457,6 @@ export class Payment extends Component { DIRECT Debit )} - - ); } diff --git a/app/javascript/components/ReCaptchaBranding.tsx b/app/javascript/components/ReCaptchaBranding.tsx new file mode 100644 index 000000000..4269824b7 --- /dev/null +++ b/app/javascript/components/ReCaptchaBranding.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { FormattedHTMLMessage } from 'react-intl'; + +const ReCaptchaBranding = () => ( +

+ + {'This site is protected by reCAPTCHA and the Google '} + Privacy Policy + {' and '} + Terms of Service + {' apply.'} +

+ } + /> +

+); + +export default ReCaptchaBranding; diff --git a/config/locales/member_facing.de.yml b/config/locales/member_facing.de.yml index b150ffe98..d429124a5 100644 --- a/config/locales/member_facing.de.yml +++ b/config/locales/member_facing.de.yml @@ -262,3 +262,4 @@ de: message: 'Cookies erleichtern die Nutzung unserer Webseite.' dismiss_button_text: 'Einverstanden, weiter' privacy_policy_link_text: 'Datenschutz' + recaptcha_branding_html: 'Diese Website ist durch reCAPTCHA geschützt und es gelten die Datenschutzbestimmungen and Nutzungsbedingungen von Google.' diff --git a/config/locales/member_facing.en.yml b/config/locales/member_facing.en.yml index 26ec61d89..23aae7a57 100644 --- a/config/locales/member_facing.en.yml +++ b/config/locales/member_facing.en.yml @@ -269,3 +269,4 @@ en: message: 'Cookies are important to the proper functioning of this site.' dismiss_button_text: 'Agree & Proceed' privacy_policy_link_text: 'Privacy Policy' + recaptcha_branding_html: 'This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.' diff --git a/config/locales/member_facing.es.yml b/config/locales/member_facing.es.yml index 9263d1d3b..6454ffac0 100644 --- a/config/locales/member_facing.es.yml +++ b/config/locales/member_facing.es.yml @@ -270,3 +270,4 @@ es: message: 'Las cookies son importantes para el funcionamiento de este sitio.' dismiss_button_text: 'Aceptar & Continuar' privacy_policy_link_text: 'Política de privacidad' + recaptcha_branding_html: 'Este sitio está protegido por reCAPTCHA y se aplican la Política de Privacidad y los Términos de Servicio de Google.' diff --git a/config/locales/member_facing.fr.yml b/config/locales/member_facing.fr.yml index 5e2184b5c..dabd2ca09 100644 --- a/config/locales/member_facing.fr.yml +++ b/config/locales/member_facing.fr.yml @@ -253,3 +253,4 @@ fr: message: 'Les cookies sont importants pour le bon fonctionnement de notre site.' dismiss_button_text: 'Accepter et continuer' privacy_policy_link_text: 'Mentions Légales' + recaptcha_branding_html: 'Ce site est protégé par reCAPTCHA. Les règles de confidentialité et les conditions d'utilisation de Google s'appliquent.' From 631ea2c0ab38447236b4ca6d9ba2df735eb8978d Mon Sep 17 00:00:00 2001 From: Omar Sahyoun Date: Mon, 9 Sep 2019 16:20:39 +0100 Subject: [PATCH 7/8] Wrap in double quotes --- config/locales/member_facing.fr.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/locales/member_facing.fr.yml b/config/locales/member_facing.fr.yml index dabd2ca09..caa62fc17 100644 --- a/config/locales/member_facing.fr.yml +++ b/config/locales/member_facing.fr.yml @@ -248,9 +248,8 @@ fr: opt_in_reason: "

Nous demandons aux membres SumOfUs de confirmer qu’ils/elles souhaitent toujours recevoir nos courriels à ce sujet et concernant d’autres campagnes urgentes.

Souhaitez-vous confirmer aujourd’hui?

" decline: "Pas maintenant" accept: "Oui" - cookie_consent: message: 'Les cookies sont importants pour le bon fonctionnement de notre site.' dismiss_button_text: 'Accepter et continuer' privacy_policy_link_text: 'Mentions Légales' - recaptcha_branding_html: 'Ce site est protégé par reCAPTCHA. Les règles de confidentialité et les conditions d'utilisation de Google s'appliquent.' + recaptcha_branding_html: "Ce site est protégé par reCAPTCHA. Les règles de confidentialité et les conditions d'utilisation de Google s'appliquent." \ No newline at end of file From afef217b63835c0b02fc29708aeff10a8e2261cc Mon Sep 17 00:00:00 2001 From: Omar Sahyoun Date: Mon, 9 Sep 2019 16:31:28 +0100 Subject: [PATCH 8/8] Updates snapshots --- .../DonationBands/__snapshots__/DonationBands.test.js.snap | 2 ++ .../components/__snapshots__/ComponentWrapper.test.js.snap | 1 + 2 files changed, 3 insertions(+) diff --git a/app/javascript/components/DonationBands/__snapshots__/DonationBands.test.js.snap b/app/javascript/components/DonationBands/__snapshots__/DonationBands.test.js.snap index 7c1bb9530..10b7836a8 100644 --- a/app/javascript/components/DonationBands/__snapshots__/DonationBands.test.js.snap +++ b/app/javascript/components/DonationBands/__snapshots__/DonationBands.test.js.snap @@ -202,6 +202,7 @@ Thanks so much for everything you do!", "petition.sign_it": "Sign the petition", "petition.target_prefix": "TO", "petition.thank_you": "Thanks for adding your name to \\"{petition_title}\\"", + "recaptcha_branding_html": "This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.", "recommend_pages.actions": "{action_count} actions", "recommend_pages.learn_more": "learn more", "recommend_pages.recommend_pages_title": "Here are other similar campaigns we need your support on!", @@ -645,6 +646,7 @@ Thanks so much for everything you do!", "petition.sign_it": "Sign the petition", "petition.target_prefix": "TO", "petition.thank_you": "Thanks for adding your name to \\"{petition_title}\\"", + "recaptcha_branding_html": "This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.", "recommend_pages.actions": "{action_count} actions", "recommend_pages.learn_more": "learn more", "recommend_pages.recommend_pages_title": "Here are other similar campaigns we need your support on!", diff --git a/app/javascript/components/__snapshots__/ComponentWrapper.test.js.snap b/app/javascript/components/__snapshots__/ComponentWrapper.test.js.snap index 8ba6fec5e..41898f1ba 100644 --- a/app/javascript/components/__snapshots__/ComponentWrapper.test.js.snap +++ b/app/javascript/components/__snapshots__/ComponentWrapper.test.js.snap @@ -188,6 +188,7 @@ Thanks so much for everything you do!", "petition.sign_it": "Sign the petition", "petition.target_prefix": "TO", "petition.thank_you": "Thanks for adding your name to \\"{petition_title}\\"", + "recaptcha_branding_html": "This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.", "recommend_pages.actions": "{action_count} actions", "recommend_pages.learn_more": "learn more", "recommend_pages.recommend_pages_title": "Here are other similar campaigns we need your support on!",