Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add BI webview reconnection #1656

Merged
merged 4 commits into from
Jun 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,10 @@ const DumbEditAccountModal = withRouter(
showError={true}
onVaultDismiss={redirectToAccount}
fieldOptions={fieldOptions}
reconnect={fromReconnect}
/>
)}
<div className="u-mb-2" />
</DialogContent>
</Dialog>
)
Expand Down
19 changes: 11 additions & 8 deletions packages/cozy-harvest-lib/src/components/OAuthForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,12 @@ export class OAuthForm extends PureComponent {
this.handleOAuthCancel = this.handleOAuthCancel.bind(this)
this.handleExtraParams = this.handleExtraParams.bind(this)
this.state = {
initialValues: null,
showingOAuthModal: false
}
}

componentDidMount() {
const { account, konnector, flow, client } = this.props
this.setState({ initialValues: account ? account.oauth : null })

const konnectorPolicy = findKonnectorPolicy(konnector)

Expand Down Expand Up @@ -72,30 +70,33 @@ export class OAuthForm extends PureComponent {
}

render() {
const { konnector, t, flowState } = this.props
const { initialValues, showOAuthWindow, needExtraParams, extraParams } =
this.state
const { konnector, t, flowState, reconnect, account } = this.props
doubleface marked this conversation as resolved.
Show resolved Hide resolved
const { showOAuthWindow, needExtraParams, extraParams } = this.state
const isBusy =
showOAuthWindow === true ||
flowState.running ||
(needExtraParams && !extraParams)

return initialValues ? null : (
doubleface marked this conversation as resolved.
Show resolved Hide resolved
const buttonLabel = reconnect ? 'oauth.reconnect.label' : 'oauth.connect.label'

return (
<>
<Button
className="u-mt-1"
busy={isBusy}
disabled={isBusy}
extension="full"
label={t('oauth.connect.label')}
label={t(buttonLabel)}
onClick={this.handleConnect}
/>
{showOAuthWindow && (
<OAuthWindow
extraParams={extraParams}
konnector={konnector}
reconnect={reconnect}
onSuccess={this.handleAccountId}
onCancel={this.handleOAuthCancel}
account={account}
/>
)}
</>
Expand All @@ -111,7 +112,9 @@ OAuthForm.propTypes = {
/** Success callback, takes account as parameter */
onSuccess: PropTypes.func,
/** Translation function */
t: PropTypes.func.isRequired
t: PropTypes.func.isRequired,
/** Is it a reconnection or not */
reconnect: PropTypes.bool
}

export default compose(translate(), withConnectionFlow())(OAuthForm)
5 changes: 3 additions & 2 deletions packages/cozy-harvest-lib/src/components/OAuthForm.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,17 @@ describe('OAuthForm', () => {
expect(component).toMatchSnapshot()
})

it('should not render button when update', () => {
it('should render reconnect button when updating an account', () => {
const component = shallow(
<OAuthForm
flowState={{}}
account={{ oauth: { access_token: '1234abcd' } }}
konnector={fixtures.konnector}
reconnect={true}
t={t}
/>
).getElement()
expect(component).toBeNull()
expect(component).toMatchSnapshot()
})
it('should call policy fetchExtraOAuthUrlParams with proper params', () => {
shallow(
Expand Down
12 changes: 9 additions & 3 deletions packages/cozy-harvest-lib/src/components/OAuthWindow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class OAuthWindow extends PureComponent {
}

componentDidMount() {
const { client, konnector, redirectSlug, extraParams } = this.props
const { client, konnector, redirectSlug, extraParams, reconnect, account } = this.props
doubleface marked this conversation as resolved.
Show resolved Hide resolved
this.realtime = new CozyRealtime({ client })
this.realtime.subscribe(
'notified',
Expand All @@ -57,7 +57,9 @@ export class OAuthWindow extends PureComponent {
client,
konnector,
redirectSlug,
extraParams
extraParams,
reconnect,
account
)
this.setState({ oAuthStateKey, oAuthUrl, succeed: false })
}
Expand Down Expand Up @@ -171,7 +173,11 @@ OAuthWindow.propTypes = {
an account id */
onCancel: PropTypes.func,
/** The app we want to redirect the user on, after the OAuth flow. It used by the stack */
redirectSlug: PropTypes.string
redirectSlug: PropTypes.string,
/** Is it a reconnection or not */
reconnect: PropTypes.bool,
/** Existing account */
account: PropTypes.object
}

export default translate()(withClient(OAuthWindow))
8 changes: 6 additions & 2 deletions packages/cozy-harvest-lib/src/components/TriggerManager.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,8 @@ export class DumbTriggerManager extends Component {
flow,
flowState,
client,
OAuthFormWrapperComp
OAuthFormWrapperComp,
reconnect
doubleface marked this conversation as resolved.
Show resolved Hide resolved
} = this.props

const submitting = flowState.running
Expand All @@ -345,6 +346,7 @@ export class DumbTriggerManager extends Component {
client={client}
flow={flow}
account={account}
reconnect={reconnect}
konnector={konnector}
onSuccess={this.handleOAuthAccountId}
/>
Expand Down Expand Up @@ -444,7 +446,9 @@ DumbTriggerManager.propTypes = {
flow: PropTypes.object,
flowState: PropTypes.object,
// Used to inject a component around OAuthForm, and so customize the UI from the app
OAuthFormWrapperComp: PropTypes.node
OAuthFormWrapperComp: PropTypes.node,
/** Is it a reconnection or not */
reconnect: PropTypes.bool,
}

const TriggerManager = compose(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,16 @@ exports[`OAuthForm should render 1`] = `
/>
</React.Fragment>
`;

exports[`OAuthForm should render reconnect button when update 1`] = `
<React.Fragment>
<DefaultButton
busy={true}
className="u-mt-1"
disabled={true}
extension="full"
label="oauth.reconnect.label"
onClick={[Function]}
/>
</React.Fragment>
`;
47 changes: 35 additions & 12 deletions packages/cozy-harvest-lib/src/helpers/oauth.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ export const handleOAuthResponse = (options = {}) => {
* @param {string} redirectSlug The app we want to redirect the user on after the end of the flow
* @param {string} nonce unique nonce string
* @param {Object} extraParams some extra parameters to add to the query string
* @param {Boolean} reconnect Are we trying to reconnect an existing account ?
* @param {io.cozy.accounts} account targeted account if any
* @returns {String} final OAuth url string
*/
export const getOAuthUrl = ({
cozyUrl,
Expand All @@ -108,9 +111,18 @@ export const getOAuthUrl = ({
oAuthConf = {},
nonce,
redirectSlug,
extraParams
extraParams,
reconnect,
account
doubleface marked this conversation as resolved.
Show resolved Hide resolved
}) => {
let oAuthUrl = `${cozyUrl}/accounts/${accountType}/start?state=${oAuthStateKey}&nonce=${nonce}`
const startOrReconnect = reconnect ? 'reconnect' : 'start'
const accountIdParam = reconnect ? account._id + '/' : ''
const oAuthUrl = new URL(
`${cozyUrl}/accounts/${accountType}/${accountIdParam}${startOrReconnect}`
)
oAuthUrl.searchParams.set('state', oAuthStateKey)
oAuthUrl.searchParams.set('nonce', nonce)

if (
oAuthConf.scope !== undefined &&
oAuthConf.scope !== null &&
Expand All @@ -119,19 +131,19 @@ export const getOAuthUrl = ({
const urlScope = Array.isArray(oAuthConf.scope)
? oAuthConf.scope.join('+')
: oAuthConf.scope
oAuthUrl += `&scope=${urlScope}`
oAuthUrl.searchParams.set('scope', urlScope)
}
if (redirectSlug) {
oAuthUrl += `&slug=${redirectSlug}`
oAuthUrl.searchParams.set('slug', redirectSlug)
}

if (extraParams) {
for (const key in extraParams) {
oAuthUrl += `&${key}=${extraParams[key]}`
}
Object.entries(extraParams).forEach(([key, value]) =>
oAuthUrl.searchParams.set(key, value)
)
}

return oAuthUrl
return oAuthUrl.toString()
}

const getAppSlug = client => {
Expand All @@ -145,10 +157,19 @@ const getAppSlug = client => {
* @param {string} domain Cozy domain
* @param {Object} konnector
* @param {string} redirectSlug The app we want to redirect the user on after the end of the flow
* @return {Object} Object containing: `oAuthUrl` (URL of cozy stack
* OAuth endpoint) and `oAuthStateKey` (localStorage key)
* @param {Object} extraParams some extra parameters to add to the query string
* @param {Boolean} reconnect Are we trying to reconnect an existing account ?
* @param {io.cozy.accounts} account targetted account if any
doubleface marked this conversation as resolved.
Show resolved Hide resolved
* @return {Object} Object containing: `oAuthUrl` (URL of cozy stack OAuth endpoint) and `oAuthStateKey` (localStorage key)
doubleface marked this conversation as resolved.
Show resolved Hide resolved
*/
export const prepareOAuth = (client, konnector, redirectSlug, extraParams) => {
export const prepareOAuth = (
client,
konnector,
redirectSlug,
extraParams,
reconnect = false,
account
doubleface marked this conversation as resolved.
Show resolved Hide resolved
) => {
const { oauth } = konnector
const accountType = konnectors.getAccountType(konnector)

Expand All @@ -168,7 +189,9 @@ export const prepareOAuth = (client, konnector, redirectSlug, extraParams) => {
oAuthConf: oauth,
nonce: Date.now(),
redirectSlug: redirectSlug || getAppSlug(client),
extraParams
extraParams,
reconnect,
account
})

return { oAuthStateKey, oAuthUrl }
Expand Down
17 changes: 16 additions & 1 deletion packages/cozy-harvest-lib/src/helpers/oauth.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe('Oauth helper', () => {
oAuthConf: { scope: ['thescope', 'thescope2'] }
})
expect(url).toEqual(
'https://cozyurl/accounts/testslug/start?state=statekey&nonce=1234&scope=thescope+thescope2'
'https://cozyurl/accounts/testslug/start?state=statekey&nonce=1234&scope=thescope%2Bthescope2'
)
})
it('should use redirectSlug if present', () => {
Expand All @@ -76,6 +76,21 @@ describe('Oauth helper', () => {
'https://cozyurl/accounts/testslug/start?state=statekey&nonce=1234&token=thetoken&id_connector=40'
)
})
it('should return reconnect url with account id if reconnect', () => {
const url = getOAuthUrl({
...defaultConf,
oAuthConf: {},
account: { _id: 'accountid' },
reconnect: true,
extraParams: {
code: 'thecode',
connection_id: 12345
}
})
expect(url).toEqual(
'https://cozyurl/accounts/testslug/accountid/reconnect?state=statekey&nonce=1234&code=thecode&connection_id=12345'
)
})
})
describe('handleOAuthResponse', () => {
let originalLocation
Expand Down
3 changes: 3 additions & 0 deletions packages/cozy-harvest-lib/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,9 @@
}
},
"oauth": {
"reconnect": {
"label": "Reconnect"
},
"connect": {
"label": "Connect"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/cozy-harvest-lib/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,9 @@
}
},
"oauth": {
"reconnect": {
"label": "Se reconnecter"
},
"connect": {
"label": "Connecter"
},
Expand Down
51 changes: 49 additions & 2 deletions packages/cozy-harvest-lib/src/services/biWebView.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
saveBIConfig,
findAccountWithBiConnection,
convertBIErrortoKonnectorJobError,
isBudgetInsightConnector
isBudgetInsightConnector,
getBIConnectionIdFromAccount
} from './budget-insight'
import { KonnectorJobError } from '../helpers/konnectors'
import { waitForRealtimeEvent } from './jobUtils'
Expand Down Expand Up @@ -189,6 +190,36 @@ export const onBIAccountCreation = async ({
return updatedAccount
}

/**
* Create OAuth extra parameters specific to reconnect webview
*
* @param {Number} options.biBankId - Connector bank id (compatible with non webview bi connectors)
* @param {Array<Number>} options.biBankIds - connector bank ids (for webview connectors)
* @param {String} options.token - BI temporary token
* @param {Number} options.connId - BI bi connection id
* @return {Object}
*/
const getReconnectExtraOAuthUrlParams = async ({
biBankId,
biBankIds,
token,
connId
}) => {
doubleface marked this conversation as resolved.
Show resolved Hide resolved
return {
id_connector: biBankId || biBankIds,
code: token,
connection_id: connId
}
}

/**
* Create OAuth extra parameters
*
* @param {CozyClient} options.client - CozyClient instance
* @param {io.cozy.konnectors} options.konnector connector manifest content
* @param {io.cozy.accounts} options.account The account content
* @return {Promise<Object>}
*/
export const fetchExtraOAuthUrlParams = async ({
client,
konnector,
Expand All @@ -204,7 +235,23 @@ export const fetchExtraOAuthUrlParams = async ({
account
})

return { id_connector: biBankId || biBankIds, token }
const connId = getBIConnectionIdFromAccount(account)

const isReconnect = Boolean(connId)

if (isReconnect) {
return getReconnectExtraOAuthUrlParams({
biBankId,
biBankIds,
token,
connId
})
} else {
return {
id_connector: biBankId || biBankIds,
token
}
}
}

/**
Expand Down
Loading