From a6448b0261646eb2ed8250b43a7111e6a9a104b5 Mon Sep 17 00:00:00 2001 From: NejcZdovc Date: Wed, 23 May 2018 12:05:01 +0200 Subject: [PATCH] Adds capctha to the grant flow Resolves #14188 Auditors: Test Plan: --- app/browser/api/ledger.js | 56 +++++- app/browser/reducers/ledgerReducer.js | 12 +- app/common/constants/promotionStatuses.js | 12 ++ app/common/state/ledgerState.js | 5 + .../brave/img/ledger/BAT_captcha_dragicon.png | Bin 0 -> 4003 bytes .../locales/en-US/preferences.properties | 5 + .../components/preferences/payment/captcha.js | 182 ++++++++++++++++++ .../preferences/payment/enabledContent.js | 43 ++++- js/actions/appActions.js | 19 +- js/constants/appConstants.js | 2 + test/unit/about/preferencesTest.js | 1 + .../components/preferences/paymentsTabTest.js | 1 + 12 files changed, 323 insertions(+), 15 deletions(-) create mode 100644 app/common/constants/promotionStatuses.js create mode 100644 app/extensions/brave/img/ledger/BAT_captcha_dragicon.png create mode 100644 app/renderer/components/preferences/payment/captcha.js diff --git a/app/browser/api/ledger.js b/app/browser/api/ledger.js index fcc7917987e..5d668b3fd76 100644 --- a/app/browser/api/ledger.js +++ b/app/browser/api/ledger.js @@ -56,6 +56,7 @@ const windowState = require('../../common/state/windowState') const {makeImmutable, makeJS, isList} = require('../../common/state/immutableUtil') const siteHacks = require('../../siteHacks') const UrlUtil = require('../../../js/lib/urlutil') +const promotionStatuses = require('../../common/constants/promotionStatuses') // Caching let locationDefault = 'NOOP' @@ -1520,6 +1521,7 @@ const roundTripFromWindow = (params, callback) => { * @param {object} params.payload - payload that we want to send to the server * @param {object} params.headers - HTTP headers * @param {string} params.path - relative path to requested url + * @param {boolean} params.binaryP - are we receiving raw payload back * @param {object} options * @param {boolean} options.verboseP - tells us if we want to log the process or not * @param {object} options.headers - headers that are used in the request.request @@ -1534,7 +1536,7 @@ const roundtrip = (params, options, callback) => { let parts = typeof params.server === 'string' ? urlParse(params.server) : typeof params.server !== 'undefined' ? params.server : typeof options.server === 'string' ? urlParse(options.server) : options.server - const binaryP = options.binaryP + const binaryP = options.binaryP || params.binaryP const rawP = binaryP || options.rawP || options.scrapeP if (!params.method) params.method = 'GET' @@ -2925,7 +2927,7 @@ const getPromotion = (state) => { }) } -const claimPromotion = (state) => { +const getCaptcha = (state) => { if (!client) { return } @@ -2935,7 +2937,45 @@ const claimPromotion = (state) => { return } - client.setPromotion(promotion.get('promotionId'), (err, _, status) => { + client.getPromotionCaptcha(promotion.get('promotionId'), (err, body) => { + if (err) { + console.error(`Problem getting promotion captcha ${err.toString()}`) + appActions.onCaptchaResponse(null) + } + + appActions.onCaptchaResponse(body) + }) +} + +const onCaptchaResponse = (state, body) => { + if (body == null) { + // TODO handle this problem + return state + } + + const image = `data:image/jpeg;base64,${Buffer.from(body).toString('base64')}` + + state = ledgerState.setPromotionProp(state, 'captcha', image) + const currentStatus = ledgerState.getPromotionProp(state, 'promotionStatus') + + if (currentStatus !== promotionStatuses.CAPTCHA_ERROR) { + state = ledgerState.setPromotionProp(state, 'promotionStatus', promotionStatuses.CAPTCHA_CHECK) + } + + return state +} + +const claimPromotion = (state, x, y) => { + if (!client) { + return + } + + const promotion = ledgerState.getPromotion(state) + if (promotion.isEmpty()) { + return + } + + client.setPromotion(promotion.get('promotionId'), {x, y}, (err, _, status) => { let param = null if (err) { console.error(`Problem claiming promotion ${err.toString()}`) @@ -2950,10 +2990,14 @@ const onPromotionResponse = (state, status) => { if (status) { if (status.get('statusCode') === 422) { // promotion already claimed - state = ledgerState.setPromotionProp(state, 'promotionStatus', 'expiredError') + state = ledgerState.setPromotionProp(state, 'promotionStatus', promotionStatuses.PROMO_EXPIRED) + } else if (status.get('statusCode') === 403) { + // captcha verification failed + state = ledgerState.setPromotionProp(state, 'promotionStatus', promotionStatuses.CAPTCHA_ERROR) + getCaptcha(state) } else { // general error - state = ledgerState.setPromotionProp(state, 'promotionStatus', 'generalError') + state = ledgerState.setPromotionProp(state, 'promotionStatus', promotionStatuses.GENERAL_ERROR) } return state } @@ -3120,6 +3164,8 @@ const getMethods = () => { processMediaData, addNewLocation, addSiteVisit, + getCaptcha, + onCaptchaResponse, shouldTrackTab } diff --git a/app/browser/reducers/ledgerReducer.js b/app/browser/reducers/ledgerReducer.js index 0d03878afc2..48b11b80a72 100644 --- a/app/browser/reducers/ledgerReducer.js +++ b/app/browser/reducers/ledgerReducer.js @@ -431,9 +431,19 @@ const ledgerReducer = (state, action, immutableAction) => { state = ledgerNotifications.onPromotionReceived(state) break } + case appConstants.APP_ON_PROMOTION_CLICK: + { + ledgerApi.getCaptcha(state) + break + } + case appConstants.APP_ON_CAPTCHA_RESPONSE: + { + state = ledgerApi.onCaptchaResponse(state, action.get('body')) + break + } case appConstants.APP_ON_PROMOTION_CLAIM: { - ledgerApi.claimPromotion(state) + ledgerApi.claimPromotion(state, action.get('x'), action.get('y')) break } case appConstants.APP_ON_PROMOTION_REMIND: diff --git a/app/common/constants/promotionStatuses.js b/app/common/constants/promotionStatuses.js new file mode 100644 index 00000000000..767f42e4834 --- /dev/null +++ b/app/common/constants/promotionStatuses.js @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const statuses = { + GENERAL_ERROR: 'generalError', + PROMO_EXPIRED: 'expiredError', + CAPTCHA_CHECK: 'captchaCheck', + CAPTCHA_ERROR: 'captchaError' +} + +module.exports = statuses diff --git a/app/common/state/ledgerState.js b/app/common/state/ledgerState.js index 760091c2cab..53298da550c 100644 --- a/app/common/state/ledgerState.js +++ b/app/common/state/ledgerState.js @@ -541,6 +541,7 @@ const ledgerState = { let promotion = ledgerState.getActivePromotion(state) const claim = state.getIn(['ledger', 'promotion', 'claimedTimestamp']) || null const status = state.getIn(['ledger', 'promotion', 'promotionStatus']) || null + const captcha = state.getIn(['ledger', 'promotion', 'captcha']) || null if (claim) { promotion = promotion.set('claimedTimestamp', claim) @@ -550,6 +551,10 @@ const ledgerState = { promotion = promotion.set('promotionStatus', status) } + if (captcha) { + promotion = promotion.set('captcha', captcha) + } + return promotion }, diff --git a/app/extensions/brave/img/ledger/BAT_captcha_dragicon.png b/app/extensions/brave/img/ledger/BAT_captcha_dragicon.png new file mode 100644 index 0000000000000000000000000000000000000000..fc8c7199eb2df227d7e8d8c94de307cfc70404c7 GIT binary patch literal 4003 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 { this.captchaBox = node }} + > + { +
+ } +

+ + +

+ +
+

+

+ } +} + +const styles = StyleSheet.create({ + enabledContent__overlay: { + position: 'absolute', + zIndex: 3, + top: 0, + left: 0, + width: '100%', + minHeight: '159px', + background: '#f3f3f3', + borderRadius: '8px', + padding: '27px 50px 17px', + boxSizing: 'border-box', + boxShadow: '4px 6px 3px #dadada' + }, + + enabledContent__overlay_close: { + position: 'absolute', + right: '15px', + top: '15px', + height: '15px', + width: '15px', + cursor: 'pointer', + + background: `url(${closeButton}) center no-repeat`, + backgroundSize: `15px`, + + ':focus': { + outline: 'none' + } + }, + + enabledContent__overlay_title: { + color: '#5f5f5f', + fontSize: '20px', + display: 'block', + marginBottom: '10px' + }, + + enabledContent__overlay_bold: { + color: '#ff5500', + paddingRight: '5px' + }, + + enabledContent__overlay_text: { + fontSize: '16px', + color: '#828282', + maxWidth: '700px', + lineHeight: '25px', + padding: '5px 5px 5px 0', + marginTop: '10px' + }, + + enabledContent__captcha: { + // TODO we need to make sure that text is not in DND zone + width: '805px', + height: '180px', + padding: '20px' + }, + + enabledContent__captcha__drop: { + position: 'absolute', + width: '400px', + height: '180px', + top: 0, + right: 0, + zIndex: 2, + display: 'block' + }, + + enabledContent__captcha__image: { + marginTop: '10px' + } +}) + +module.exports = Captcha diff --git a/app/renderer/components/preferences/payment/enabledContent.js b/app/renderer/components/preferences/payment/enabledContent.js index c089be4f07b..396ba02c74a 100644 --- a/app/renderer/components/preferences/payment/enabledContent.js +++ b/app/renderer/components/preferences/payment/enabledContent.js @@ -13,6 +13,7 @@ const BrowserButton = require('../../common/browserButton') const {FormTextbox} = require('../../common/textbox') const {FormDropdown} = require('../../common/dropdown') const LedgerTable = require('./ledgerTable') +const Captcha = require('./captcha') // State const ledgerState = require('../../../../common/state/ledgerState') @@ -41,6 +42,7 @@ const globalStyles = require('../../styles/global') const cx = require('../../../../../js/lib/classSet') const {paymentStylesVariables} = require('../../styles/payment') const closeButton = require('../../../../../img/toolbar/stoploading_btn.svg') +const promotionStatuses = require('../../../../common/constants/promotionStatuses') // TODO: report when funds are too low class EnabledContent extends ImmutableComponent { @@ -93,7 +95,7 @@ class EnabledContent extends ImmutableComponent { } onClaimClick () { - appActions.onPromotionClaim() + appActions.onPromotionClick() } claimButton () { @@ -227,7 +229,7 @@ class EnabledContent extends ImmutableComponent { const promo = this.props.ledgerData.get('promotion') || Immutable.Map() const status = promo.get('promotionStatus') if (status && !promo.has('claimedTimestamp')) { - if (status === 'expiredError') { + if (status === promotionStatuses.PROMO_EXPIRED) { appActions.onPromotionRemoval() } else { appActions.onPromotionClose() @@ -245,6 +247,10 @@ class EnabledContent extends ImmutableComponent { ) } + captchaOverlay (promo) { + return + } + statusMessage () { const promo = this.props.ledgerData.get('promotion') || Immutable.Map() const status = this.props.ledgerData.get('status') || '' @@ -275,20 +281,25 @@ class EnabledContent extends ImmutableComponent { if (promotionStatus) { switch (promotionStatus) { - case 'generalError': + case promotionStatuses.GENERAL_ERROR: { title = locale.translation('promotionGeneralErrorTitle') message = locale.translation('promotionGeneralErrorMessage') text = locale.translation('promotionGeneralErrorText') break } - case 'expiredError': + case promotionStatuses.PROMO_EXPIRED: { title = locale.translation('promotionClaimedErrorTitle') message = locale.translation('promotionClaimedErrorMessage') text = locale.translation('promotionClaimedErrorText') break } + case promotionStatuses.CAPTCHA_CHECK: + case promotionStatuses.CAPTCHA_ERROR: + { + return this.captchaOverlay(promo) + } } } } else { @@ -313,7 +324,6 @@ class EnabledContent extends ImmutableComponent { /> break } - case ledgerStatuses.SERVER_PROBLEM: { showClose = false @@ -337,7 +347,8 @@ class EnabledContent extends ImmutableComponent { /> : null }

- {title} {message} + {title} + {message}

{text} @@ -594,7 +605,8 @@ const styles = StyleSheet.create({ }, enabledContent__overlay_bold: { - color: '#ff5500' + color: '#ff5500', + paddingRight: '5px' }, enabledContent__overlay_text: { @@ -632,6 +644,23 @@ const styles = StyleSheet.create({ } }, + enabledContent__captcha: { + // TODO we need to make sure that text is not in DND zone + width: '805px', + height: '180px', + padding: '20px' + }, + + enabledContent__captcha__drop: { + position: 'absolute', + width: '400px', + height: '180px', + top: 0, + right: 0, + zIndex: 2, + display: 'block' + }, + enabledContent__walletBar: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', diff --git a/js/actions/appActions.js b/js/actions/appActions.js index 8413effb3bd..e5851a5d3e5 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -1864,9 +1864,24 @@ const appActions = { }) }, - onPromotionClaim: function () { + onPromotionClick: function () { dispatch({ - actionType: appConstants.APP_ON_PROMOTION_CLAIM + actionType: appConstants.APP_ON_PROMOTION_CLICK + }) + }, + + onCaptchaResponse: function (body) { + dispatch({ + actionType: appConstants.APP_ON_CAPTCHA_RESPONSE, + body + }) + }, + + onPromotionClaim: function (x, y) { + dispatch({ + actionType: appConstants.APP_ON_PROMOTION_CLAIM, + x, + y }) }, diff --git a/js/constants/appConstants.js b/js/constants/appConstants.js index cd19b2e5ed9..f85bca87a38 100644 --- a/js/constants/appConstants.js +++ b/js/constants/appConstants.js @@ -185,6 +185,8 @@ const appConstants = { APP_ON_PUBLISHER_TIMESTAMP: _, APP_SAVE_LEDGER_PROMOTION: _, APP_ON_PROMOTION_CLAIM: _, + APP_ON_PROMOTION_CLICK: _, + APP_ON_CAPTCHA_RESPONSE: _, APP_ON_PROMOTION_RESPONSE: _, APP_ON_FETCH_REFERRAL_HEADERS: _, APP_ON_PROMOTION_REMIND: _, diff --git a/test/unit/about/preferencesTest.js b/test/unit/about/preferencesTest.js index 3ef3742f174..bfe604342a0 100644 --- a/test/unit/about/preferencesTest.js +++ b/test/unit/about/preferencesTest.js @@ -57,6 +57,7 @@ describe('Preferences component unittest', function () { // Mocks the icon used in payments tab mockery.registerMock('../../../extensions/brave/img/ledger/cryptoIcons/BAT_icon.svg') mockery.registerMock('../../../../../img/toolbar/stoploading_btn.svg') + mockery.registerMock('../../../../extensions/brave/img/ledger/BAT_captcha_dragicon.png') // Mocks the icons used in addFundsDialog and its steps mockery.registerMock('../../../../../../extensions/brave/img/ledger/wallet_icon.svg') mockery.registerMock('../../../../../../extensions/brave/img/ledger/cryptoIcons/ETH_icon.svg') diff --git a/test/unit/app/renderer/components/preferences/paymentsTabTest.js b/test/unit/app/renderer/components/preferences/paymentsTabTest.js index 4c7b9b12b65..56c962c315b 100644 --- a/test/unit/app/renderer/components/preferences/paymentsTabTest.js +++ b/test/unit/app/renderer/components/preferences/paymentsTabTest.js @@ -54,6 +54,7 @@ describe('PaymentsTab component', function () { // Mocks the icon used in payments tab mockery.registerMock('../../../extensions/brave/img/ledger/cryptoIcons/BAT_icon.svg') mockery.registerMock('../../../../../img/toolbar/stoploading_btn.svg') + mockery.registerMock('../../../../extensions/brave/img/ledger/BAT_captcha_dragicon.png') // Mocks the icons used in addFundsDialog and its steps mockery.registerMock('../../../../../../extensions/brave/img/ledger/wallet_icon.svg') mockery.registerMock('../../../../../../extensions/brave/img/ledger/cryptoIcons/ETH_icon.svg')