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 00000000000..fc8c7199eb2 Binary files /dev/null and b/app/extensions/brave/img/ledger/BAT_captcha_dragicon.png differ diff --git a/app/extensions/brave/locales/en-US/preferences.properties b/app/extensions/brave/locales/en-US/preferences.properties index cfb5a36be09..fd2144c67b9 100644 --- a/app/extensions/brave/locales/en-US/preferences.properties +++ b/app/extensions/brave/locales/en-US/preferences.properties @@ -259,6 +259,11 @@ printKeys=Print key privacy=Privacy privateData=Private Data privateDataMessage=Clear the following data types when I close Brave +promotionCaptchaTitle=Almost there! +promotionCaptchaErrorTitle=Oh oh! +promotionCaptchaErrorText=Let's try again +promotionCaptchaText=First, prove you are mostly human: +promotionCaptchaMessage=Using your mouse to move the BAT icon into it's home position promotionGeneralErrorMessage=The Brave Payments server is not responding. Please try again later to claim your token grant. promotionGeneralErrorText=Note: This error could also be caused by a network connection problem. promotionGeneralErrorTitle=Uh oh. diff --git a/app/renderer/components/preferences/payment/captcha.js b/app/renderer/components/preferences/payment/captcha.js new file mode 100644 index 00000000000..ab8124841ba --- /dev/null +++ b/app/renderer/components/preferences/payment/captcha.js @@ -0,0 +1,182 @@ +/* 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 React = require('react') +const {StyleSheet, css} = require('aphrodite/no-important') + +// Components +const ImmutableComponent = require('../../immutableComponent') + +// Actions +const appActions = require('../../../../../js/actions/appActions') + +// Constants +const promotionStatuses = require('../../../../common/constants/promotionStatuses') + +// Styles +const cx = require('../../../../../js/lib/classSet') +const closeButton = require('../../../../../img/toolbar/stoploading_btn.svg') +const dragIcon = require('../../../../extensions/brave/img/ledger/BAT_captcha_dragicon.png') + +// TODO: report when funds are too low +class Captcha extends ImmutableComponent { + constructor (props) { + super(props) + this.onCaptchaDrop = this.onCaptchaDrop.bind(this) + this.onCaptchaDrag = this.onCaptchaDrag.bind(this) + this.getText = this.getText.bind(this) + this.captchaBox = null + this.dndStartPosition = { + x: 0, + y: 0 + } + } + + onCaptchaDrop (event) { + event.preventDefault() + const target = this.captchaBox.getBoundingClientRect() + console.log('start X', this.dndStartPosition.x) + console.log('start Y', this.dndStartPosition.y) + console.log('clientX', event.clientX) + console.log('clientY', event.clientY) + console.log('X', (event.clientX - target.left)) + console.log('Y', (event.clientY - target.top)) + console.log('box', target) + + const x = event.clientX - target.left - this.dndStartPosition.x - 400 + const y = event.clientY - target.top - this.dndStartPosition.y + + appActions.onPromotionClaim(x, y) + } + + onCaptchaDrag (event) { + const target = event.target.getBoundingClientRect() + this.dndStartPosition = { + x: event.clientX - target.left, + y: event.clientY - target.top + } + } + + preventDefault (event) { + event.preventDefault() + } + + getText () { + if (this.props.promo.get('promotionStatus') === promotionStatuses.CAPTCHA_ERROR) { + return { + title: 'promotionCaptchaErrorTitle', + text: 'promotionCaptchaErrorText' + } + } + + return { + title: 'promotionCaptchaTitle', + text: 'promotionCaptchaText' + } + } + + render () { + const text = this.getText() + + return
{ 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')