Skip to content

Commit

Permalink
feat(harvest): 💅 Let KonnectorJob handle statuses
Browse files Browse the repository at this point in the history
  • Loading branch information
CPatchane committed May 7, 2019
1 parent 48fd910 commit fee5d47
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 87 deletions.
126 changes: 46 additions & 80 deletions packages/cozy-harvest-lib/src/components/TriggerLauncher.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,14 @@ import TwoFAForm from './TwoFAForm'
import { accountsMutations } from '../connections/accounts'
import { triggersMutations } from '../connections/triggers'

import KonnectorJob from '../models/KonnectorJob'

// Statuses
const ERRORED = 'ERRORED'
const IDLE = 'IDLE'
const LOGIN_SUCCESS = 'LOGIN_SUCCESS'
const TWO_FA_REQUEST = 'TWO_FA_REQUEST'
const TWO_FA_MISMATCH = 'TWO_FA_MISMATCH'
const PENDING = 'PENDING'
const SUCCESS = 'SUCCESS'
const RUNNING_TWOFA = 'RUNNING_TWOFA'
import {
withKonnectorJob,
KonnectorJobPropTypes,
ERROR_EVENT,
SUCCESS_EVENT,
TWO_FA_REQUEST_EVENT,
TWO_FA_MISMATCH_EVENT
} from '../models/KonnectorJob'

/**
* Trigger Launcher renders its children with following props:
Expand All @@ -38,86 +35,55 @@ const RUNNING_TWOFA = 'RUNNING_TWOFA'
* TriggerManager for example
*/
export class TriggerLauncher extends Component {
state = {
status: IDLE
}

constructor(props, context) {
super(props, context)
this.state = { showTwoFAModal: false }

this.launch = this.launch.bind(this)
this.handleTwoFAModalDismiss = this.handleTwoFAModalDismiss.bind(this)
this.handle2FACode = this.handle2FACode.bind(this)
this.handleError = this.handleError.bind(this)
this.handleSuccess = this.handleSuccess.bind(this)
this.handleLoginSuccess = this.handleLoginSuccess.bind(this)
this.handleTwoFARequest = this.handleTwoFARequest.bind(this)
}

handleTwoFAModalDismiss() {
// TODO: Make the modale not closable, or offer possibility to re-open it.
this.setState({ showTwoFAModal: false })
}

handleError() {
this.setState({ status: ERRORED, showTwoFAModal: false })
}

handleSuccess() {
this.setState({ status: SUCCESS, showTwoFAModal: false })
}

handleLoginSuccess() {
this.setState({ status: LOGIN_SUCCESS })
this.dismissTwoFAModal = this.dismissTwoFAModal.bind(this)
this.displayTwoFAModal = this.displayTwoFAModal.bind(this)
}

handleTwoFARequest() {
this.setState({ status: TWO_FA_REQUEST, showTwoFAModal: true })
componentDidMount() {
this.props.konnectorJob
.on(ERROR_EVENT, this.dismissTwoFAModal)
.on(SUCCESS_EVENT, this.dismissTwoFAModal)
.on(TWO_FA_REQUEST_EVENT, this.displayTwoFAModal)
.on(TWO_FA_MISMATCH_EVENT, this.displayTwoFAModal)
}

handleTwoFAMismatch() {
this.setState({ status: TWO_FA_MISMATCH, showTwoFAModal: true })
}

handle2FACode(code) {
this.setState({ status: RUNNING_TWOFA })
this.konnectorJob.sendTwoFACode(code)
dismissTwoFAModal() {
// TODO: Make the modal not closable, or offer possibility to re-open it.
this.setState({ showTwoFAModal: false })
}

async launch() {
const { trigger } = this.props
const { client } = this.context

this.setState({ status: PENDING })

this.konnectorJob = new KonnectorJob(client, trigger)
this.konnectorJob
.on('error', this.handleError)
.on('loginSuccess', this.handleLoginSuccess)
.on('success', this.handleSuccess)
.on('twoFARequest', this.handleTwoFARequest)
.on('twoFAMismatch', this.handleTwoFAMismatch)
this.konnectorJob.launch()
displayTwoFAModal() {
this.setState({ showTwoFAModal: true })
}

render() {
const { showTwoFAModal, status } = this.state
const { account, children, konnector, submitting } = this.props
const { showTwoFAModal } = this.state
const {
account,
children,
konnector,
konnectorJob,
submitting
} = this.props

return (
<div>
{children({
launch: this.launch,
running: ![ERRORED, IDLE, SUCCESS].includes(status) || submitting
launch: konnectorJob.launch,
running: konnectorJob.isRunning() || submitting
})}
{showTwoFAModal && (
<TwoFAForm
account={account}
konnector={konnector}
dismissAction={this.handleTwoFAModalDismiss}
handleSubmitTwoFACode={this.handle2FACode}
submitting={status === RUNNING_TWOFA}
retryAsked={status === TWO_FA_MISMATCH}
dismissAction={this.dismissTwoFAModal}
handleSubmitTwoFACode={konnectorJob.sendTwoFACode}
submitting={konnectorJob.isTwoFARunning()}
retryAsked={konnectorJob.isTwoFARetry()}
into="coz-harvest-modal-place"
/>
)}
Expand All @@ -132,19 +98,19 @@ TriggerLauncher.propTypes = {
*/
children: PropTypes.func.isRequired,
/**
* Indicates if trigger is already runnning
* Account document to use for 2FA
*/
submitting: PropTypes.bool,
account: PropTypes.object.isRequired,
/**
* The trigger to launch
* Konnector required to fetch app icon
*/
trigger: PropTypes.object.isRequired
}

TriggerLauncher.contextTypes = {
client: PropTypes.object.isRequired
konnector: PropTypes.object.isRequired,
/**
* Indicates if trigger is already runnning
*/
submitting: PropTypes.bool
}

export default withMutations(accountsMutations, triggersMutations)(
TriggerLauncher
export default withKonnectorJob(
withMutations(accountsMutations, triggersMutations)(TriggerLauncher)
)
117 changes: 110 additions & 7 deletions packages/cozy-harvest-lib/src/models/KonnectorJob.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React, { PureComponent } from 'react'
import MicroEE from 'microee'

import accounts from '../helpers/accounts'
Expand All @@ -6,12 +7,25 @@ import triggers from '../helpers/triggers'
import accountsMutations from '../connections/accounts'
import triggersMutations from '../connections/triggers'

import PropTypes from 'prop-types'

// events
export const ERROR_EVENT = 'error'
export const LOGIN_SUCCESS_EVENT = 'loginSuccess'
export const SUCCESS_EVENT = 'success'
export const TWO_FA_REQUEST_EVENT = 'twoFARequest'
export const TWO_FA_MISMATCH_EVENT = 'twoFAMismatch'

// statuses
export const IDLE = 'IDLE'
export const PENDING = 'PENDING'
export const ERRORED = 'ERRORED'
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'
export const SUCCESS = 'SUCCESS'
export const RUNNING_TWOFA = 'RUNNING_TWOFA'
export const TWO_FA_MISMATCH = 'TWO_FA_MISMATCH'
export const TWO_FA_REQUEST = 'TWO_FA_REQUEST'

// helpers
export const prepareTriggerAccount = async (trigger, accountsMutations) => {
const { findAccount, updateAccount } = accountsMutations
Expand All @@ -31,6 +45,36 @@ export class KonnectorJob {
// Bind methods used as callbacks
this.handleTwoFA = this.handleTwoFA.bind(this)
this.sendTwoFACode = this.sendTwoFACode.bind(this)
this.handleLegacyEvent = this.handleLegacyEvent.bind(this)
this.launch = this.launch.bind(this)

// status and setter/getters
this._status = IDLE
this.setStatus = this.setStatus.bind(this)
this.getStatus = this.getStatus.bind(this)
this.isRunning = this.isRunning.bind(this)
this.isTwoFARunning = this.isTwoFARunning.bind(this)
this.isTwoFARetry = this.isTwoFARetry.bind(this)
}

setStatus(status) {
this._status = status
}

getStatus() {
return this._status
}

isRunning() {
return ![ERRORED, IDLE, SUCCESS].includes(this.getStatus())
}

isTwoFARunning() {
return this.getStatus() === RUNNING_TWOFA
}

isTwoFARetry() {
return this.getStatus() === TWO_FA_MISMATCH
}

// TODO: Pass updated account as parameter
Expand All @@ -41,35 +85,55 @@ export class KonnectorJob {
this.account.state = state

if (accounts.isTwoFANeeded(state)) {
this.setStatus(TWO_FA_REQUEST)
this.emit(TWO_FA_REQUEST_EVENT, this.account)
} else if (accounts.isTwoFARetry(state)) {
this.setStatus(TWO_FA_MISMATCH)
this.emit(TWO_FA_MISMATCH_EVENT, this.account)
}
}

/**
* Re-emit an event received from an internal object
* Legacy events use the KonnectorJobWatcher unstil it has been merged into
* KonnectorJob so we need to re-emit the events
*/
reEmit(event) {
return (...args) => this.emit(event, ...args)
handleLegacyEvent(event) {
return (...args) => {
switch (event) {
case ERROR_EVENT:
this.setStatus(ERRORED)
break
case LOGIN_SUCCESS_EVENT:
this.setStatus(LOGIN_SUCCESS)
break
case SUCCESS_EVENT:
this.setStatus(SUCCESS)
break
}
this.emit(event, ...args)
}
}

/**
* Send Two FA Code, i.e. save it into account
*/
async sendTwoFACode(code) {
this.setStatus(RUNNING_TWOFA)
const { updateAccount } = this.accountsMutations
try {
await updateAccount(accounts.updateTwoFaCode(this.account, code))
} catch (error) {
return this.handleError(error)
// eslint-disable-next-line no-console
console.error(error)
this.setStatus(ERRORED)
}
}

/**
* Launch the job and set up everything to follow execution.
*/
async launch() {
this.setStatus(PENDING)
const { watchKonnectorAccount } = this.accountsMutations
const { launchTrigger, watchKonnectorJob } = this.triggersMutations

Expand All @@ -85,12 +149,51 @@ export class KonnectorJob {
const konnectorJob = watchKonnectorJob(await launchTrigger(this.trigger))
// Temporary reEmitting until merging of KonnectorJobWatcher and
// KonnectorAccountWatcher into KonnectorJob
konnectorJob.on(ERROR_EVENT, this.reEmit(ERROR_EVENT))
konnectorJob.on(LOGIN_SUCCESS_EVENT, this.reEmit(LOGIN_SUCCESS_EVENT))
konnectorJob.on(SUCCESS_EVENT, this.reEmit(SUCCESS_EVENT))
konnectorJob.on(ERROR_EVENT, this.handleLegacyEvent(ERROR_EVENT))
konnectorJob.on(
LOGIN_SUCCESS_EVENT,
this.handleLegacyEvent(LOGIN_SUCCESS_EVENT)
)
konnectorJob.on(SUCCESS_EVENT, this.handleLegacyEvent(SUCCESS_EVENT))
}
}

MicroEE.mixin(KonnectorJob)

export const KonnectorJobPropTypes = {
/**
* The trigger to launch
*/
trigger: PropTypes.object.isRequired,
/**
* The konnectorJob instance provided by withKonnectorJob
*/
konnectorJob: PropTypes.object.isRequired
}

export const withKonnectorJob = WrappedComponent => {
class ComponentWithKonnectorJob extends PureComponent {
constructor(props, context) {
super(props, context)
this.konnectorJob = new KonnectorJob(context.client, props.trigger)
}
render() {
return (
<WrappedComponent {...this.props} konnectorJob={this.konnectorJob} />
)
}
}
ComponentWithKonnectorJob.contextTypes = {
client: PropTypes.object.isRequired
}
ComponentWithKonnectorJob.propTypes = {
/**
* KonnectorJob required and provided props
*/
...KonnectorJobPropTypes,
...(WrappedComponent.propTypes || {})
}
return ComponentWithKonnectorJob
}

export default KonnectorJob

0 comments on commit fee5d47

Please sign in to comment.