Skip to content

Commit

Permalink
Adds capctha to the grant flow
Browse files Browse the repository at this point in the history
Resolves brave#14188

Auditors:

Test Plan:
  • Loading branch information
NejcZdovc committed May 23, 2018
1 parent 5f7bc31 commit de50a5a
Show file tree
Hide file tree
Showing 15 changed files with 430 additions and 21 deletions.
71 changes: 64 additions & 7 deletions app/browser/api/ledger.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ const promoCodeFirstRunStorage = require('../../promoCodeFirstRunStorage')
const appUrlUtil = require('../../../js/lib/appUrlUtil')
const urlutil = require('../../../js/lib/urlutil')
const windowState = require('../../common/state/windowState')
const {makeImmutable, makeJS, isList} = require('../../common/state/immutableUtil')
const {makeImmutable, makeJS, isList, isImmutable} = require('../../common/state/immutableUtil')
const siteHacks = require('../../siteHacks')
const UrlUtil = require('../../../js/lib/urlutil')
const promotionStatuses = require('../../common/constants/promotionStatuses')

// Caching
let locationDefault = 'NOOP'
Expand Down Expand Up @@ -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
Expand All @@ -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'
Expand Down Expand Up @@ -2925,7 +2927,7 @@ const getPromotion = (state) => {
})
}

const claimPromotion = (state) => {
const getCaptcha = (state) => {
if (!client) {
return
}
Expand All @@ -2935,7 +2937,47 @@ 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
}

console.log('COORDINATES', {x, y})

client.setPromotion(promotion.get('promotionId'), {x, y}, (err, _, status) => {
let param = null
if (err) {
console.error(`Problem claiming promotion ${err.toString()}`)
Expand All @@ -2947,17 +2989,30 @@ const claimPromotion = (state) => {
}

const onPromotionResponse = (state, status) => {
if (status) {
if (status && isImmutable(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)
module.exports.getCaptcha(state)
} else {
// general error
state = ledgerState.setPromotionProp(state, 'promotionStatus', 'generalError')
state = ledgerState.setPromotionProp(state, 'promotionStatus', promotionStatuses.GENERAL_ERROR)
}
return state
}

const currentStatus = ledgerState.getPromotionProp(state, 'promotionStatus')

if (
currentStatus === promotionStatuses.CAPTCHA_ERROR ||
currentStatus === promotionStatuses.CAPTCHA_CHECK
) {
state = ledgerState.setPromotionProp(state, 'promotionStatus', null)
}

ledgerNotifications.removePromotionNotification(state)
state = ledgerState.setPromotionProp(state, 'claimedTimestamp', new Date().getTime())

Expand Down Expand Up @@ -3120,6 +3175,8 @@ const getMethods = () => {
processMediaData,
addNewLocation,
addSiteVisit,
getCaptcha,
onCaptchaResponse,
shouldTrackTab
}

Expand Down
12 changes: 11 additions & 1 deletion app/browser/reducers/ledgerReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
12 changes: 12 additions & 0 deletions app/common/constants/promotionStatuses.js
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions app/common/state/ledgerState.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -550,6 +551,10 @@ const ledgerState = {
promotion = promotion.set('promotionStatus', status)
}

if (captcha) {
promotion = promotion.set('captcha', captcha)
}

return promotion
},

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions app/extensions/brave/locales/en-US/preferences.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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=Hmmm…not quite.
promotionCaptchaErrorText=Please try again.
promotionCaptchaText=First, prove you are mostly human:
promotionCaptchaMessage=Use 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.
Expand Down
186 changes: 186 additions & 0 deletions app/renderer/components/preferences/payment/captcha.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/* 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,
width: 0,
height: 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 + (this.dndStartPosition.height / 2)
const y = event.clientY - target.top - this.dndStartPosition.y + (this.dndStartPosition.width / 2)

appActions.onPromotionClaim(x, y)
}

onCaptchaDrag (event) {
const target = event.target.getBoundingClientRect()
this.dndStartPosition = {
x: event.clientX - target.left,
y: event.clientY - target.top,
width: target.width,
height: target.height
}
}

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 <div
className={cx({
[css(styles.enabledContent__overlay)]: true,
[css(styles.enabledContent__captcha)]: true,
'enabledContent__overlay': true
})}
style={{'background': `url(${this.props.promo.get('captcha')}) no-repeat top left #f3f3f3`}}
ref={(node) => { this.captchaBox = node }}
>
{
<div
className={css(styles.enabledContent__overlay_close)}
onClick={this.closePromotionClick}
/>
}
<p className={css(styles.enabledContent__overlay_title)}>
<span className={css(styles.enabledContent__overlay_bold)} data-l10n-id={text.title} />
<span data-l10n-id={text.text} />
</p>
<img onDragStart={this.onCaptchaDrag} src={dragIcon} draggable='true' className={css(styles.enabledContent__captcha__image)} />
<div onDrop={this.onCaptchaDrop} onDragOver={this.preventDefault} className={css(styles.enabledContent__captcha__drop)} />
<p className={css(styles.enabledContent__overlay_text)} data-l10n-id='promotionCaptchaMessage' />
</div>
}
}

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
Loading

0 comments on commit de50a5a

Please sign in to comment.