cozy-harvest-lib 9.22.3 → 9.23.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,40 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [9.23.2](https://github.com/cozy/cozy-libs/compare/cozy-harvest-lib@9.23.1...cozy-harvest-lib@9.23.2) (2022-07-26)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * Invalidate temporary token cache when there is a change of BI user ([0fa3893](https://github.com/cozy/cozy-libs/commit/0fa3893bb6ac3c02bc88d15dae79e3772a6aef97))
12
+
13
+
14
+
15
+
16
+
17
+ ## [9.23.1](https://github.com/cozy/cozy-libs/compare/cozy-harvest-lib@9.23.0...cozy-harvest-lib@9.23.1) (2022-07-26)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * Google connector in web mode ([1777fcc](https://github.com/cozy/cozy-libs/commit/1777fccc6b9e1e581189ca4563f851b341a60644))
23
+ * OauthWindowInAppBrowser re-render ([334e7d8](https://github.com/cozy/cozy-libs/commit/334e7d8b7d777bf265803f81583a65f39fb0cabb))
24
+
25
+
26
+
27
+
28
+
29
+ # [9.23.0](https://github.com/cozy/cozy-libs/compare/cozy-harvest-lib@9.22.3...cozy-harvest-lib@9.23.0) (2022-07-26)
30
+
31
+
32
+ ### Features
33
+
34
+ * Add BI aggregator releationship to BI accounts ([cb7a79c](https://github.com/cozy/cozy-libs/commit/cb7a79c6cd72a9f8e95ae71307f27f04b68f0e94))
35
+
36
+
37
+
38
+
39
+
6
40
  ## [9.22.3](https://github.com/cozy/cozy-libs/compare/cozy-harvest-lib@9.22.2...cozy-harvest-lib@9.22.3) (2022-07-26)
7
41
 
8
42
  **Note:** Version bump only for package cozy-harvest-lib
@@ -1,6 +1,6 @@
1
1
  import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
2
2
  import _regeneratorRuntime from "@babel/runtime/regenerator";
3
- import { useEffect } from 'react';
3
+ import React, { useEffect } from 'react';
4
4
  import PropTypes from 'prop-types';
5
5
  import { useWebviewIntent } from 'cozy-intent';
6
6
  import logger from '../logger';
@@ -9,22 +9,27 @@ import { intentsApiProptype } from '../helpers/proptypes';
9
9
  var InAppBrowser = function InAppBrowser(_ref) {
10
10
  var url = _ref.url,
11
11
  onClose = _ref.onClose,
12
- _ref$intentsApi = _ref.intentsApi,
13
- intentsApi = _ref$intentsApi === void 0 ? {} : _ref$intentsApi;
14
- var webviewIntent = useWebviewIntent();
15
- var fetchSessionCode = intentsApi !== null && intentsApi !== void 0 && intentsApi.fetchSessionCode ? intentsApi === null || intentsApi === void 0 ? void 0 : intentsApi.fetchSessionCode : function () {
16
- return webviewIntent.call('fetchSessionCode');
17
- };
18
- var showInAppBrowser = intentsApi !== null && intentsApi !== void 0 && intentsApi.showInAppBrowser ? intentsApi === null || intentsApi === void 0 ? void 0 : intentsApi.showInAppBrowser : function (url) {
19
- return webviewIntent.call('showInAppBrowser', {
20
- url: url
12
+ intentsApi = _ref.intentsApi;
13
+
14
+ if (intentsApi) {
15
+ return /*#__PURE__*/React.createElement(InAppBrowserWithIntentsApi, {
16
+ url: url,
17
+ onClose: onClose,
18
+ intentsApi: intentsApi
21
19
  });
22
- };
23
- var closeInAppBrowser = intentsApi !== null && intentsApi !== void 0 && intentsApi.closeInAppBrowser ? intentsApi === null || intentsApi === void 0 ? void 0 : intentsApi.closeInAppBrowser : function () {
24
- return webviewIntent.call('closeInAppBrowser');
25
- };
26
- var tokenParamName = intentsApi !== null && intentsApi !== void 0 && intentsApi.tokenParamName ? intentsApi === null || intentsApi === void 0 ? void 0 : intentsApi.tokenParamName : 'session_code';
27
- var isReady = Boolean(webviewIntent || (intentsApi === null || intentsApi === void 0 ? void 0 : intentsApi.fetchSessionCode) && (intentsApi === null || intentsApi === void 0 ? void 0 : intentsApi.showInAppBrowser) && (intentsApi === null || intentsApi === void 0 ? void 0 : intentsApi.closeInAppBrowser));
20
+ } else {
21
+ return /*#__PURE__*/React.createElement(InAppBrowserWithWebviewIntent, {
22
+ url: url,
23
+ onClose: onClose
24
+ });
25
+ }
26
+ };
27
+
28
+ var InAppBrowserWithWebviewIntent = function InAppBrowserWithWebviewIntent(_ref2) {
29
+ var url = _ref2.url,
30
+ onClose = _ref2.onClose;
31
+ var webviewIntent = useWebviewIntent();
32
+ var isReady = Boolean(webviewIntent);
28
33
  useEffect(function () {
29
34
  function insideEffect() {
30
35
  return _insideEffect.apply(this, arguments);
@@ -45,19 +50,21 @@ var InAppBrowser = function InAppBrowser(_ref) {
45
50
  _context.prev = 1;
46
51
  logger.debug('url at the beginning: ', url);
47
52
  _context.next = 5;
48
- return fetchSessionCode();
53
+ return webviewIntent.call('fetchSessionCode');
49
54
 
50
55
  case 5:
51
56
  sessionCode = _context.sent;
52
57
  logger.debug('got session code', sessionCode);
53
58
  iabUrl = new URL(url);
54
- iabUrl.searchParams.append(tokenParamName, sessionCode); // we need to decodeURIComponent since toString() encodes URL
59
+ iabUrl.searchParams.append('session_code', sessionCode); // we need to decodeURIComponent since toString() encodes URL
55
60
  // but native browser will also encode them.
56
61
 
57
62
  urlToOpen = decodeURIComponent(iabUrl.toString());
58
63
  logger.debug('url to open: ', urlToOpen);
59
64
  _context.next = 13;
60
- return showInAppBrowser(urlToOpen);
65
+ return webviewIntent.call('showInAppBrowser', {
66
+ url: urlToOpen
67
+ });
61
68
 
62
69
  case 13:
63
70
  result = _context.sent;
@@ -89,6 +96,89 @@ var InAppBrowser = function InAppBrowser(_ref) {
89
96
  return _insideEffect.apply(this, arguments);
90
97
  }
91
98
 
99
+ insideEffect();
100
+ return function cleanup() {
101
+ webviewIntent.call('closeInAppBrowser');
102
+ };
103
+ }, [isReady, url, onClose, webviewIntent]);
104
+ return null;
105
+ };
106
+
107
+ var InAppBrowserWithIntentsApi = function InAppBrowserWithIntentsApi(_ref3) {
108
+ var url = _ref3.url,
109
+ onClose = _ref3.onClose,
110
+ _ref3$intentsApi = _ref3.intentsApi,
111
+ intentsApi = _ref3$intentsApi === void 0 ? {} : _ref3$intentsApi;
112
+ var fetchSessionCode = intentsApi.fetchSessionCode,
113
+ showInAppBrowser = intentsApi.showInAppBrowser,
114
+ closeInAppBrowser = intentsApi.closeInAppBrowser,
115
+ _intentsApi$tokenPara = intentsApi.tokenParamName,
116
+ tokenParamName = _intentsApi$tokenPara === void 0 ? 'session_code' : _intentsApi$tokenPara;
117
+ var isReady = Boolean((intentsApi === null || intentsApi === void 0 ? void 0 : intentsApi.fetchSessionCode) && (intentsApi === null || intentsApi === void 0 ? void 0 : intentsApi.showInAppBrowser) && (intentsApi === null || intentsApi === void 0 ? void 0 : intentsApi.closeInAppBrowser));
118
+ useEffect(function () {
119
+ function insideEffect() {
120
+ return _insideEffect2.apply(this, arguments);
121
+ }
122
+
123
+ function _insideEffect2() {
124
+ _insideEffect2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2() {
125
+ var sessionCode, iabUrl, urlToOpen, result;
126
+ return _regeneratorRuntime.wrap(function _callee2$(_context2) {
127
+ while (1) {
128
+ switch (_context2.prev = _context2.next) {
129
+ case 0:
130
+ if (!isReady) {
131
+ _context2.next = 21;
132
+ break;
133
+ }
134
+
135
+ _context2.prev = 1;
136
+ logger.debug('url at the beginning: ', url);
137
+ _context2.next = 5;
138
+ return fetchSessionCode();
139
+
140
+ case 5:
141
+ sessionCode = _context2.sent;
142
+ logger.debug('got session code', sessionCode);
143
+ iabUrl = new URL(url);
144
+ iabUrl.searchParams.append(tokenParamName, sessionCode); // we need to decodeURIComponent since toString() encodes URL
145
+ // but native browser will also encode them.
146
+
147
+ urlToOpen = decodeURIComponent(iabUrl.toString());
148
+ logger.debug('url to open: ', urlToOpen);
149
+ _context2.next = 13;
150
+ return showInAppBrowser(urlToOpen);
151
+
152
+ case 13:
153
+ result = _context2.sent;
154
+
155
+ if ((result === null || result === void 0 ? void 0 : result.type) !== 'dismiss' && (result === null || result === void 0 ? void 0 : result.type) !== 'cancel') {
156
+ logger.error('Unexpected InAppBrowser result', result);
157
+ }
158
+
159
+ _context2.next = 20;
160
+ break;
161
+
162
+ case 17:
163
+ _context2.prev = 17;
164
+ _context2.t0 = _context2["catch"](1);
165
+ logger.error('unexpected fetchSessionCode result', _context2.t0);
166
+
167
+ case 20:
168
+ if (onClose) {
169
+ onClose();
170
+ }
171
+
172
+ case 21:
173
+ case "end":
174
+ return _context2.stop();
175
+ }
176
+ }
177
+ }, _callee2, null, [[1, 17]]);
178
+ }));
179
+ return _insideEffect2.apply(this, arguments);
180
+ }
181
+
92
182
  insideEffect();
93
183
  return function cleanup() {
94
184
  closeInAppBrowser();
@@ -118,7 +118,7 @@ export var getOAuthUrl = function getOAuthUrl(_ref) {
118
118
  oAuthUrl.searchParams.set('nonce', nonce);
119
119
 
120
120
  if (oAuthConf.scope !== undefined && oAuthConf.scope !== null && oAuthConf.scope !== false) {
121
- var urlScope = Array.isArray(oAuthConf.scope) ? oAuthConf.scope.join('+') : oAuthConf.scope;
121
+ var urlScope = Array.isArray(oAuthConf.scope) ? oAuthConf.scope.join('%2B') : oAuthConf.scope;
122
122
  oAuthUrl.searchParams.set('scope', urlScope);
123
123
  }
124
124
 
@@ -53,7 +53,7 @@ describe('Oauth helper', function () {
53
53
  scope: ['thescope', 'thescope2']
54
54
  }
55
55
  }));
56
- expect(url).toEqual('https://cozyurl/accounts/testslug/start?state=statekey&nonce=1234&scope=thescope+thescope2');
56
+ expect(url).toEqual('https://cozyurl/accounts/testslug/start?state=statekey&nonce=1234&scope=thescope%2Bthescope2');
57
57
  });
58
58
  it('should use redirectSlug if present', function () {
59
59
  var url = getOAuthUrl(_objectSpread(_objectSpread({}, defaultConf), {}, {
@@ -33,6 +33,7 @@ import { waitForRealtimeEvent } from './jobUtils';
33
33
  import '../types';
34
34
  import { LOGIN_SUCCESS_EVENT } from '../models/flowEvents';
35
35
  var TEMP_TOKEN_TIMOUT_S = 60;
36
+ export var ACCOUNTS_DOCTYPE = 'io.cozy.accounts';
36
37
  export var isBiWebViewConnector = function isBiWebViewConnector(konnector) {
37
38
  return flag('harvest.bi.webview') && isBudgetInsightConnector(konnector);
38
39
  };
@@ -201,7 +202,7 @@ export var handleOAuthAccount = /*#__PURE__*/function () {
201
202
  logger.info("Found a BI webview connection id: ".concat(connectionId));
202
203
  flow.konnector = konnector;
203
204
  _context3.next = 12;
204
- return flow.saveAccount(setBIConnectionId(biWebviewAccount, connectionId));
205
+ return flow.saveAccount(_objectSpread(_objectSpread({}, setBIConnectionId(biWebviewAccount, connectionId)), getBiAggregatorParentRelationship(konnector)));
205
206
 
206
207
  case 12:
207
208
  biWebviewAccount = _context3.sent;
@@ -228,6 +229,34 @@ export var handleOAuthAccount = /*#__PURE__*/function () {
228
229
  return _ref6.apply(this, arguments);
229
230
  };
230
231
  }();
232
+ /**
233
+ * Return the bi aggregator parent relationship configuration for a given konnector
234
+ *
235
+ * @param {io.cozy.konnectors} konnector connector manifest content
236
+ *
237
+ * @return {Object}
238
+ */
239
+
240
+ var getBiAggregatorParentRelationship = function getBiAggregatorParentRelationship(konnector) {
241
+ var _konnector$aggregator;
242
+
243
+ var biAggregatorId = konnector === null || konnector === void 0 ? void 0 : (_konnector$aggregator = konnector.aggregator) === null || _konnector$aggregator === void 0 ? void 0 : _konnector$aggregator.accountId;
244
+
245
+ if (!biAggregatorId) {
246
+ return {};
247
+ }
248
+
249
+ return {
250
+ relationships: {
251
+ parent: {
252
+ data: {
253
+ _id: biAggregatorId,
254
+ _type: ACCOUNTS_DOCTYPE
255
+ }
256
+ }
257
+ }
258
+ };
259
+ };
231
260
  /**
232
261
  * Gets BI webview connection id which is returned in the account by the stack
233
262
  * via oauth callback url
@@ -237,6 +266,7 @@ export var handleOAuthAccount = /*#__PURE__*/function () {
237
266
  * @return {Integer} Connection Id
238
267
  */
239
268
 
269
+
240
270
  var getWebviewBIConnectionId = function getWebviewBIConnectionId(account) {
241
271
  var _account$oauth, _account$oauth$query, _account$oauth$query$;
242
272
 
@@ -546,16 +576,22 @@ function _getBiTemporaryTokenFromCache() {
546
576
  return _getBiTemporaryTokenFromCache.apply(this, arguments);
547
577
  }
548
578
 
549
- function isCacheExpired(_ref17) {
550
- var tokenCache = _ref17.tokenCache;
579
+ export function isCacheExpired(_ref17) {
580
+ var tokenCache = _ref17.tokenCache,
581
+ biUser = _ref17.biUser;
551
582
  var cacheAge = Date.now() - Number(tokenCache === null || tokenCache === void 0 ? void 0 : tokenCache.timestamp);
552
583
  logger.debug('tokenCache age', cacheAge / 1000 / 60, 'minutes');
553
584
  var MAX_TOKEN_CACHE_AGE = 29 * 60 * 1000;
585
+ var isSameUserId = tokenCache.userId === (biUser === null || biUser === void 0 ? void 0 : biUser.userId);
554
586
 
555
- if (tokenCache && cacheAge < MAX_TOKEN_CACHE_AGE) {
587
+ if (tokenCache && cacheAge < MAX_TOKEN_CACHE_AGE && isSameUserId) {
556
588
  return false;
557
589
  }
558
590
 
591
+ if (!isSameUserId) {
592
+ logger.warn("BI user id in cache ".concat(tokenCache.userId, " is different than current user id ").concat(biUser === null || biUser === void 0 ? void 0 : biUser.userId));
593
+ }
594
+
559
595
  return true;
560
596
  }
561
597
  /**
@@ -568,7 +604,6 @@ function isCacheExpired(_ref17) {
568
604
  * @return {createTemporaryTokenResponse}
569
605
  */
570
606
 
571
-
572
607
  function updateCache(_x9) {
573
608
  return _updateCache.apply(this, arguments);
574
609
  }
@@ -633,7 +668,7 @@ export var createTemporaryToken = /*#__PURE__*/function () {
633
668
  var _ref20 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee8(_ref19) {
634
669
  var _tokenCache;
635
670
 
636
- var client, konnector, account, tokenCache, cozyBankIds, _tokenCache2, biMapping;
671
+ var client, konnector, account, tokenCache, cozyBankIds, _yield$client$query, biUser, _tokenCache2, biMapping;
637
672
 
638
673
  return _regeneratorRuntime.wrap(function _callee8$(_context8) {
639
674
  while (1) {
@@ -652,16 +687,23 @@ export var createTemporaryToken = /*#__PURE__*/function () {
652
687
  konnector: konnector,
653
688
  account: account
654
689
  });
690
+ _context8.next = 8;
691
+ return client.query(Q('io.cozy.accounts').getById('bi-aggregator-user'));
692
+
693
+ case 8:
694
+ _yield$client$query = _context8.sent;
695
+ biUser = _yield$client$query.data;
655
696
 
656
697
  if (!isCacheExpired({
657
- tokenCache: tokenCache
698
+ tokenCache: tokenCache,
699
+ biUser: biUser
658
700
  })) {
659
- _context8.next = 11;
701
+ _context8.next = 15;
660
702
  break;
661
703
  }
662
704
 
663
705
  logger.debug('temporaryToken cache is expired. Updating');
664
- _context8.next = 10;
706
+ _context8.next = 14;
665
707
  return updateCache({
666
708
  client: client,
667
709
  konnector: konnector,
@@ -669,10 +711,10 @@ export var createTemporaryToken = /*#__PURE__*/function () {
669
711
  cozyBankIds: cozyBankIds
670
712
  });
671
713
 
672
- case 10:
714
+ case 14:
673
715
  tokenCache = _context8.sent;
674
716
 
675
- case 11:
717
+ case 15:
676
718
  assert(cozyBankIds.length, 'createTemporaryToken: Could not determine cozyBankIds from account or konnector');
677
719
  assert((_tokenCache = tokenCache) === null || _tokenCache === void 0 ? void 0 : _tokenCache.biMapping, 'createTemporaryToken: could not find a BI mapping in createTemporaryToken response, you should update your konnector to the last version');
678
720
  _tokenCache2 = tokenCache, biMapping = _tokenCache2.biMapping;
@@ -681,7 +723,7 @@ export var createTemporaryToken = /*#__PURE__*/function () {
681
723
  })));
682
724
  return _context8.abrupt("return", tokenCache);
683
725
 
684
- case 16:
726
+ case 20:
685
727
  case "end":
686
728
  return _context8.stop();
687
729
  }
@@ -7,7 +7,7 @@ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { va
7
7
 
8
8
  import _regeneratorRuntime from "@babel/runtime/regenerator";
9
9
  import CozyClient from 'cozy-client';
10
- import { handleOAuthAccount, checkBIConnection, isBiWebViewConnector, fetchContractSynchronizationUrl, refreshContracts, fetchExtraOAuthUrlParams } from './biWebView';
10
+ import { handleOAuthAccount, checkBIConnection, isBiWebViewConnector, fetchContractSynchronizationUrl, refreshContracts, fetchExtraOAuthUrlParams, isCacheExpired } from './biWebView';
11
11
  import ConnectionFlow from '../models/ConnectionFlow';
12
12
  import { waitForRealtimeEvent } from './jobUtils';
13
13
  import biPublicKeyProd from './bi-public-key-prod.json';
@@ -539,6 +539,72 @@ describe('refreshContracts', function () {
539
539
  }, _callee9);
540
540
  })));
541
541
  });
542
+ describe('isCacheExpired', function () {
543
+ it('should not be marked as expired when userId did not change and cache is not old', function () {
544
+ var tokenCache = {
545
+ timestamp: Date.now(),
546
+ userId: 666
547
+ };
548
+ var biUser = {
549
+ userId: 666
550
+ };
551
+ expect(isCacheExpired({
552
+ tokenCache: tokenCache,
553
+ biUser: biUser
554
+ })).toBe(false);
555
+ });
556
+ it('should be marked as expired when userId did not change and cache is old', function () {
557
+ var tokenCache = {
558
+ timestamp: 0,
559
+ userId: 666
560
+ };
561
+ var biUser = {
562
+ userId: 666
563
+ };
564
+ expect(isCacheExpired({
565
+ tokenCache: tokenCache,
566
+ biUser: biUser
567
+ })).toBe(true);
568
+ });
569
+ it('should be marked as expired when userId did change and cache is old', function () {
570
+ var tokenCache = {
571
+ timestamp: 0,
572
+ userId: 666
573
+ };
574
+ var biUser = {
575
+ userId: 667
576
+ };
577
+ expect(isCacheExpired({
578
+ tokenCache: tokenCache,
579
+ biUser: biUser
580
+ })).toBe(true);
581
+ });
582
+ it('should be marked as expired when userId did change and cache is not old', function () {
583
+ var tokenCache = {
584
+ timestamp: Date.now(),
585
+ userId: 666
586
+ };
587
+ var biUser = {
588
+ userId: 667
589
+ };
590
+ expect(isCacheExpired({
591
+ tokenCache: tokenCache,
592
+ biUser: biUser
593
+ })).toBe(true);
594
+ });
595
+ it('should be marked as expired when cache has no user id', function () {
596
+ var tokenCache = {
597
+ timestamp: Date.now()
598
+ };
599
+ var biUser = {
600
+ userId: 666
601
+ };
602
+ expect(isCacheExpired({
603
+ tokenCache: tokenCache,
604
+ biUser: biUser
605
+ })).toBe(true);
606
+ });
607
+ });
542
608
  describe('fetchExtraOAuthUrlParams', function () {
543
609
  it('should asynchronously fetch BI token', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee10() {
544
610
  var client, result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-harvest-lib",
3
- "version": "9.22.3",
3
+ "version": "9.23.2",
4
4
  "description": "Provides logic, modules and components for Cozy's harvest applications.",
5
5
  "main": "dist/index.js",
6
6
  "author": "Cozy",
@@ -29,7 +29,7 @@
29
29
  "@cozy/minilog": "^1.0.0",
30
30
  "@sentry/browser": "^6.0.1",
31
31
  "cozy-bi-auth": "0.0.25",
32
- "cozy-doctypes": "^1.84.0",
32
+ "cozy-doctypes": "^1.85.0",
33
33
  "cozy-logger": "^1.9.0",
34
34
  "date-fns": "^1.30.1",
35
35
  "final-form": "^4.18.5",
@@ -90,5 +90,5 @@
90
90
  "react-router-dom": "^5.0.1"
91
91
  },
92
92
  "sideEffects": false,
93
- "gitHead": "6bfb68f221c204c40e54baab55cc25655e71a548"
93
+ "gitHead": "c5c6e0d609fafbc41a4b8cbfa8e1d4582544bbdd"
94
94
  }
@@ -1,30 +1,73 @@
1
- import { useEffect } from 'react'
1
+ import React, { useEffect } from 'react'
2
2
  import PropTypes from 'prop-types'
3
3
  import { useWebviewIntent } from 'cozy-intent'
4
4
  import logger from '../logger'
5
5
  import { intentsApiProptype } from '../helpers/proptypes'
6
6
 
7
- const InAppBrowser = ({ url, onClose, intentsApi = {} }) => {
7
+ const InAppBrowser = ({ url, onClose, intentsApi }) => {
8
+ if (intentsApi) {
9
+ return (
10
+ <InAppBrowserWithIntentsApi
11
+ url={url}
12
+ onClose={onClose}
13
+ intentsApi={intentsApi}
14
+ />
15
+ )
16
+ } else {
17
+ return <InAppBrowserWithWebviewIntent url={url} onClose={onClose} />
18
+ }
19
+ }
20
+
21
+ const InAppBrowserWithWebviewIntent = ({ url, onClose }) => {
8
22
  const webviewIntent = useWebviewIntent()
9
- const fetchSessionCode = intentsApi?.fetchSessionCode
10
- ? intentsApi?.fetchSessionCode
11
- : () => webviewIntent.call('fetchSessionCode')
12
- const showInAppBrowser = intentsApi?.showInAppBrowser
13
- ? intentsApi?.showInAppBrowser
14
- : url => webviewIntent.call('showInAppBrowser', { url })
15
- const closeInAppBrowser = intentsApi?.closeInAppBrowser
16
- ? intentsApi?.closeInAppBrowser
17
- : () => webviewIntent.call('closeInAppBrowser')
23
+ const isReady = Boolean(webviewIntent)
24
+ useEffect(() => {
25
+ async function insideEffect() {
26
+ if (isReady) {
27
+ try {
28
+ logger.debug('url at the beginning: ', url)
29
+ const sessionCode = await webviewIntent.call('fetchSessionCode')
30
+ logger.debug('got session code', sessionCode)
31
+ const iabUrl = new URL(url)
32
+ iabUrl.searchParams.append('session_code', sessionCode)
33
+ // we need to decodeURIComponent since toString() encodes URL
34
+ // but native browser will also encode them.
35
+ const urlToOpen = decodeURIComponent(iabUrl.toString())
36
+ logger.debug('url to open: ', urlToOpen)
37
+ const result = await webviewIntent.call('showInAppBrowser', {
38
+ url: urlToOpen
39
+ })
40
+ if (result?.type !== 'dismiss' && result?.type !== 'cancel') {
41
+ logger.error('Unexpected InAppBrowser result', result)
42
+ }
43
+ } catch (err) {
44
+ logger.error('unexpected fetchSessionCode result', err)
45
+ }
46
+ if (onClose) {
47
+ onClose()
48
+ }
49
+ }
50
+ }
51
+ insideEffect()
52
+ return function cleanup() {
53
+ webviewIntent.call('closeInAppBrowser')
54
+ }
55
+ }, [isReady, url, onClose, webviewIntent])
56
+ return null
57
+ }
18
58
 
19
- const tokenParamName = intentsApi?.tokenParamName
20
- ? intentsApi?.tokenParamName
21
- : 'session_code'
59
+ const InAppBrowserWithIntentsApi = ({ url, onClose, intentsApi = {} }) => {
60
+ const {
61
+ fetchSessionCode,
62
+ showInAppBrowser,
63
+ closeInAppBrowser,
64
+ tokenParamName = 'session_code'
65
+ } = intentsApi
22
66
 
23
67
  const isReady = Boolean(
24
- webviewIntent ||
25
- (intentsApi?.fetchSessionCode &&
26
- intentsApi?.showInAppBrowser &&
27
- intentsApi?.closeInAppBrowser)
68
+ intentsApi?.fetchSessionCode &&
69
+ intentsApi?.showInAppBrowser &&
70
+ intentsApi?.closeInAppBrowser
28
71
  )
29
72
 
30
73
  useEffect(() => {
@@ -52,6 +95,7 @@ const InAppBrowser = ({ url, onClose, intentsApi = {} }) => {
52
95
  }
53
96
  }
54
97
  }
98
+
55
99
  insideEffect()
56
100
  return function cleanup() {
57
101
  closeInAppBrowser()
@@ -130,7 +130,7 @@ export const getOAuthUrl = ({
130
130
  oAuthConf.scope !== false
131
131
  ) {
132
132
  const urlScope = Array.isArray(oAuthConf.scope)
133
- ? oAuthConf.scope.join('+')
133
+ ? oAuthConf.scope.join('%2B')
134
134
  : oAuthConf.scope
135
135
  oAuthUrl.searchParams.set('scope', urlScope)
136
136
  }
@@ -50,7 +50,7 @@ describe('Oauth helper', () => {
50
50
  oAuthConf: { scope: ['thescope', 'thescope2'] }
51
51
  })
52
52
  expect(url).toEqual(
53
- 'https://cozyurl/accounts/testslug/start?state=statekey&nonce=1234&scope=thescope+thescope2'
53
+ 'https://cozyurl/accounts/testslug/start?state=statekey&nonce=1234&scope=thescope%2Bthescope2'
54
54
  )
55
55
  })
56
56
  it('should use redirectSlug if present', () => {
@@ -23,6 +23,7 @@ import '../types'
23
23
  import { LOGIN_SUCCESS_EVENT } from '../models/flowEvents'
24
24
 
25
25
  const TEMP_TOKEN_TIMOUT_S = 60
26
+ export const ACCOUNTS_DOCTYPE = 'io.cozy.accounts'
26
27
 
27
28
  export const isBiWebViewConnector = konnector =>
28
29
  flag('harvest.bi.webview') && isBudgetInsightConnector(konnector)
@@ -130,9 +131,11 @@ export const handleOAuthAccount = async ({
130
131
  if (connectionId) {
131
132
  logger.info(`Found a BI webview connection id: ${connectionId}`)
132
133
  flow.konnector = konnector
133
- biWebviewAccount = await flow.saveAccount(
134
- setBIConnectionId(biWebviewAccount, connectionId)
135
- )
134
+
135
+ biWebviewAccount = await flow.saveAccount({
136
+ ...setBIConnectionId(biWebviewAccount, connectionId),
137
+ ...getBiAggregatorParentRelationship(konnector)
138
+ })
136
139
 
137
140
  await flow.handleFormSubmit({
138
141
  client,
@@ -145,6 +148,30 @@ export const handleOAuthAccount = async ({
145
148
  return connectionId
146
149
  }
147
150
 
151
+ /**
152
+ * Return the bi aggregator parent relationship configuration for a given konnector
153
+ *
154
+ * @param {io.cozy.konnectors} konnector connector manifest content
155
+ *
156
+ * @return {Object}
157
+ */
158
+ const getBiAggregatorParentRelationship = konnector => {
159
+ const biAggregatorId = konnector?.aggregator?.accountId
160
+ if (!biAggregatorId) {
161
+ return {}
162
+ }
163
+ return {
164
+ relationships: {
165
+ parent: {
166
+ data: {
167
+ _id: biAggregatorId,
168
+ _type: ACCOUNTS_DOCTYPE
169
+ }
170
+ }
171
+ }
172
+ }
173
+ }
174
+
148
175
  /**
149
176
  * Gets BI webview connection id which is returned in the account by the stack
150
177
  * via oauth callback url
@@ -335,13 +362,20 @@ async function getBiTemporaryTokenFromCache({ client }) {
335
362
  * @param {createTemporaryTokenResponse} options.tokenCache
336
363
  * @return {Boolean}
337
364
  */
338
- function isCacheExpired({ tokenCache }) {
365
+ export function isCacheExpired({ tokenCache, biUser }) {
339
366
  const cacheAge = Date.now() - Number(tokenCache?.timestamp)
340
367
  logger.debug('tokenCache age', cacheAge / 1000 / 60, 'minutes')
341
368
  const MAX_TOKEN_CACHE_AGE = 29 * 60 * 1000
342
- if (tokenCache && cacheAge < MAX_TOKEN_CACHE_AGE) {
369
+ const isSameUserId = tokenCache.userId === biUser?.userId
370
+ if (tokenCache && cacheAge < MAX_TOKEN_CACHE_AGE && isSameUserId) {
343
371
  return false
344
372
  }
373
+
374
+ if (!isSameUserId) {
375
+ logger.warn(
376
+ `BI user id in cache ${tokenCache.userId} is different than current user id ${biUser?.userId}`
377
+ )
378
+ }
345
379
  return true
346
380
  }
347
381
 
@@ -398,7 +432,12 @@ export const createTemporaryToken = async ({ client, konnector, account }) => {
398
432
 
399
433
  let tokenCache = await getBiTemporaryTokenFromCache({ client })
400
434
  const cozyBankIds = getCozyBankIds({ konnector, account })
401
- if (isCacheExpired({ tokenCache })) {
435
+
436
+ const { data: biUser } = await client.query(
437
+ Q('io.cozy.accounts').getById('bi-aggregator-user')
438
+ )
439
+
440
+ if (isCacheExpired({ tokenCache, biUser })) {
402
441
  logger.debug('temporaryToken cache is expired. Updating')
403
442
  tokenCache = await updateCache({
404
443
  client,
@@ -5,7 +5,8 @@ import {
5
5
  isBiWebViewConnector,
6
6
  fetchContractSynchronizationUrl,
7
7
  refreshContracts,
8
- fetchExtraOAuthUrlParams
8
+ fetchExtraOAuthUrlParams,
9
+ isCacheExpired
9
10
  } from './biWebView'
10
11
  import ConnectionFlow from '../models/ConnectionFlow'
11
12
  import { waitForRealtimeEvent } from './jobUtils'
@@ -330,6 +331,34 @@ describe('refreshContracts', () => {
330
331
  })
331
332
  })
332
333
 
334
+ describe('isCacheExpired', () => {
335
+ it('should not be marked as expired when userId did not change and cache is not old', () => {
336
+ const tokenCache = { timestamp: Date.now(), userId: 666 }
337
+ const biUser = { userId: 666 }
338
+ expect(isCacheExpired({ tokenCache, biUser })).toBe(false)
339
+ })
340
+ it('should be marked as expired when userId did not change and cache is old', () => {
341
+ const tokenCache = { timestamp: 0, userId: 666 }
342
+ const biUser = { userId: 666 }
343
+ expect(isCacheExpired({ tokenCache, biUser })).toBe(true)
344
+ })
345
+ it('should be marked as expired when userId did change and cache is old', () => {
346
+ const tokenCache = { timestamp: 0, userId: 666 }
347
+ const biUser = { userId: 667 }
348
+ expect(isCacheExpired({ tokenCache, biUser })).toBe(true)
349
+ })
350
+ it('should be marked as expired when userId did change and cache is not old', () => {
351
+ const tokenCache = { timestamp: Date.now(), userId: 666 }
352
+ const biUser = { userId: 667 }
353
+ expect(isCacheExpired({ tokenCache, biUser })).toBe(true)
354
+ })
355
+ it('should be marked as expired when cache has no user id', () => {
356
+ const tokenCache = { timestamp: Date.now() }
357
+ const biUser = { userId: 666 }
358
+ expect(isCacheExpired({ tokenCache, biUser })).toBe(true)
359
+ })
360
+ })
361
+
333
362
  describe('fetchExtraOAuthUrlParams', () => {
334
363
  it('should asynchronously fetch BI token', async () => {
335
364
  const client = new CozyClient({