cozy-bar 0.0.0-development

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.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -0
  3. package/dist/cozy-bar.min.js +77 -0
  4. package/dist/cozy-bar.min.js.map +1 -0
  5. package/package.json +165 -0
  6. package/src/assets/icons/16/icon-storage-16.svg +3 -0
  7. package/src/assets/icons/24/icon-arrow-left.svg +3 -0
  8. package/src/assets/icons/32/icon-claudy.svg +1 -0
  9. package/src/assets/icons/apps/icon-collect.svg +25 -0
  10. package/src/assets/icons/apps/icon-drive.svg +17 -0
  11. package/src/assets/icons/apps/icon-market-soon.svg +25 -0
  12. package/src/assets/icons/apps/icon-photos.svg +19 -0
  13. package/src/assets/icons/apps/icon-soon.svg +21 -0
  14. package/src/assets/icons/apps/icon-store.svg +19 -0
  15. package/src/assets/icons/claudyActions/icon-bills.svg +6 -0
  16. package/src/assets/icons/claudyActions/icon-laptop.svg +7 -0
  17. package/src/assets/icons/claudyActions/icon-phone.svg +8 -0
  18. package/src/assets/icons/claudyActions/icon-question-mark.svg +6 -0
  19. package/src/assets/icons/comingsoon/icon-bank.svg +12 -0
  20. package/src/assets/icons/comingsoon/icon-sante.svg +12 -0
  21. package/src/assets/icons/comingsoon/icon-store.svg +6 -0
  22. package/src/assets/icons/icon-cozy.svg +3 -0
  23. package/src/assets/icons/icon-shield.svg +3 -0
  24. package/src/assets/icons/spinner.svg +4 -0
  25. package/src/assets/sprites/icon-apps.svg +1 -0
  26. package/src/assets/sprites/icon-cozy-home.svg +16 -0
  27. package/src/components/Apps/AppItem.jsx +117 -0
  28. package/src/components/Apps/AppItemPlaceholder.jsx +12 -0
  29. package/src/components/Apps/AppNavButtons.jsx +94 -0
  30. package/src/components/Apps/AppsContent.jsx +91 -0
  31. package/src/components/Apps/ButtonCozyHome.jsx +30 -0
  32. package/src/components/Apps/ButtonCozyHome.spec.jsx +53 -0
  33. package/src/components/Apps/IconCozyHome.jsx +38 -0
  34. package/src/components/Apps/index.jsx +72 -0
  35. package/src/components/Banner.jsx +41 -0
  36. package/src/components/Bar.jsx +295 -0
  37. package/src/components/Bar.spec.jsx +133 -0
  38. package/src/components/Claudy.jsx +81 -0
  39. package/src/components/ClaudyIcon.jsx +18 -0
  40. package/src/components/Drawer.jsx +227 -0
  41. package/src/components/Drawer.spec.jsx +98 -0
  42. package/src/components/SearchBar.jsx +358 -0
  43. package/src/components/Settings/SettingsContent.jsx +163 -0
  44. package/src/components/Settings/StorageData.jsx +29 -0
  45. package/src/components/Settings/helper.js +8 -0
  46. package/src/components/Settings/index.jsx +220 -0
  47. package/src/components/StorageIcon.jsx +16 -0
  48. package/src/components/SupportModal.jsx +59 -0
  49. package/src/components/__snapshots__/Bar.spec.jsx.snap +302 -0
  50. package/src/config/claudyActions.json +20 -0
  51. package/src/config/persistWhitelist.json +4 -0
  52. package/src/dom.js +80 -0
  53. package/src/index.jsx +242 -0
  54. package/src/index.spec.jsx +34 -0
  55. package/src/lib/api/helpers.js +13 -0
  56. package/src/lib/api/index.jsx +145 -0
  57. package/src/lib/exceptions.js +89 -0
  58. package/src/lib/expiringMemoize.js +13 -0
  59. package/src/lib/icon.js +77 -0
  60. package/src/lib/intents.js +16 -0
  61. package/src/lib/logger.js +11 -0
  62. package/src/lib/middlewares/appsI18n.js +57 -0
  63. package/src/lib/realtime.js +43 -0
  64. package/src/lib/reducers/apps.js +175 -0
  65. package/src/lib/reducers/apps.spec.js +59 -0
  66. package/src/lib/reducers/content.js +50 -0
  67. package/src/lib/reducers/context.js +86 -0
  68. package/src/lib/reducers/index.js +73 -0
  69. package/src/lib/reducers/locale.js +22 -0
  70. package/src/lib/reducers/settings.js +111 -0
  71. package/src/lib/reducers/theme.js +48 -0
  72. package/src/lib/reducers/unserializable.js +26 -0
  73. package/src/lib/stack-client.js +401 -0
  74. package/src/lib/stack.js +79 -0
  75. package/src/lib/store/index.js +44 -0
  76. package/src/locales/de.json +57 -0
  77. package/src/locales/en.json +57 -0
  78. package/src/locales/es.json +57 -0
  79. package/src/locales/fr.json +57 -0
  80. package/src/locales/it.json +57 -0
  81. package/src/locales/ja.json +57 -0
  82. package/src/locales/nl_NL.json +57 -0
  83. package/src/locales/pl.json +57 -0
  84. package/src/locales/ru.json +57 -0
  85. package/src/locales/sq.json +57 -0
  86. package/src/locales/zh_CN.json +57 -0
  87. package/src/proptypes/index.js +10 -0
  88. package/src/queries/index.js +16 -0
  89. package/src/styles/apps.css +248 -0
  90. package/src/styles/banner.css +64 -0
  91. package/src/styles/bar.css +106 -0
  92. package/src/styles/base.css +21 -0
  93. package/src/styles/claudy.css +98 -0
  94. package/src/styles/drawer.css +126 -0
  95. package/src/styles/index.styl +33 -0
  96. package/src/styles/indicators.css +58 -0
  97. package/src/styles/nav.css +81 -0
  98. package/src/styles/navigation_item.css +34 -0
  99. package/src/styles/searchbar.css +156 -0
  100. package/src/styles/settings.css +34 -0
  101. package/src/styles/storage.css +22 -0
  102. package/src/styles/supportModal.css +20 -0
  103. package/src/styles/theme.styl +25 -0
@@ -0,0 +1,145 @@
1
+ import React, { Component } from 'react'
2
+ import {
3
+ setContent,
4
+ unsetContent,
5
+ setLocale,
6
+ setTheme,
7
+ setWebviewContext
8
+ } from 'lib/reducers'
9
+
10
+ import { locations, getJsApiName, getReactApiName } from 'lib/api/helpers'
11
+
12
+ // The React API need unique IDs, so we will increment this variable
13
+ let idToIncrement = 0
14
+
15
+ /**
16
+ * Wraps argument into a React element if it is a string. Is used
17
+ * for setBar{Left,Right,Center} to be able to pass HTML
18
+ *
19
+ * @param {ReactElement|string} v
20
+ * @return {ReactElement}
21
+ */
22
+ const wrapInElement = v => {
23
+ if (typeof v === 'string') {
24
+ return <span dangerouslySetInnerHTML={{ __html: v }} />
25
+ } else {
26
+ return v
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Creates a React component that enables to access store
32
+ * properties in a declarative way.
33
+ *
34
+ * @param {BarStore} store
35
+ */
36
+ const barContentComponent = (store, location) =>
37
+ class BarContent extends Component {
38
+ componentDidMount() {
39
+ this.componentId = idToIncrement++
40
+ this.setContent(this.props.children)
41
+ }
42
+
43
+ setContent(content) {
44
+ try {
45
+ content = React.Children.only(content)
46
+ // eslint-disable-next-line no-empty
47
+ } catch (e) {}
48
+ store.dispatch(setContent(location, content, this.componentId))
49
+ }
50
+
51
+ unsetContent() {
52
+ store.dispatch(unsetContent(location, this.componentId))
53
+ }
54
+
55
+ componentWillUnmount() {
56
+ this.unsetContent()
57
+ }
58
+
59
+ componentDidUpdate(prevProps) {
60
+ if (this.props.children !== prevProps.children) {
61
+ this.setContent(this.props.children)
62
+ }
63
+ }
64
+
65
+ render() {
66
+ return null
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Creates a public API
72
+ *
73
+ * - getters/setters for public attributes
74
+ * - React components that act as getters/setters
75
+ *
76
+ * @param {ReduxStore} store - Store on which the API will act
77
+ * @return {object} - Methods of the public API
78
+ */
79
+ export const createBarAPI = store => {
80
+ // setBar{Left,Right,Center} and <Bar{Left,Right,Center} />
81
+ const methods = {}
82
+ locations.forEach(location => {
83
+ // expose JS API
84
+ methods[getJsApiName(location)] = value =>
85
+ store.dispatch(setContent(location, wrapInElement(value), 'js'))
86
+
87
+ // expose React API
88
+ methods[getReactApiName(location)] = barContentComponent(store, location)
89
+ })
90
+
91
+ methods.setLocale = (...args) => {
92
+ store.dispatch(setLocale(...args))
93
+ }
94
+
95
+ methods.setTheme = (...args) => {
96
+ store.dispatch(setTheme(...args))
97
+ }
98
+
99
+ methods.setWebviewContext = (...args) => {
100
+ store.dispatch(setWebviewContext(...args))
101
+ }
102
+
103
+ return methods
104
+ }
105
+
106
+ // Handle exceptions for API before init
107
+ const showAPIError = name => {
108
+ // eslint-disable-next-line no-console
109
+ console.error(
110
+ `You tried to use the CozyBar API (${name}) but the CozyBar is not initialised yet via cozy.bar.init(...).`
111
+ )
112
+ }
113
+
114
+ const makeProxyMethodToAPI = (exposedAPI, fnName) => {
115
+ return (...args) => {
116
+ if (exposedAPI[fnName]) {
117
+ return exposedAPI[fnName](...args)
118
+ } else {
119
+ showAPIError(fnName)
120
+ }
121
+ }
122
+ }
123
+
124
+ /** Creates an API that swallows error until bar is correctly initialized */
125
+ export const createBarProxiedAPI = exposedAPI => {
126
+ const apiReferences = {}
127
+
128
+ locations.forEach(location => {
129
+ const jsAPIName = getJsApiName(location)
130
+ const reactAPIName = getReactApiName(location)
131
+ apiReferences[jsAPIName] = makeProxyMethodToAPI(exposedAPI, jsAPIName)
132
+ apiReferences[reactAPIName] = props => {
133
+ if (exposedAPI[reactAPIName]) {
134
+ return React.createElement(exposedAPI[reactAPIName], props)
135
+ } else {
136
+ showAPIError(reactAPIName)
137
+ }
138
+ }
139
+ })
140
+
141
+ for (let fnName of ['setLocale', 'setTheme', 'setWebviewContext']) {
142
+ apiReferences[fnName] = makeProxyMethodToAPI(exposedAPI, fnName)
143
+ }
144
+ return apiReferences
145
+ }
@@ -0,0 +1,89 @@
1
+ class ForbiddenException extends Error {
2
+ constructor(message) {
3
+ super()
4
+
5
+ this.name = 'Forbidden'
6
+ this.status = 403
7
+ this.message =
8
+ message ||
9
+ 'The application does not have permission to access this resource.'
10
+ this.stack = new Error().stack
11
+ }
12
+ }
13
+
14
+ class ServerErrorException extends Error {
15
+ constructor(message) {
16
+ super()
17
+
18
+ this.name = 'ServerError'
19
+ this.status = 500
20
+ this.message = message || 'A server error occurred'
21
+ this.stack = new Error().stack
22
+ }
23
+ }
24
+
25
+ class NotFoundException extends Error {
26
+ constructor(message) {
27
+ super()
28
+
29
+ this.name = 'NotFound'
30
+ this.status = 404
31
+ this.message = message || 'The ressource was not found'
32
+ this.stack = new Error().stack
33
+ }
34
+ }
35
+
36
+ class MethodNotAllowedException extends Error {
37
+ constructor(message) {
38
+ super()
39
+
40
+ this.name = 'MethodNotAllowed'
41
+ this.status = 405
42
+ this.message = message || 'Method not allowed'
43
+ this.stack = new Error().stack
44
+ }
45
+ }
46
+
47
+ class UnavailableStackException extends Error {
48
+ constructor(message) {
49
+ super()
50
+
51
+ this.name = 'UnavailableStack'
52
+ this.message = message || 'The stack is temporarily unavailable'
53
+ this.stack = new Error().stack
54
+ }
55
+ }
56
+
57
+ class UnauthorizedStackException extends Error {
58
+ constructor(message) {
59
+ super()
60
+
61
+ this.name = 'UnauthorizedStack'
62
+ this.status = 401
63
+ this.message =
64
+ message || 'The app is not allowed to access to the requested resource'
65
+ this.stack = new Error().stack
66
+ }
67
+ }
68
+
69
+ class UnavailableSettingsException extends Error {
70
+ constructor(message) {
71
+ super()
72
+
73
+ this.name = 'UnavailableSettings'
74
+ this.message =
75
+ message ||
76
+ "The 'Settings' application isn't available or installed in the stack"
77
+ this.stack = new Error().stack
78
+ }
79
+ }
80
+
81
+ export {
82
+ ForbiddenException,
83
+ ServerErrorException,
84
+ NotFoundException,
85
+ MethodNotAllowedException,
86
+ UnavailableStackException,
87
+ UnavailableSettingsException,
88
+ UnauthorizedStackException
89
+ }
@@ -0,0 +1,13 @@
1
+ export default function(fn, duration, keyFn) {
2
+ const memo = {}
3
+ return arg => {
4
+ const key = keyFn(arg)
5
+ const memoInfo = memo[key]
6
+ const uptodate =
7
+ memoInfo && memoInfo.result && memoInfo.date - Date.now() < duration
8
+ if (!uptodate) {
9
+ memo[key] = { result: fn(arg), date: Date.now() }
10
+ }
11
+ return memo[key].result
12
+ }
13
+ }
@@ -0,0 +1,77 @@
1
+ const cache = {}
2
+
3
+ const mimeTypes = {
4
+ gif: 'image/gif',
5
+ ico: 'image/vnd.microsoft.icon',
6
+ jpeg: 'image/jpeg',
7
+ jpg: 'image/jpeg',
8
+ png: 'image/png',
9
+ svg: 'image/svg+xml'
10
+ }
11
+
12
+ /**
13
+ * Get an icon URL usable in the HTML page from it's stack path
14
+ *
15
+ * @function
16
+ * @private
17
+ * @param {function} iconFetcher - takes an icon path on the stack
18
+ * and returns a fetch response with the icon
19
+ * @param {object} app - app object with a `links.icon` attribute
20
+ * @param {boolean} useCache
21
+ * @returns {Promise} url string of an icon usable in the HTML page
22
+ * may be empty if the `app` object didn't have an icon path
23
+ */
24
+ export default async function getIcon(iconFetcher, app = {}, useCache = true) {
25
+ if (useCache && cache.icons && cache.icons[url]) return cache.icons[url]
26
+
27
+ const url = app.links && app.links.icon
28
+ if (!url) return ''
29
+ let icon
30
+
31
+ try {
32
+ const resp = await iconFetcher(url)
33
+ if (!resp.ok)
34
+ throw new Error(`Error while fetching icon ${resp.statusText}: ${url}`)
35
+ icon = await resp.blob()
36
+ } catch (error) {
37
+ throw error
38
+ }
39
+ if (!icon.type) {
40
+ // iOS10 does not set correctly mime type for images, so we assume
41
+ // that an empty mime type could mean that the app is running on iOS10.
42
+ // For regular images like jpeg, png or gif it still works well in the
43
+ // Safari browser but not for SVG.
44
+ // So let's set a mime type manually. We cannot always set it to
45
+ // image/svg+xml and must guess the mime type based on the icon attribute
46
+ // from app/manifest
47
+ // See https://stackoverflow.com/questions/38318411/uiwebview-on-ios-10-beta-not-loading-any-svg-images
48
+ if (!app.icon) {
49
+ throw new Error(`${app.name}: Cannot detect mime type for icon ${url}`)
50
+ }
51
+
52
+ const extension = app.icon.split('.').pop()
53
+
54
+ if (!extension) {
55
+ throw new Error(
56
+ `${app.name}: Unable to detect icon mime type from extension (${app.icon})`
57
+ )
58
+ }
59
+
60
+ if (!mimeTypes[extension]) {
61
+ throw new Error(`${app.name}: 'Unexpected icon extension (${app.icon})`)
62
+ }
63
+
64
+ icon = new Blob([icon], { type: mimeTypes[extension] })
65
+ }
66
+
67
+ if (icon.type.match(/^image\/.*$/)) {
68
+ const iconURL = URL.createObjectURL(icon)
69
+ if (useCache) {
70
+ cache.icons = cache.icons || {}
71
+ cache.icons[url] = iconURL
72
+ }
73
+
74
+ return iconURL
75
+ }
76
+ throw new Error(`${app.name}: icon ${url} is not an image.`)
77
+ }
@@ -0,0 +1,16 @@
1
+ import { cozyFetchJSON } from 'lib/stack'
2
+
3
+ // This is a function that does the bare minimum in order to bypass the normal intent flow. To be replaced in th next version of intents.
4
+ export function fetchRawIntent(action, type, data = {}, permissions = []) {
5
+ return cozyFetchJSON(null, 'POST', '/intents', {
6
+ data: {
7
+ type: 'io.cozy.intents',
8
+ attributes: {
9
+ action: action,
10
+ type: type,
11
+ data: data,
12
+ permissions: permissions
13
+ }
14
+ }
15
+ })
16
+ }
@@ -0,0 +1,11 @@
1
+ import flag from 'cozy-flags'
2
+ import _minilog from '@cozy/minilog'
3
+
4
+ const minilog = window.minilog || _minilog
5
+ const logger = minilog('cozy-bar')
6
+
7
+ if (!flag('bar.debug')) {
8
+ minilog.suggest.deny('cozy-bar', 'info')
9
+ }
10
+
11
+ export default logger
@@ -0,0 +1,57 @@
1
+ import { extend as extendI18n } from 'cozy-ui/react/I18n'
2
+ import { SET_LOCALE } from 'lib/reducers/locale'
3
+
4
+ const extendI18nWithApp = lang => app => {
5
+ const { langs, locales } = app
6
+
7
+ const hasLangs = langs && langs.length
8
+ if (!hasLangs) {
9
+ // TODO The app does not provide langs, we should probably warn the developer
10
+ // when the app is published on the registry.
11
+ return app
12
+ }
13
+
14
+ const providesLang = hasLangs && langs.includes(lang)
15
+ const currentLang = providesLang ? lang : langs[0]
16
+
17
+ const localeKeys = locales && Object.keys(locales)
18
+ const providesLocales =
19
+ localeKeys && localeKeys.length && localeKeys.includes(currentLang)
20
+
21
+ if (!providesLocales) {
22
+ // TODO The app does not provide locales, we should probably warn the developer
23
+ // when the app is published on the regisry.
24
+ return app
25
+ }
26
+
27
+ extendI18n({ [app.slug]: locales[currentLang] })
28
+ return app
29
+ }
30
+
31
+ const useLang = (apps, lang) => {
32
+ apps && apps.forEach(extendI18nWithApp(lang))
33
+ }
34
+
35
+ export const appsI18nMiddleware = ({ getState }) => next => action => {
36
+ const state = getState()
37
+ switch (action.type) {
38
+ case SET_LOCALE: {
39
+ const apps = state.apps && state.apps.apps
40
+ useLang(apps, action.lang)
41
+ break
42
+ }
43
+ case 'RECEIVE_APP_LIST':
44
+ action.apps &&
45
+ action.apps.length &&
46
+ action.apps.forEach(extendI18nWithApp(state.locale))
47
+ break
48
+ case 'RECEIVE_APP':
49
+ action.app &&
50
+ extendI18nWithApp(state.locale && state.locale.lang)(action.app)
51
+ break
52
+ }
53
+
54
+ return next(action)
55
+ }
56
+
57
+ export default appsI18nMiddleware
@@ -0,0 +1,43 @@
1
+ import CozyRealtime from 'cozy-realtime'
2
+
3
+ const APPS_DOCTYPE = 'io.cozy.apps'
4
+
5
+ /**
6
+ * Initialize realtime sockets
7
+ *
8
+ * @private
9
+ * @param {object}
10
+ * @returns {Promise}
11
+ */
12
+ function initializeRealtime({ getApp, onCreate, onDelete, cozyClient }) {
13
+ const handleAppCreation = async app => {
14
+ // Fetch directly the app to get attributes `related` as well.
15
+ let fullApp
16
+ try {
17
+ fullApp = await getApp(app.slug)
18
+ } catch (error) {
19
+ throw new Error(`Cannot fetch app ${app.slug}: ${error.message}`)
20
+ }
21
+
22
+ if (typeof onCreate === 'function') {
23
+ onCreate(fullApp)
24
+ }
25
+ }
26
+
27
+ const handleAppRemoval = app => {
28
+ if (typeof onDelete === 'function') {
29
+ onDelete(app)
30
+ }
31
+ }
32
+
33
+ try {
34
+ const realtime = new CozyRealtime({ client: cozyClient })
35
+ realtime.subscribe('created', APPS_DOCTYPE, handleAppCreation)
36
+ realtime.subscribe('deleted', APPS_DOCTYPE, handleAppRemoval)
37
+ } catch (error) {
38
+ // eslint-disable-next-line no-console
39
+ console.warn(`Cannot initialize realtime in Cozy-bar: ${error.message}`)
40
+ }
41
+ }
42
+
43
+ export default initializeRealtime
@@ -0,0 +1,175 @@
1
+ import stack from 'lib/stack'
2
+ import unionWith from 'lodash.unionwith'
3
+
4
+ // constants
5
+ const DELETE_APP = 'DELETE_APP'
6
+ const RECEIVE_APP = 'RECEIVE_APP'
7
+ const RECEIVE_APP_LIST = 'RECEIVE_APP_LIST'
8
+ const RECEIVE_HOME_APP = 'RECEIVE_HOME_APP'
9
+ const FETCH_APPS = 'FETCH_APPS'
10
+ const FETCH_APPS_FAILURE = 'FETCH_APPS_FAILURE'
11
+ const SET_INFOS = 'SET_INFOS'
12
+
13
+ export const isCurrentApp = (state, app) => app.slug === state.appSlug
14
+
15
+ // selectors
16
+ export const getApps = state => {
17
+ if (!state.apps) return []
18
+ return state.apps
19
+ }
20
+
21
+ export const getHomeApp = state => {
22
+ return state.homeApp
23
+ }
24
+
25
+ export const isFetchingApps = state => {
26
+ return state ? state.isFetching : false
27
+ }
28
+
29
+ export const hasFetched = state => state.hasFetched
30
+
31
+ // actions
32
+ export const deleteApp = app => ({ type: DELETE_APP, app })
33
+ export const receiveApp = app => ({ type: RECEIVE_APP, app })
34
+ const receiveAppList = apps => ({ type: RECEIVE_APP_LIST, apps })
35
+ const receiveHomeApp = homeApp => ({ type: RECEIVE_HOME_APP, homeApp })
36
+ export const setInfos = (appName, appNamePrefix, appSlug) => ({
37
+ type: SET_INFOS,
38
+ appName,
39
+ appNamePrefix,
40
+ appSlug
41
+ })
42
+
43
+ // actions async
44
+ export const fetchApps = () => async dispatch => {
45
+ try {
46
+ dispatch({ type: FETCH_APPS })
47
+ const rawAppList = await stack.get.apps()
48
+ const apps = rawAppList.map(mapApp)
49
+ if (!rawAppList.length)
50
+ throw new Error('No installed apps found by the bar')
51
+ // TODO load only one time icons
52
+ await dispatch(setDefaultApp(apps))
53
+ await dispatch(receiveAppList(apps))
54
+ } catch (e) {
55
+ dispatch({ type: FETCH_APPS_FAILURE })
56
+ // eslint-disable-next-line no-console
57
+ console.warn(e.message ? e.message : e)
58
+ }
59
+ }
60
+
61
+ /**
62
+ *
63
+ * @param {Array} appsList
64
+ */
65
+ export const setDefaultApp = appsList => async dispatch => {
66
+ try {
67
+ const context = await stack.get.context()
68
+ const defaultRedirection =
69
+ context.data &&
70
+ context.data.attributes &&
71
+ context.data.attributes.default_redirection
72
+ let homeApp = null
73
+ // self hosted cozy has no context by default
74
+ // so let's use hardcoded home slug if needed
75
+ if (!defaultRedirection) {
76
+ const HOME_APP_SLUG = 'home'
77
+ homeApp = findAppInArray(HOME_APP_SLUG, appsList)
78
+ } else {
79
+ const slugRegexp = /^([^/]+)\/.*/
80
+ const matches = defaultRedirection.match(slugRegexp)
81
+ const defaultAppSlug = matches && matches[1]
82
+ homeApp = findAppInArray(defaultAppSlug, appsList)
83
+ }
84
+
85
+ if (homeApp) {
86
+ return dispatch(receiveHomeApp(homeApp))
87
+ }
88
+ } catch (error) {
89
+ // eslint-disable-next-line no-console
90
+ console.warn(`Cozy-bar cannot fetch home app data: ${error.message}`)
91
+ }
92
+ }
93
+
94
+ // reducers
95
+ const defaultState = {
96
+ apps: [],
97
+ homeApp: null,
98
+ isFetching: true,
99
+ appName: null,
100
+ appNamePrefix: null,
101
+ appSlug: null,
102
+ hasFetched: false
103
+ }
104
+
105
+ const reducer = (state = defaultState, action) => {
106
+ switch (action.type) {
107
+ case FETCH_APPS:
108
+ return { ...state, isFetching: true }
109
+ case FETCH_APPS_FAILURE:
110
+ return { ...state, isFetching: false }
111
+ case RECEIVE_APP:
112
+ return {
113
+ ...state,
114
+ apps: unionWith(
115
+ state.apps,
116
+ [mapApp(action.app)],
117
+ (appA, appB) => appA.slug === appB.slug
118
+ )
119
+ }
120
+ case RECEIVE_APP_LIST: {
121
+ const appsList = action.apps.map(app => ({
122
+ ...app,
123
+ isCurrentApp: isCurrentApp(state, app)
124
+ }))
125
+ return { ...state, isFetching: false, hasFetched: true, apps: appsList }
126
+ }
127
+ case RECEIVE_HOME_APP: {
128
+ const homeApp = action.homeApp
129
+ return isCurrentApp(state, homeApp)
130
+ ? {
131
+ ...state,
132
+ homeApp: { ...homeApp, isCurrentApp: true }
133
+ }
134
+ : { ...state, homeApp }
135
+ }
136
+ case DELETE_APP:
137
+ return {
138
+ ...state,
139
+ apps: state.apps.filter(app => app.slug !== action.app.slug)
140
+ }
141
+ case SET_INFOS:
142
+ return {
143
+ ...state,
144
+ appName: action.appName,
145
+ appNamePrefix: action.appNamePrefix,
146
+ appSlug: action.appSlug
147
+ }
148
+ default:
149
+ return state
150
+ }
151
+ }
152
+
153
+ export default reducer
154
+
155
+ // helpers
156
+ const camelCasify = object =>
157
+ !!object &&
158
+ Object.keys(object).reduce((acc, key) => {
159
+ const camelCaseKey = key
160
+ .split('_')
161
+ .map((segment, index) =>
162
+ index ? segment.charAt(0).toUpperCase() + segment.slice(1) : segment
163
+ )
164
+ .join('')
165
+ acc[camelCaseKey] = object[key]
166
+ return acc
167
+ }, {})
168
+
169
+ const mapApp = app => ({
170
+ ...app,
171
+ ...camelCasify(app.attributes),
172
+ href: app.links && app.links.related
173
+ })
174
+
175
+ const findAppInArray = (appSlug, apps) => apps.find(app => app.slug === appSlug)
@@ -0,0 +1,59 @@
1
+ import CozyClient from 'cozy-client'
2
+ import client from 'lib/stack-client.js'
3
+ import stack from 'lib/stack.js'
4
+ import { setDefaultApp } from './apps'
5
+
6
+ const cozyURL = 'https://test.mycozy.cloud'
7
+ const token = 'mytoken'
8
+ const fakeStackClient = {
9
+ uri: cozyURL,
10
+ token: { token }
11
+ }
12
+ describe('app reducer', () => {
13
+ beforeAll(() => {
14
+ jest.clearAllMocks()
15
+ const params = {
16
+ cozyClient: new CozyClient({ fakeStackClient }),
17
+ onCreate: function() {},
18
+ onDelete: function() {}
19
+ }
20
+
21
+ stack.init(params)
22
+ })
23
+ it('dispatch RECEIVE_HOME_APP if context has default redirection', async () => {
24
+ jest.spyOn(client.get, 'context').mockResolvedValue({
25
+ data: { attributes: { default_redirection: 'home/' } }
26
+ })
27
+ const dispatchMock = jest.fn(x => x)
28
+
29
+ const setted = setDefaultApp([{ slug: 'home' }])
30
+ await setted(dispatchMock)
31
+ expect(dispatchMock).toHaveBeenCalledWith({
32
+ homeApp: { slug: 'home' },
33
+ type: 'RECEIVE_HOME_APP'
34
+ })
35
+ })
36
+
37
+ it('dispatch RECEIVE_HOME_APP with hardcoded home if context has no default redirection', async () => {
38
+ jest.spyOn(client.get, 'context').mockResolvedValue({})
39
+ const dispatchMock = jest.fn(x => x)
40
+
41
+ const setted = setDefaultApp([{ slug: 'home' }])
42
+ await setted(dispatchMock)
43
+ expect(dispatchMock).toHaveBeenCalledWith({
44
+ homeApp: { slug: 'home' },
45
+ type: 'RECEIVE_HOME_APP'
46
+ })
47
+ })
48
+
49
+ it('dispatch nothing if the default redirection if not in the apps array', async () => {
50
+ jest.spyOn(client.get, 'context').mockResolvedValue({
51
+ data: { attributes: { default_redirection: 'drive/' } }
52
+ })
53
+ const dispatchMock = jest.fn(x => x)
54
+
55
+ const setted = setDefaultApp([{ slug: 'home' }])
56
+ await setted(dispatchMock)
57
+ expect(dispatchMock).not.toHaveBeenCalled()
58
+ })
59
+ })