Skip to content

Commit

Permalink
feat(signup): add captcha checks to signup and recover flows
Browse files Browse the repository at this point in the history
  • Loading branch information
schnogz committed Jan 7, 2022
1 parent 06af094 commit 2105425
Show file tree
Hide file tree
Showing 16 changed files with 191 additions and 95 deletions.
23 changes: 15 additions & 8 deletions packages/blockchain-wallet-v4-frontend/src/data/auth/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,11 @@ export default ({ api, coreSagas, networks }) => {
}

const loginRoutineSaga = function* ({
country = undefined,
email = undefined,
firstLogin = false,
country = undefined,
state = undefined,
recovery = false
recovery = false,
state = undefined
}) {
try {
// If needed, the user should upgrade its wallet before being able to open the wallet
Expand Down Expand Up @@ -482,17 +482,24 @@ export default ({ api, coreSagas, networks }) => {

const restore = function* (action) {
try {
const { captchaToken, email, language, mnemonic, network, password } = action.payload
const kvCredentials = (yield select(selectors.auth.getMetadataRestore)).getOrElse({})

yield put(actions.auth.restoreLoading())
yield put(actions.auth.setRegisterEmail(action.payload.email))
yield put(actions.auth.setRegisterEmail(email))
yield put(actions.alerts.displayInfo(C.RESTORE_WALLET_INFO))
const kvCredentials = (yield select(selectors.auth.getMetadataRestore)).getOrElse({})
yield call(coreSagas.wallet.restoreWalletSaga, {
...action.payload,
kvCredentials
captchaToken,
email,
kvCredentials,
language,
mnemonic,
network,
password
})

yield call(loginRoutineSaga, {
email: action.payload.email,
email,
firstLogin: true,
recovery: true
})
Expand Down
18 changes: 4 additions & 14 deletions packages/blockchain-wallet-v4-frontend/src/data/auth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ export enum RecoverSteps {
}

export type RecoverFormType = {
email?: string
mnemonic?: string
password: string
recoverPassword?: string
resetPassword?: string
step: RecoverSteps
}

Expand Down Expand Up @@ -95,20 +99,6 @@ export enum UserType {
WALLET_EXCHANGE_NOT_LINKED = 'WALLET_EXCHANGE_NOT_LINKED'
}

export type AuthorzieDeviceMismatchData = {
approver?: {
browser: string
country_code: string
ip_address: string
}
confirmation_required?: boolean
requester?: {
browser: string
country_code: string
ip_address: string
}
success: boolean
}
export type WalletDataFromMagicLink = {
exchange?: {
email?: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import { bindActionCreators } from 'redux'
import styled from 'styled-components'

import { RemoteDataType } from '@core/types'
import { Badge, Icon, Link, Text } from 'blockchain-info-components'
import { Wrapper } from 'components/Public'
import { Badge, Icon, Text } from 'blockchain-info-components'
import QRCodeWrapper from 'components/QRCodeWrapper'
import { actions, selectors } from 'data'
import { RecoverSteps } from 'data/types'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ContactSupportText,
GoBackArrow,
OuterWrapper,
RECOVER_FORM,
SubCard,
TroubleLoggingInRow,
WrapperWithPadding
Expand Down Expand Up @@ -55,17 +56,17 @@ const RecoveryOptions = (props: Props) => {

const cloudRecoveryClicked = () => {
if (hasCloudBackup) {
formActions.change('recover', 'step', RecoverSteps.CLOUD_RECOVERY)
formActions.change(RECOVER_FORM, 'step', RecoverSteps.CLOUD_RECOVERY)
authActions.analyticsRecoveryOptionSelected('CLOUD_BACKUP')
}
}
const recoveryPhraseClicked = () => {
formActions.change('recover', 'step', RecoverSteps.RECOVERY_PHRASE)
formActions.change(RECOVER_FORM, 'step', RecoverSteps.RECOVERY_PHRASE)
authActions.analyticsRecoveryOptionSelected('RECOVERY_PHRASE')
}

const resetAccountClicked = () => {
formActions.change('recover', 'step', RecoverSteps.RESET_ACCOUNT)
formActions.change(RECOVER_FORM, 'step', RecoverSteps.RESET_ACCOUNT)
authActions.analyticsResetAccountClicked('RECOVERY_OPTIONS')
}
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ContactSupportText,
GoBackArrow,
OuterWrapper,
RECOVER_FORM,
SubCard,
TroubleLoggingInRow,
WrapperWithPadding
Expand All @@ -26,7 +27,7 @@ const FormBody = styled.div`
const FirstStep = (props: Props) => {
const { formActions, invalid, nabuId, setStep, submitting } = props
const resetAccountClicked = () => {
formActions.change('recover', 'step', RecoverSteps.RESET_ACCOUNT)
formActions.change(RECOVER_FORM, 'step', RecoverSteps.RESET_ACCOUNT)
props.authActions.analyticsResetAccountClicked('RECOVERY_PHRASE')
}
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import Recover from './template'

class RecoverContainer extends React.PureComponent<Props> {
componentDidMount() {
const { authActions, mnemonic } = this.props
authActions.restoreFromMetadata(mnemonic)
const { authActions, formValues } = this.props
authActions.restoreFromMetadata(formValues.mnemonic)
}

render() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class SecondStep extends React.PureComponent<Props, State> {
}

render() {
const { invalid, isRestoring, isRestoringFromMetadata, recoverPassword } = this.props
const { formValues, invalid, isRestoring, isRestoringFromMetadata } = this.props
return (
<>
{!isRestoringFromMetadata && !this.state.importWalletPrompt && (
Expand Down Expand Up @@ -117,7 +117,11 @@ class SecondStep extends React.PureComponent<Props, State> {
validate={[required, validStrongPassword]}
component={PasswordBox}
showPasswordScore
passwordScore={has('zxcvbn', window) ? window.zxcvbn(recoverPassword).score : 0}
passwordScore={
has('zxcvbn', window)
? window.zxcvbn(formValues.recoverPassword || '').score
: 0
}
/>
</FormGroup>
<FormGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Form } from 'components/Form'
import { LoginSteps } from 'data/types'

import { Props } from '..'
import { RECOVER_FORM } from '../model'
import FirstStep from './FirstStep'
import SecondStep from './SecondStep'

Expand All @@ -15,38 +16,77 @@ class RecoveryPhraseContainer extends React.PureComponent<
constructor(props) {
super(props)
this.state = {
captchaToken: undefined,
step: 1
}
}

componentDidMount() {
this.initCaptcha()
}

componentWillUnmount() {
this.props.formActions.clearFields('recover', false, false, 'mnemonic')
this.props.formActions.clearFields(RECOVER_FORM, false, false, 'mnemonic')
}

setStep = (step: LoginSteps) => {
this.props.formActions.change('recover', 'step', step)
initCaptcha = (callback?) => {
/* eslint-disable */
if (!window.grecaptcha || !window.grecaptcha.enterprise) return
window.grecaptcha.enterprise.ready(() => {
window.grecaptcha.enterprise
.execute(window.CAPTCHA_KEY, { action: 'RECOVER' })
.then((captchaToken) => {
console.log('Captcha success')
this.setState({ captchaToken })
callback && callback(captchaToken)
})
.catch((e) => {
console.error('Captcha error: ', e)
})
})
/* eslint-enable */
}

handleSubmit = (e) => {
e.preventDefault()
const { authActions, email, language, mnemonic, recoverPassword } = this.props
const { captchaToken } = this.state
const { authActions, formValues, language } = this.props

if (this.state.step === 1) {
this.setState({ step: 2 })
} else {
authActions.restore({
email,
language,
mnemonic,
network: undefined,
password: recoverPassword
})
return this.setState({ step: 2 })
}

// sometimes captcha doesnt mount correctly (race condition?)
// if it's undefined, try to re-init for token
if (!captchaToken) {
return this.initCaptcha(
authActions.restore({
captchaToken,
email: formValues.email,
language,
mnemonic: formValues.mnemonic,
password: formValues.recoverPassword
})
)
}
// we have a captcha token, continue recover process
authActions.restore({
captchaToken,
email: formValues.email,
language,
mnemonic: formValues.mnemonic,
password: formValues.recoverPassword
})
}

previousStep = () => {
this.setState({ step: 1 })
}

setStep = (step: LoginSteps) => {
this.props.formActions.change(RECOVER_FORM, 'step', step)
}

render() {
return (
<Form onSubmit={this.handleSubmit}>
Expand All @@ -58,6 +98,7 @@ class RecoveryPhraseContainer extends React.PureComponent<
}

export type StateProps = {
captchaToken?: string
step: number
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const Footer = styled(FormGroup)`
const validatePasswordConfirmation = validPasswordConfirmation('resetAccountPassword')

const SecondStep = (props: Props) => {
const { emailFromMagicLink, invalid, isRegistering, resetPassword, setStep } = props
const { emailFromMagicLink, formValues, invalid, isRegistering, setStep } = props
return (
<>
<BackArrowFormHeader
Expand All @@ -41,7 +41,9 @@ const SecondStep = (props: Props) => {
validate={[required, validStrongPassword]}
component={PasswordBox}
showPasswordScore
passwordScore={has('zxcvbn', window) ? window.zxcvbn(resetPassword).score : 0}
passwordScore={
has('zxcvbn', window) ? window.zxcvbn(formValues.resetPassword || '').score : 0
}
/>
</FormGroup>
<FormGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { bindActionCreators, Dispatch } from 'redux'
import { InjectedFormProps } from 'redux-form'

import { Remote } from '@core'
import { Text } from 'blockchain-info-components'
import { Form } from 'components/Form'
import { Wrapper } from 'components/Public'
import { actions, selectors } from 'data'
Expand All @@ -29,8 +28,8 @@ class ResetAccount extends React.PureComponent<InjectedFormProps<{}, Props> & Pr

handleSubmit = (e) => {
e.preventDefault()
const { authActions, cachedEmail, language, resetPassword } = this.props
authActions.resetAccount({ email: cachedEmail, language, password: resetPassword })
const { authActions, cachedEmail, formValues, language } = this.props
authActions.resetAccount({ email: cachedEmail, language, password: formValues.resetPassword })
}

render() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
import React from 'react'
import { connect, ConnectedProps } from 'react-redux'
import { bindActionCreators, compose } from 'redux'
import { formValueSelector, getFormMeta, InjectedFormProps, reduxForm } from 'redux-form'
import { getFormMeta, InjectedFormProps, reduxForm } from 'redux-form'

import { RemoteDataType } from '@core/types'
import { Form } from 'components/Form'
import { actions, selectors } from 'data'
import { RecoverFormType, RecoverSteps } from 'data/types'

import CloudRecovery from './CloudRecovery'
import { RECOVER_FORM } from './model'
import RecoveryOptions from './RecoveryOptions'
import RecoveryPhrase from './RecoveryPhrase'
import ResetAccount from './ResetAccount'

class RecoverWalletContainer extends React.PureComponent<InjectedFormProps<{}, Props> & Props> {
componentDidMount() {
this.props.formActions.change('recover', 'step', RecoverSteps.RECOVERY_OPTIONS)
this.props.formActions.change(RECOVER_FORM, 'step', RecoverSteps.RECOVERY_OPTIONS)
}

componentWillUnmount() {
this.props.formActions.destroy('recover')
this.props.formActions.destroy(RECOVER_FORM)
}

setStep = (step: RecoverSteps) => {
this.props.formActions.change('recover', 'step', step)
this.props.formActions.change(RECOVER_FORM, 'step', step)
}

render() {
Expand Down Expand Up @@ -52,20 +53,16 @@ class RecoverWalletContainer extends React.PureComponent<InjectedFormProps<{}, P
const mapStateToProps = (state) => ({
cachedEmail: selectors.cache.getEmail(state),
cachedGuid: selectors.cache.getStoredGuid(state),
email: formValueSelector('recover')(state, 'email'),
emailFromMagicLink: selectors.auth.getMagicLinkData(state)?.wallet?.email as string,
formMeta: getFormMeta('recover')(state),
formValues: selectors.form.getFormValues('recover')(state) as RecoverFormType,
formMeta: getFormMeta(RECOVER_FORM)(state),
formValues: selectors.form.getFormValues(RECOVER_FORM)(state) as RecoverFormType,
hasCloudBackup: selectors.cache.getHasCloudBackup(state) as boolean,
kycReset: selectors.auth.getKycResetStatus(state),
language: selectors.preferences.getLanguage(state),
lastGuid: selectors.cache.getLastGuid(state),
loginFormValues: selectors.form.getFormValues('login')(state),
mnemonic: formValueSelector('recover')(state, 'mnemonic'),
nabuId: selectors.auth.getMagicLinkData(state)?.wallet?.nabu?.user_id,
recoverPassword: formValueSelector('recover')(state, 'recoverPassword') || '',
registering: selectors.auth.getRegistering(state) as RemoteDataType<string, any>,
resetPassword: formValueSelector('recover')(state, 'resetAccountPassword') || ''
registering: selectors.auth.getRegistering(state) as RemoteDataType<string, any>
})

const mapDispatchToProps = (dispatch) => ({
Expand All @@ -89,7 +86,7 @@ const connector = connect(mapStateToProps, mapDispatchToProps)
const enhance = compose<any>(
reduxForm({
destroyOnUnmount: false,
form: 'recover'
form: RECOVER_FORM
}),
connector
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { Wrapper } from 'components/Public'
import { RecoverSteps } from 'data/types'
import { media } from 'services/styles'

export const RECOVER_FORM = 'recover'

export const ActionButton = styled(Button)`
margin-top: 15px;
`
Expand Down

0 comments on commit 2105425

Please sign in to comment.