diff --git a/.eslintrc.js b/.eslintrc.js index 8a4f5246f5034f96f455c4bc9a6c6d92d109d269..2cbc020ed844954fee7f93bfafc07509dd6a9352 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,12 @@ module.exports = { + plugins: [ + '@typescript-eslint', + 'react', + 'react-hooks', + 'jest', + 'jsx-a11y', + '@eslint-react/eslint-plugin', + ], extends: [ 'eslint:recommended', 'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react @@ -15,6 +23,8 @@ module.exports = { 'plugin:@typescript-eslint/recommended', // Stylistic rule configurations, for consistent and predictable syntax usage 'plugin:@typescript-eslint/stylistic-type-checked', + // Eslint React plugin + 'plugin:@eslint-react/recommended-legacy', ], files: ['**/*.{ts,tsx}'], parserOptions: { @@ -31,6 +41,12 @@ module.exports = { '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-unnecessary-type-assertion': 'error', + '@typescript-eslint/default-param-last': 'error', + '@typescript-eslint/prefer-for-of': 'warn', + '@typescript-eslint/prefer-includes': 'error', + '@typescript-eslint/prefer-string-starts-ends-with': 'error', + '@typescript-eslint/switch-exhaustiveness-check': 'error', + // causes a build error and has a lot of effects on components '@typescript-eslint/prefer-nullish-coalescing': 'off', @@ -51,6 +67,17 @@ module.exports = { // a11y label fix, nesting is enough 'jsx-a11y/label-has-associated-control': 0, + + // Note: you must disable the base rule as it can report incorrect errors + 'require-await': 'off', + '@typescript-eslint/require-await': 'error', + + // disable recommandations + '@eslint-react/hooks-extra/no-redundant-custom-hook': 0, // great rule but should not be run in tests files + '@eslint-react/hooks-extra/no-direct-set-state-in-use-effect': 0, // to enable in another MR will have a lot of impact + '@eslint-react/dom/no-dangerously-set-innerhtml': 0, // used for for injecting html tags + '@eslint-react/no-array-index-key': 0, + '@eslint-react/no-unused-class-component-members': 0, // disabled for ".d.ts" files }, }, { @@ -61,7 +88,6 @@ module.exports = { }, }, ], - plugins: ['@typescript-eslint', 'react', 'react-hooks', 'jest', 'jsx-a11y'], parser: '@typescript-eslint/parser', // Specifies the ESLint parser parserOptions: { ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features @@ -82,6 +108,7 @@ module.exports = { 'spaced-comment': ['error', 'always', { block: { exceptions: ['*'] } }], 'react/self-closing-comp': 'warn', 'react/jsx-curly-brace-presence': ['error'], + 'react/jsx-no-useless-fragment': ['error'], // Rule to suggest using useAppDispatch instead of regular useDispatch 'no-restricted-imports': 'off', diff --git a/.gitlab/merge_request_templates/[QA] Ecolyo.md b/.gitlab/merge_request_templates/[QA] Ecolyo.md index 823451ecf3b71a6b4c972cd4cec38940a3f53b5a..7ed6971bfe60d29ae6168c2150a81df3df1bb8eb 100644 --- a/.gitlab/merge_request_templates/[QA] Ecolyo.md +++ b/.gitlab/merge_request_templates/[QA] Ecolyo.md @@ -4,63 +4,75 @@ # Cahier de recette pour la version _x.x.x_ -## Tests de non régression sur la [Nouvelle instance](https://ecolyo.ecolyo-x-x.cozy.self-data.alpha.grandlyon.com/#/) +## Tests de non régression sur la [Nouvelle instance](https://ecolyoXX-ecolyo.cozy.self-data.alpha.grandlyon.com/#/) -1. **Onboarding** - - [ ] Le chargement de l'application fonctionne correctement - - [ ] La modal "Vous êtes à mis chemin" est présentée - - [ ] Les CGU sont demandées +### 1. Onboarding -2. **Comportement sans connecteur connecté** - - [ ] La page conso pousse à la connexion - - [ ] La page analyse pousse à la connexion +- [ ] Le chargement de l'application fonctionne correctement +- [ ] La modal "Vous êtes à mis chemin" est présentée +- [ ] Les CGU sont demandées -3. **Lancement des connecteurs** - - [ ] SGE - - [ ] EPGL - - [ ] GRDF +### 2. **Comportement sans connecteur connecté** -4. **Page Défis** - - [ ] L'utilisateur peut lancer le 1er défi +- [ ] La page conso pousse à la connexion +- [ ] La page analyse pousse à la connexion -5. **Conso** +### 3. **Lancement des connecteurs** -**Premier lancement** - - [ ] La profondeur de données va jusqu'à 1 an dans la conso pour les pas de temps journalier - - [ ] La profondeur de données à la 1/2h (elec) va jusqu'à 1 semaine +- [ ] SGE +- [ ] EPGL +- [ ] GRDF -**Le lendemain** - - [ ] La profondeur de données va jusqu'à 3 ans pour l'élec et le gaz - - [ ] La profondeur de données va jusqu'à novembre 2022 pour l'eau (à cause du passage en régie) - - [ ] La profondeur de données à la 1/2h (elec) va jusqu'à 1 mois +### 4. **Page Défis** -6. **Analyse** - - [ ] La profondeur de données va jusqu'à 3 mois antérieur - - [ ] Les modules de l'analyse sont tous fonctionnels (à l'exception du special elec qui devra être déclenché par un service) - - [ ] Le module de comparaison des températures s'affiche dans la comparaison annuelle et mensuelle +- [ ] L'utilisateur peut lancer le 1er défi -7. **Page Astuces** - - [ ] Chargement de la base des astuces dans "Toutes" - - [ ] Lancement du module du choix des astuces Ok - - [ ] Le profil raccourci est proposé si le profil complet n'est pas renseigné +### 5. **Conso** -8. **Page Options** - - [ ] L'utilisateur est inscrit par défaut à la newsletter - - [ ] L'utilisateur n'est pas exclu des statistiques d'usage MATOMO - - [ ] L'accès au SAU est fonctionnel - - [ ] Les mentions légales et les CGU sont accessibles - - [ ] La page d'accessibilité est accessible - - [ ] L'export de données fonctionne quand un connecteur est connecté +#### Premier lancement +- [ ] La profondeur de données va jusqu'à 1 an dans la conso pour les pas de temps journalier +- [ ] La profondeur de données à la 1/2h (elec) va jusqu'à 1 semaine -9. **Icône raccourci** - - [ ] L'utilisateur peut ajouter l'application en raccourci (pwa android) - - [ ] L'utilisateur peut ajouter l'application en raccourci (pwa iPhone) +#### Le lendemain -10. **Autres** - - [ ] Les informations de navigation remontent dans le matomo recette - > 💡 Aller consulter des écogestes en particulier, repérer leurs id et vérifier sur matomo dans "Comportement / Pages" +- [ ] La profondeur de données va jusqu'à 3 ans pour l'élec et le gaz +- [ ] La profondeur de données va jusqu'à novembre 2022 pour l'eau (à cause du passage en régie) +- [ ] La profondeur de données à la 1/2h (elec) va jusqu'à 1 mois +#### Prix + +- [ ] modifier le prix d'un des fluides (électricité ou gaz) dans le backoffice de recette, relancer les connecteurs pour mettre à jour les prix (= correspond à la quantité multipliée par le prix modifié). Attention à ne modifier des prix que sur la dernière année. + +### 6. **Analyse** + +- [ ] La profondeur de données va jusqu'à 3 mois antérieur +- [ ] Les modules de l'analyse sont tous fonctionnels (à l'exception du special elec qui devra être déclenché par un service) +- [ ] Le module de comparaison des températures s'affiche dans la comparaison annuelle et mensuelle + +### 7. **Page Astuces** + +- [ ] Chargement de la base des astuces dans "Toutes" +- [ ] Lancement du module du choix des astuces Ok +- [ ] Le profil raccourci est proposé si le profil complet n'est pas renseigné + +### 8. **Page Options** + +- [ ] L'utilisateur n'est pas inscrit par défaut à la newsletter +- [ ] L'utilisateur n'est pas exclu des statistiques d'usage MATOMO +- [ ] L'accès au SAU est fonctionnel +- [ ] Les mentions légales et les CGU sont accessibles +- [ ] La page d'accessibilité est accessible +- [ ] L'export de données fonctionne quand un connecteur est connecté + +### 9. **Icône raccourci** + +- [ ] L'utilisateur peut ajouter l'application en raccourci (pwa android) +- [ ] L'utilisateur peut ajouter l'application en raccourci (pwa iPhone) + +### 10. **Autres** + +- [ ] Les informations de navigation remontent dans le matomo recette > 💡 Aller consulter des écogestes en particulier, repérer leurs id et vérifier sur matomo dans "Comportement / Pages" ## Tests des nouvelles fonctionnalités @@ -68,9 +80,9 @@ - [ ] ... -## Migrations [Instance historique](https://ecolyo.ecolyodemo.cozy.self-data.alpha.grandlyon.com) +## Migrations [Instance historique](https://ecolyodemo-ecolyo.cozy.self-data.alpha.grandlyon.com) -_Si une migration était nécessaire, vérifier quand c'est possible qu'elle a bien eu lieu_ +📝 _Si une migration était nécessaire, vérifier quand c'est possible qu'elle a bien eu lieu_ - [ ] ... diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f0e8b35159a5bf4f79e079474c983f1699f2006..c4a3050ffea10e0921c9d23d91c5601b7ebd1f24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,83 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [3.1.1](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/compare/v3.1.0...v3.1.1) (2024-10-02) + + +### Bug Fixes + +* **app:** remove console.group ([bbcf10f](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/bbcf10f308de9cbad3fbfc953c9bababf3339a45)) + +## [3.1.0](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/compare/v2.8.0...v3.1.0) (2024-09-30) + + +### ⚠ BREAKING CHANGES + +* **grdf:** update error messages + +### Features + +* **a11y:** Add a focus style to components ([bbfa418](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/bbfa418c5ff74cbb7d80013a2bb722b821574f39)) +* **a11y:** Add a quick access link ([ab54126](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/ab54126414c37233b3299b62e1d147be5944c344)) +* **a11y:** Optimize keyboard navigation between pages ([6dae9c4](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/6dae9c439302ff10a688b81fc9acec73b5409d83)) +* **a11y:** Unlocked challenges focusable with keyboard ([6a787ef](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/6a787efa3919f94a8443f6c37306232e37a91fe5)) +* **accessibility:** allow bars to handle keyboard focus ([7bd3d68](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/7bd3d6854d7efcccc4f7c0ed5cb02e13449612b5)) +* add dynamic page titles ([506da0c](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/506da0ce1b6bc1746896afa8ff8279779279396c)) +* **analysis:** add monthly average temperature comparison ([c64756f](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/c64756f1774957567c93cafa91da99034611d5a7)) +* **analysis:** change text order in modal ([c976b9b](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/c976b9b4ed546cece976bbb8758b42045a9aa4cc)) +* **analysis:** Make monthly summary button clickable ([4f966f6](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/4f966f60f54bf3cc0a9b68db35dcdfbb44941650)) +* **challenge:** change challenge description ([c82653c](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/c82653c15145d8c9eca24199c01d09b19f560949)) +* **ecogesture/profile:** add "garden" equipment ([87e6ff1](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/87e6ff137131fb3d0461f4c315b5c9fed90cf425)) +* **ecogestures:** rework ecogesture order and add score ([f9d847e](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/f9d847e0291db0791ce8179bc28903b8aeb458c0)) +* **ecogestures:** unify ecogesture init modal components ([2a6928a](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/2a6928ab9e1797eb2e4e16180f1d24c0a76b026d)) +* fluidsPrices no longer apply prices ([7887b85](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/7887b8540a559dc3fc975cc4113ffde335087aba)) +* GRDF consent email ([819af22](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/819af22356342b33c0e8dfbcc843caaa5ffc4f3a)) +* **grdf:** update error messages ([dfecbc3](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/dfecbc306aaac9dd8faca7e16be062aaa250c795)) +* **grdf:** update login method ([c0709f7](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/c0709f7031caab19d41b49b50a3147b496f43666)) +* improve app load time ([2235acc](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/2235acc72f33caa67a67650cdb9a7a4512fda50f)) +* init prices in scripts ([543eb79](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/543eb79b904b02839f9765253d3982cb087ab2e0)) +* newsletter is now opt-in ([2e78ec2](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/2e78ec262cefa4fe8382d09a335eff5205e05d2a)) +* **newsletter:** incite user to connect his compteur when no data ([bfecbae](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/bfecbaebc0a0da518aab3bcc4f9d00e6c7bb7d2d)) +* node 20 ([391d686](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/391d686728b9b62c9282fd6e8f5c23f5b0ab4121)) +* spread services execution ([a10b40b](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/a10b40bb7eb4d970a5f49b3ce5f4f8107e9fd964)) +* **titles:** change header titles to match page titles ([dd6ea8d](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/dd6ea8d907e8067f621d8201e40508212ede1dd7)) +* **UI:** Use MaterialUI components for input fields ([71e3907](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/71e39078187079fef9b8fb680731cd9ede079dcc)) +* **water:** solidarity pricing ([7e85111](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/7e851115d4fd3972a8f9823331d60ff6d688e0f6)) + + +### Bug Fixes + +* **a11y:** Adapt to small screens 320x256 ([ec2a8b8](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/ec2a8b859dcdc70e11289cb1acca19ffe33d6c48)) +* **a11y:** Add label and description to buttons ([dc57452](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/dc5745235be19e598c6436b36595035e01399f1f)) +* **a11y:** Added aria roles to lists ([93d3b59](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/93d3b59c53f3c367655ca70ecfdcc3b8aaa7d11d)) +* **a11y:** Added autoComplete types to TextFields ([9373300](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/9373300764049087800413ba4627f667eb202116)) +* **a11y:** Adds the required property to inputs ([c86b478](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/c86b478369588c10d3b8aceae017a1e87f695033)) +* **a11y:** allow navigation on help link ([211c477](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/211c477a8abe499adc7b4410ad5d193b6711a1f7)) +* **a11y:** Checkboxes must be accessible ([6f72e8e](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/6f72e8e5b6f42e33bfcb899d656fc43a2600213f)) +* **accessibility:** alt mentions for images ([7aab7ee](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/7aab7ee75e0ec92fc76b09a9b6bff36cdafb76ab)) +* **accessibility:** remove graph shift when navigating with keyboard ([ec41d38](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/ec41d38086797d3b7475d51c802f24a94f0d1f7c)) +* **accessibility:** SAU Link ([79d628a](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/79d628a8cc0f755cb146d9b971904f758b60e36b)) +* add wait consent modal ([017eeeb](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/017eeeb18ec5e5c3a745667ffe0fdca4c70bbeae)) +* **alt:** correct alt for logo des financeurs ([7d53753](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/7d537539aac484ade9e1e12c3f8f06dc375bf0da)) +* **analysis:** adjust temperature loader ([ee0a721](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/ee0a72165c1c77351f126cbf6355f1b2de82b1ee)) +* **analysis:** apple prices for forecast ([3ec3dca](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/3ec3dca37d92d38a79a4abe52df37364a5ff435c)) +* **analysis:** update order of "plus d'infos" modal ([c76d94b](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/c76d94beb1becb502f72b8ecf2f8c9f246da7967)) +* **challenges:** last card height ([3d725f1](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/3d725f136303617666b5d98250e98d87b3756483)) +* **deps:** update dependency cozy-device-helper ([e76a275](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/e76a275a790b41e0c32dc6a0fd6a89189ee88ea3)) +* **deps:** update dependency cozy-flags ([f2dba38](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/f2dba387ea4c4e4458705a8c77b4355deb34257c)) +* **deps:** update dependency cozy-intent ([7416184](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/741618415b2f03f2836215290a4a2844f2084789)) +* **deps:** update dependency cozy-keys-lib & cozy-ui ([4824e44](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/4824e4437fc7e08f19c334b45617c2cc49497c76)) +* **deps:** update dependency cozy-realtime ([4d84114](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/4d84114887d71d5565d4e67f95d6fc6f8465b15a)) +* **deps:** update dependency cozy-scripts ([a71ed93](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/a71ed93b96cbb1b73a96ae1578631bf62573283f)) +* **deps:** update dependency cozy-ui to v86 ([6073651](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/60736513aafd1d29e4f49242701e9e13d1323675)) +* **dju:** use dju sum instead of average ([966a404](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/966a4040edfd708f5e9f9ad120e9f2ccb77482fe)) +* **electricity:** remove loader delay ([de28d30](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/de28d30382e5e20bbbb27ba09c670f869d844570)) +* **equipments:** rename "Garden" to "Extérieur" ([df55664](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/df5566437798e09ea815a206a60dcc892a1d029b)) +* **quiz:** resolve black screen bug when customQuestion is not loaded ([9ae1a47](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/9ae1a47a76bbfdd6a0d7227b1a9b0bf32c339502)) +* render "0" on analysis view ([f311a7f](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/f311a7f87ca9b1459fefb00fada394525f32a01e)) +* transparent loader background ([19f27d8](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/19f27d84b85a0f7e98c76b4b80c226c0162f9596)) +* **UI:** center ecogesture content ([c48af14](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/commit/c48af14054390a87b03be99202837cdc1db1c753)) + ## [3.0.0](https://forge.grandlyon.com/web-et-numerique/factory/llle_project/ecolyo/compare/v2.8.0...v3.0.0) (2024-05-29) diff --git a/manifest.webapp b/manifest.webapp index 3411faf99ff858d614f69402c5defe92f42ab0a7..009d27918cea52d33148e947d1ae607b517c77cd 100644 --- a/manifest.webapp +++ b/manifest.webapp @@ -3,7 +3,7 @@ "slug": "ecolyo", "icon": "public/icon.svg", "categories": ["energy"], - "version": "3.0.0", + "version": "3.1.1", "licence": "AGPL-3.0", "editor": "Métropole de Lyon", "default_locale": "fr", diff --git a/package.json b/package.json index bfdfcccad02db5d79a1c34de412d1153a93760eb..3ff8e0532fc5d2cc90b8f34d84a623517d0eab76 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ecolyo", - "version": "3.0.0", + "version": "3.1.1", "engines": { "node": "20" }, @@ -48,11 +48,11 @@ "@sentry/react": "^8.26.0", "classnames": "^2.5.1", "cozy-bar": "10.0.0", - "cozy-client": "48.21.0", + "cozy-client": "49.1.1", "cozy-device-helper": "3.1.0", "cozy-flags": "4.0.0", "cozy-harvest-lib": "9.26.14", - "cozy-intent": "^2.22.0", + "cozy-intent": "^2.23.0", "cozy-keys-lib": ">=6.0.0", "cozy-logger": ">1.7.0", "cozy-realtime": "5.0.1", @@ -80,6 +80,7 @@ }, "devDependencies": { "@babel/preset-typescript": "^7.7.4", + "@eslint-react/eslint-plugin": "^1.14.3", "@testing-library/dom": "^9.3.3", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.3.0", diff --git a/scripts/createDayDataFiles.js b/scripts/createDayDataFiles.js index 1302b0384c784baaba9e3df4b934989f1e2c5f3b..94a78e4a64ac98c7f069dfa9b5dbd298d2bd73fc 100644 --- a/scripts/createDayDataFiles.js +++ b/scripts/createDayDataFiles.js @@ -5,6 +5,7 @@ const fs = require('fs') const { DateTime } = require('luxon') const config = require('./config') +const fluidConfig = require('../src/constants/config.json').fluidConfig function getRandomNumber(min, max) { // Generate a random float between min and max @@ -32,13 +33,14 @@ const generateHalfAnHourData = (startingDate, endingDate, min, max) => { day: parsingDate.day, hour: parsingDate.hour, minute: parsingDate.minute, + price: load * fluidConfig[0].coefficient, }) parsingDate = parsingDate.plus({ minute: 30 }) } return halfAnHourDumpArray } -const generateData = (startingDate, endingDate, min, max) => { +const generateData = (startingDate, endingDate, min, max, fluidType) => { let parsingDate = DateTime.local( startingDate.year, startingDate.month, @@ -54,6 +56,7 @@ const generateData = (startingDate, endingDate, min, max) => { day: parsingDate.day, hour: 0, minute: 0, + price: load * fluidConfig[fluidType].coefficient, }) parsingDate = parsingDate.plus({ days: 1 }) } @@ -94,9 +97,11 @@ function aggregateLoadData(data, period) { day: entryCopy.day, hour: entryCopy.hour, minute: entryCopy.minute, + price: entryCopy.price, } } else { aggregatedData[key].load += entryCopy.load + aggregatedData[key].price += entryCopy.price } }) return Object.values(aggregatedData) @@ -125,17 +130,18 @@ const elecGeneratedDayData = generateData( startingDate, halfHourStartingDate.minus({ days: 1 }), 4, - 8 + 8, + 0 ) const elecDayData = [...elecAggregatedDayData, ...elecGeneratedDayData] const elecMonthData = aggregateLoadData(elecDayData, 'month') const elecYearData = aggregateLoadData(elecMonthData, 'year') -const gasDayData = generateData(startingDate, endingDate, 16, 68) +const gasDayData = generateData(startingDate, endingDate, 16, 68, 2) const gasMonthData = aggregateLoadData(gasDayData, 'month') const gasYearData = aggregateLoadData(gasMonthData, 'year') -const waterDayData = generateData(startingDate, endingDate, 200, 300) +const waterDayData = generateData(startingDate, endingDate, 200, 300, 1) const waterMonthData = aggregateLoadData(waterDayData, 'month') const waterYearData = aggregateLoadData(waterMonthData, 'year') diff --git a/src/components/Action/ActionDone/ActionDone.spec.tsx b/src/components/Action/ActionDone/ActionDone.spec.tsx index 038a6dd206fee247504430ae81e435a579e305b0..f377200ae1ac9975b1c0611ae2401f70bda628eb 100644 --- a/src/components/Action/ActionDone/ActionDone.spec.tsx +++ b/src/components/Action/ActionDone/ActionDone.spec.tsx @@ -16,7 +16,7 @@ jest.mock('services/challenge.service', () => { describe('ActionDone component', () => { const store = createMockEcolyoStore() - it('should be rendered correctly', async () => { + it('should be rendered correctly', () => { const { container } = render( <Provider store={store}> <ActionDone currentChallenge={userChallengeData[1]} /> diff --git a/src/components/Action/ActionList/ActionList.tsx b/src/components/Action/ActionList/ActionList.tsx index 13317e4ca09d3918f642ec160fe8f8566df4b91b..0b35b952d93524d2e2ed610aa5cc8f6ab820e89c 100644 --- a/src/components/Action/ActionList/ActionList.tsx +++ b/src/components/Action/ActionList/ActionList.tsx @@ -42,23 +42,23 @@ const ActionList = ({ } }, [client, isProfileTypeCompleted, fluidTypes]) - return ( - <> - {actions && ( - <div className="action-list-container"> - {actions.map(action => ( - <ActionCard - key={action.id} - action={action} - setSelectedAction={setSelectedAction} - setShowList={setShowList} - setFocus={setFocus} - /> - ))} - </div> - )} - </> - ) + if (actions) { + return ( + <div className="action-list-container"> + {actions.map(action => ( + <ActionCard + key={action.id} + action={action} + setSelectedAction={setSelectedAction} + setShowList={setShowList} + setFocus={setFocus} + /> + ))} + </div> + ) + } + + return null } export default ActionList diff --git a/src/components/Action/ActionModal/ActionModal.spec.tsx b/src/components/Action/ActionModal/ActionModal.spec.tsx index 2e8f36c8ddac42c65c619b5b89b5a9f6d234ba07..37e94eda6c1e803758c3574930f5a831d1fb9a30 100644 --- a/src/components/Action/ActionModal/ActionModal.spec.tsx +++ b/src/components/Action/ActionModal/ActionModal.spec.tsx @@ -17,7 +17,7 @@ jest.mock('services/challenge.service', () => { describe('ActionModal component', () => { const store = createMockEcolyoStore() - it('should render correctly', async () => { + it('should render correctly', () => { const { baseElement } = render( <Provider store={store}> <ActionModal diff --git a/src/components/Action/ActionView.spec.tsx b/src/components/Action/ActionView.spec.tsx index 25bd7eef03a284e261ef00572d1eab1f1d20a43e..a6d116988944097f46dbc9fcf1b80896b0fdc12e 100644 --- a/src/components/Action/ActionView.spec.tsx +++ b/src/components/Action/ActionView.spec.tsx @@ -26,7 +26,7 @@ jest.mock( ) describe('ActionView component', () => { - it('should render match snapshot with "Unstarted" state', async () => { + it('should render match snapshot with "Unstarted" state', () => { const store = createMockEcolyoStore({ challenge: { ...mockChallengeState, @@ -46,7 +46,7 @@ describe('ActionView component', () => { ) expect(container).toMatchSnapshot() }) - it('should render match snapshot with "onGoing" state', async () => { + it('should render match snapshot with "onGoing" state', () => { const store = createMockEcolyoStore({ challenge: { ...mockChallengeState, @@ -66,7 +66,7 @@ describe('ActionView component', () => { ) expect(container).toMatchSnapshot() }) - it('should render match snapshot with "Notification" state', async () => { + it('should render match snapshot with "Notification" state', () => { const store = createMockEcolyoStore({ challenge: { ...mockChallengeState, @@ -86,7 +86,7 @@ describe('ActionView component', () => { ) expect(container).toMatchSnapshot() }) - it('should render match snapshot with default case', async () => { + it('should render match snapshot with default case', () => { const store = createMockEcolyoStore({ challenge: { ...mockChallengeState, diff --git a/src/components/Analysis/MonthlyAnalysis.tsx b/src/components/Analysis/MonthlyAnalysis.tsx index e66f960076d486d9faba827e17d1ba468f118da0..dbe071720132403ca6d0197190bfca46c84b42b0 100644 --- a/src/components/Analysis/MonthlyAnalysis.tsx +++ b/src/components/Analysis/MonthlyAnalysis.tsx @@ -12,10 +12,11 @@ import Comparison from './Comparison/Comparison' import ElecHalfHourMonthlyAnalysis from './ElecHalfHourMonthlyAnalysis/ElecHalfHourMonthlyAnalysis' import IncompleteDataWarning from './IncompleteDataWarning/IncompleteDataWarning' import MaxConsumptionCard from './MaxConsumptionCard/MaxConsumptionCard' +import './monthlyanalysis.scss' +import NewsletterReminder from './NewsletterReminder/NewsletterReminder' import NoAnalysisModal from './NoKonnector/NoAnalysisModal' import ProfileComparator from './ProfileComparator/ProfileComparator' import TotalAnalysisChart from './TotalAnalysisChart/TotalAnalysisChart' -import './monthlyanalysis.scss' interface MonthlyAnalysisProps { saveLastScrollPosition: () => void @@ -28,8 +29,9 @@ const MonthlyAnalysis = ({ }: MonthlyAnalysisProps) => { const client = useClient() const { - analysis: { analysisMonth }, + analysis: { analysisMonth, haveSeenNewsletterReminder }, global: { fluidStatus }, + profile: { sendAnalysisNotification, isAnalysisReminderEnabled }, } = useAppSelector(state => state.ecolyo) const consumptionService = useMemo( @@ -61,6 +63,11 @@ const MonthlyAnalysis = ({ percentageVariation: 0, }) + const displayNewsletterReminder = + !sendAnalysisNotification && + isAnalysisReminderEnabled && + !haveSeenNewsletterReminder + useEffect(() => { let subscribed = true @@ -144,6 +151,12 @@ const MonthlyAnalysis = ({ {!isLoadingAnalysis && ( <Fade in={!isLoadingAnalysis}> <div className="analysis-root"> + {displayNewsletterReminder && ( + <div className="analysis-content"> + <NewsletterReminder /> + </div> + )} + {incompleteDataFluids.length !== 0 && ( <div className="analysis-content"> <IncompleteDataWarning diff --git a/src/components/Analysis/NewsletterReminder/NewsletterReminder.spec.tsx b/src/components/Analysis/NewsletterReminder/NewsletterReminder.spec.tsx new file mode 100644 index 0000000000000000000000000000000000000000..56381062c49a4e205397dc5075158e86b14f36e1 --- /dev/null +++ b/src/components/Analysis/NewsletterReminder/NewsletterReminder.spec.tsx @@ -0,0 +1,18 @@ +import { render } from '@testing-library/react' +import React from 'react' +import { Provider } from 'react-redux' +import { createMockEcolyoStore } from 'tests/__mocks__/store' +import NewsletterReminder from './NewsletterReminder' + +describe('NewsletterReminder', () => { + const store = createMockEcolyoStore() + + it('renders component correctly and have correct snapshot', () => { + const { container } = render( + <Provider store={store}> + <NewsletterReminder /> + </Provider> + ) + expect(container).toMatchSnapshot() + }) +}) diff --git a/src/components/Analysis/NewsletterReminder/NewsletterReminder.tsx b/src/components/Analysis/NewsletterReminder/NewsletterReminder.tsx new file mode 100644 index 0000000000000000000000000000000000000000..92d0c30dfc7bf9ba83150225198809bf0e28fa64 --- /dev/null +++ b/src/components/Analysis/NewsletterReminder/NewsletterReminder.tsx @@ -0,0 +1,58 @@ +import { Button } from '@material-ui/core' +import CloseIcon from 'assets/icons/ico/close.svg' +import StyledIconButton from 'components/CommonKit/IconButton/StyledIconButton' +import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n' +import React from 'react' +import { setHaveSeenNewsletterReminder } from 'store/analysis/analysis.slice' +import { useAppDispatch } from 'store/hooks' +import { updateProfile } from 'store/profile/profile.slice' +import './newsletterReminder.scss' + +const NewsletterReminder = () => { + const { t } = useI18n() + const dispatch = useAppDispatch() + + return ( + <div className="newsletter-reminder"> + <StyledIconButton + icon={CloseIcon} + sized={18} + onClick={() => dispatch(setHaveSeenNewsletterReminder(true))} + aria-label={t('analysis.newsletter_reminder.close')} + className="close-button" + /> + <div className="text-container"> + <h2 className="title text-20-bold"> + {t('analysis.newsletter_reminder.title')} + </h2> + <p className="text-18-normal"> + {t('analysis.newsletter_reminder.text')} + </p> + </div> + <div className="buttons"> + <Button + className="btnPrimary" + onClick={() => + dispatch(updateProfile({ sendAnalysisNotification: true })) + } + > + {t('analysis.newsletter_reminder.button')} + </Button> + <Button + classes={{ + root: 'btnText', + label: 'text-16-normal stop-show', + }} + size="small" + onClick={() => + dispatch(updateProfile({ isAnalysisReminderEnabled: false })) + } + > + {t('analysis.newsletter_reminder.stop_showing')} + </Button> + </div> + </div> + ) +} + +export default NewsletterReminder diff --git a/src/components/Analysis/NewsletterReminder/__snapshots__/NewsletterReminder.spec.tsx.snap b/src/components/Analysis/NewsletterReminder/__snapshots__/NewsletterReminder.spec.tsx.snap new file mode 100644 index 0000000000000000000000000000000000000000..bd881fec4b75b973494922ee89b9aba0b1815b8d --- /dev/null +++ b/src/components/Analysis/NewsletterReminder/__snapshots__/NewsletterReminder.spec.tsx.snap @@ -0,0 +1,80 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NewsletterReminder renders component correctly and have correct snapshot 1`] = ` +<div> + <div + class="newsletter-reminder" + > + <button + aria-label="analysis.newsletter_reminder.close" + class="MuiButtonBase-root MuiIconButton-root close-button" + tabindex="0" + type="button" + > + <span + class="MuiIconButton-label" + > + <svg + aria-hidden="true" + class="styles__icon___23x3R" + height="18" + width="18" + > + <use + xlink:href="#test-file-stub" + /> + </svg> + </span> + <span + class="MuiTouchRipple-root" + /> + </button> + <div + class="text-container" + > + <h2 + class="title text-20-bold" + > + analysis.newsletter_reminder.title + </h2> + <p + class="text-18-normal" + > + analysis.newsletter_reminder.text + </p> + </div> + <div + class="buttons" + > + <button + class="MuiButtonBase-root MuiButton-root MuiButton-text btnPrimary" + tabindex="0" + type="button" + > + <span + class="MuiButton-label" + > + analysis.newsletter_reminder.button + </span> + <span + class="MuiTouchRipple-root" + /> + </button> + <button + class="MuiButtonBase-root MuiButton-root btnText MuiButton-text MuiButton-textSizeSmall MuiButton-sizeSmall" + tabindex="0" + type="button" + > + <span + class="MuiButton-label text-16-normal stop-show" + > + analysis.newsletter_reminder.stop_showing + </span> + <span + class="MuiTouchRipple-root" + /> + </button> + </div> + </div> +</div> +`; diff --git a/src/components/Analysis/NewsletterReminder/newsletterReminder.scss b/src/components/Analysis/NewsletterReminder/newsletterReminder.scss new file mode 100644 index 0000000000000000000000000000000000000000..e2366137e672b43c8c7f07b628cb076791f25f83 --- /dev/null +++ b/src/components/Analysis/NewsletterReminder/newsletterReminder.scss @@ -0,0 +1,46 @@ +@import 'src/styles/base/color'; + +.newsletter-reminder { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + background: $grey-linear-gradient-background; + border: 1px solid $gold-shadow; + border-radius: 4px; + padding: 24px 16px 12px 16px; + gap: 16px; + + .close-button { + position: absolute; + top: 0.5rem; + right: 0.5rem; + padding: 5px 5px; + } + + .text-container { + h2 { + color: $gold-shadow; + margin: 0; + padding-inline: 1.5rem; + } + p { + color: $white; + margin: 0; + } + } + + .buttons { + display: flex; + flex-direction: column; + gap: 12px; + + button.btnPrimary { + max-width: 134px; + } + .stop-show { + color: $soft-grey; + } + } +} diff --git a/src/components/Analysis/ProfileComparator/ProfileComparator.tsx b/src/components/Analysis/ProfileComparator/ProfileComparator.tsx index 3ff398827566cb7b9534ee5d64ee515fe7107730..097182f98140b37fec2dbdff4832420a079efe12 100644 --- a/src/components/Analysis/ProfileComparator/ProfileComparator.tsx +++ b/src/components/Analysis/ProfileComparator/ProfileComparator.tsx @@ -142,7 +142,7 @@ const ProfileComparator = ({ > {t('analysis.accessibility.button_go_to_profil')} </Button> - <StyledIcon icon={PlaceHolderIcon} width="100%" height="60%" /> + <StyledIcon icon={PlaceHolderIcon} height={150} /> </div> ) diff --git a/src/components/Analysis/ProfileComparator/ProfileComparatorRow.spec.tsx b/src/components/Analysis/ProfileComparator/ProfileComparatorRow.spec.tsx index 62845a52f0cebe1be5cba0856bd46af2285133bc..18ccf5ee69556c50ba5e8ce37743a21a8f3eef66 100644 --- a/src/components/Analysis/ProfileComparator/ProfileComparatorRow.spec.tsx +++ b/src/components/Analysis/ProfileComparator/ProfileComparatorRow.spec.tsx @@ -11,7 +11,7 @@ describe('AnalysisConsumptionRow component', () => { const performanceValue = 25 describe('Multifluid row', () => { - it('should be rendered correctly for Multifluid and at least fluid connected', async () => { + it('should be rendered correctly for Multifluid and at least fluid connected', () => { const { container } = render( <ProfileComparatorRow fluidType={FluidType.MULTIFLUID} @@ -34,7 +34,7 @@ describe('AnalysisConsumptionRow component', () => { ).toBeFalsy() }) - it('should be rendered correctly for Multifluid and at none fluid connected', async () => { + it('should be rendered correctly for Multifluid and at none fluid connected', () => { const mockConnected = false const { container } = render( <ProfileComparatorRow @@ -62,7 +62,7 @@ describe('AnalysisConsumptionRow component', () => { }) describe('Single fluid row', () => { - it('should be rendered correctly for singleFluid connected for average', async () => { + it('should be rendered correctly for singleFluid connected for average', () => { const { container } = render( <ProfileComparatorRow fluidType={FluidType.ELECTRICITY} @@ -90,7 +90,7 @@ describe('AnalysisConsumptionRow component', () => { ).toBeFalsy() }) - it('should be rendered correctly for singleFluid not connected', async () => { + it('should be rendered correctly for singleFluid not connected', () => { const mockConnected = false const { container } = render( <ProfileComparatorRow @@ -119,8 +119,8 @@ describe('AnalysisConsumptionRow component', () => { ).toBeTruthy() }) - it('should be rendered correctly for singleFluid with none performance value', async () => { - const mockPerformanceValue: number | null = null + it('should be rendered correctly for singleFluid with none performance value', () => { + const mockPerformanceValue = null const { container } = render( <ProfileComparatorRow fluidType={FluidType.ELECTRICITY} @@ -146,7 +146,7 @@ describe('AnalysisConsumptionRow component', () => { ).toBeFalsy() }) - it('should be rendered correctly with unit', async () => { + it('should be rendered correctly with unit', () => { const mockForecast: MonthlyForecast = { ...mockMonthlyForecastJanuaryTestProfile1, fluidForecast: [ diff --git a/src/components/Analysis/ProfileComparator/__snapshots__/ProfileComparator.spec.tsx.snap b/src/components/Analysis/ProfileComparator/__snapshots__/ProfileComparator.spec.tsx.snap index 40897d20f9d65accdadbbc1b929458ef5a404575..bf328e562fc59ee28024e3c48c951869eb4cbc26 100644 --- a/src/components/Analysis/ProfileComparator/__snapshots__/ProfileComparator.spec.tsx.snap +++ b/src/components/Analysis/ProfileComparator/__snapshots__/ProfileComparator.spec.tsx.snap @@ -49,8 +49,8 @@ exports[`AnalysisConsumption component should be rendered correctly with profile <svg aria-hidden="true" class="styles__icon___23x3R" - height="60%" - width="100%" + height="150" + width="16" > <use xlink:href="#test-file-stub" diff --git a/src/components/Analysis/ProfileComparator/profileComparator.scss b/src/components/Analysis/ProfileComparator/profileComparator.scss index 266ca39cea142d2df12dacd3c4dca7b43c1cdee4..3b5d0afcdcc8d31e2303fd22013925b4a4f2a7e9 100644 --- a/src/components/Analysis/ProfileComparator/profileComparator.scss +++ b/src/components/Analysis/ProfileComparator/profileComparator.scss @@ -65,4 +65,8 @@ button { max-width: $width-small-phone; } + + svg { + width: 100%; + } } diff --git a/src/components/Analysis/TotalAnalysisChart/TotalAnalysisChart.tsx b/src/components/Analysis/TotalAnalysisChart/TotalAnalysisChart.tsx index 96246cd52cda2c750dca475dfabd2e01eabcc126..fc276de1dbf7c487c08bb227427a990987784814 100644 --- a/src/components/Analysis/TotalAnalysisChart/TotalAnalysisChart.tsx +++ b/src/components/Analysis/TotalAnalysisChart/TotalAnalysisChart.tsx @@ -75,78 +75,76 @@ const TotalAnalysisChart = ({ ) return ( - <> - <div - className="totalAnalysis-container" - style={{ - minHeight: radius + 100, - }} - > - <div className="text-24-normal title">{t('analysis_pie.total')}</div> + <div + className="totalAnalysis-container" + style={{ + minHeight: radius + 100, + }} + > + <div className="text-24-normal title">{t('analysis_pie.total')}</div> - {isLoading && <Loader />} - {!isLoading && !dataLoadValueDetailArray && emptyPieChart()} - {!isLoading && dataLoadValueDetailArray && ( - <> - <PieChart - dataloadValueDetailArray={dataLoadValueDetailArray} - radius={radius} - innerRadius={innerRadius} - outerRadius={outerRadius} + {isLoading && <Loader />} + {!isLoading && !dataLoadValueDetailArray && emptyPieChart()} + {!isLoading && dataLoadValueDetailArray && ( + <> + <PieChart + dataloadValueDetailArray={dataLoadValueDetailArray} + radius={radius} + innerRadius={innerRadius} + outerRadius={outerRadius} + > + <div className="text-36-bold"> + {formatNumberValues(totalLoadValue)} + <span className="euro-unit">{t('FLUID.MULTIFLUID.UNIT')}</span> + </div> + <div className="text-16-normal date"> + {t('analysis_pie.month') + + getMonthNameWithPrep(analysisMonth.minus({ month: 1 }))} + </div> + <Button + className="btnText" + onClick={() => setOpenEstimationModal(true)} + > + <span + className="estimated" + dangerouslySetInnerHTML={{ + __html: t('analysis_pie.estimation'), + }} + /> + </Button> + </PieChart> + <EstimatedConsumptionModal + open={openEstimationModal} + handleCloseClick={() => setOpenEstimationModal(false)} + /> + {dataLoadValueDetailArray && fluidsWithData.length > 1 && ( + <div + role="list" + aria-label={t('analysis_pie.details')} + className="total-card-container" > - <div className="text-36-bold"> - {formatNumberValues(totalLoadValue)} - <span className="euro-unit">{t('FLUID.MULTIFLUID.UNIT')}</span> - </div> - <div className="text-16-normal date"> - {t('analysis_pie.month') + - getMonthNameWithPrep(analysisMonth.minus({ month: 1 }))} - </div> - <Button - className="btnText" - onClick={() => setOpenEstimationModal(true)} - > - <span - className="estimated" - dangerouslySetInnerHTML={{ - __html: t('analysis_pie.estimation'), - }} - /> - </Button> - </PieChart> - <EstimatedConsumptionModal - open={openEstimationModal} - handleCloseClick={() => setOpenEstimationModal(false)} - /> - {dataLoadValueDetailArray && fluidsWithData.length > 1 && ( - <div - role="list" - aria-label={t('analysis_pie.details')} - className="total-card-container" - > - {dataLoadValueDetailArray.map((dataload, index) => ( - <div key={index} role="listitem" className="total-card"> - <div className="text-18-bold fluidconso"> - {dataload.value !== -1 - ? `${formatNumberValues(dataload.value)} €` - : '--- €'} - </div> - <StyledIcon - className="euro-fluid-icon" - icon={getNavPicto(index, true, true)} - size={38} - /> - <div className="text-16-normal"> - {t(`FLUID.${FluidType[index]}.LABEL`)} - </div> + {dataLoadValueDetailArray.map((dataload, index) => ( + <div key={index} role="listitem" className="total-card"> + <div className="text-18-bold fluidconso"> + {dataload.value !== -1 + ? `${formatNumberValues(dataload.value)} €` + : '--- €'} </div> - ))} - </div> - )} - </> - )} - </div> - </> + <StyledIcon + className="euro-fluid-icon" + icon={getNavPicto(index, true, true)} + size={38} + /> + <div className="text-16-normal"> + {t(`FLUID.${FluidType[index]}.LABEL`)} + </div> + </div> + ))} + </div> + )} + </> + )} + </div> ) } diff --git a/src/components/App.tsx b/src/components/App.tsx index 21b8c485d12854d1abdc07d48a2ea7aa782aaec2..b017acb9fc731913a3cf09494e435ef98d0b0c94 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -42,18 +42,20 @@ export const App = ({ tracker }: AppProps) => { <> <SkipLink /> <Layout> - <SplashRoot> - {termsStatus.accepted && ( - <> - <WelcomeModal open={!onboarding.isWelcomeSeen} /> - <Navbar /> - </> - )} - <main id="app-content" className="app-content" tabIndex={-1}> - <AppRoutes termsStatus={termsStatus} /> - </main> - </SplashRoot> - {process.env.NODE_ENV !== 'production' ? <CozyDevtools /> : null} + <> + <SplashRoot> + {termsStatus.accepted && ( + <> + <WelcomeModal open={!onboarding.isWelcomeSeen} /> + <Navbar /> + </> + )} + <main id="app-content" className="app-content" tabIndex={-1}> + <AppRoutes termsStatus={termsStatus} /> + </main> + </SplashRoot> + {process.env.NODE_ENV !== 'production' ? <CozyDevtools /> : null} + </> </Layout> </> ) diff --git a/src/components/Challenge/ChallengeCard/ChallengeCard.tsx b/src/components/Challenge/ChallengeCard/ChallengeCard.tsx index 70d193f59831201cc7d75240731a9bc15e23223e..004413f1484d92596ecb4dc5248103cff31444df 100644 --- a/src/components/Challenge/ChallengeCard/ChallengeCard.tsx +++ b/src/components/Challenge/ChallengeCard/ChallengeCard.tsx @@ -46,6 +46,7 @@ const ChallengeCard = ({ return ( <button + type="button" aria-label={t('challenge.card.goto')} onClick={() => moveToSlide(index)} className={indexSlider === index ? 'slide active' : 'slide inactive'} diff --git a/src/components/Challenge/ChallengeCard/__snapshots__/ChallengeCard.spec.tsx.snap b/src/components/Challenge/ChallengeCard/__snapshots__/ChallengeCard.spec.tsx.snap index 2500a582f326dd82eda13780f8013d711a194b2a..32fa2966106a328f1e420f8dd02226d97fd08af1 100644 --- a/src/components/Challenge/ChallengeCard/__snapshots__/ChallengeCard.spec.tsx.snap +++ b/src/components/Challenge/ChallengeCard/__snapshots__/ChallengeCard.spec.tsx.snap @@ -6,6 +6,7 @@ exports[`ChallengeCard component should be rendered correctly 1`] = ` aria-label="challenge.card.goto" class="slide active" style="min-width: 200px; max-width: 200px; min-height: 400px; background: none; padding: 0px;" + type="button" > <div class="cardContent cardDone" diff --git a/src/components/Challenge/ChallengeCardDone/ChallengeCardDone.spec.tsx b/src/components/Challenge/ChallengeCardDone/ChallengeCardDone.spec.tsx index ec7e9511042657b725442790da1133df0c10e116..f174bacde00861ab29e4f91befb7e4079148f24f 100644 --- a/src/components/Challenge/ChallengeCardDone/ChallengeCardDone.spec.tsx +++ b/src/components/Challenge/ChallengeCardDone/ChallengeCardDone.spec.tsx @@ -24,7 +24,7 @@ describe('ChallengeCardDone component', () => { challenge: { currentChallenge: null }, }, }) - it('should be rendered correctly', async () => { + it('should be rendered correctly', () => { const { container } = render( <Provider store={storeNoCurrentChallenge}> <ChallengeCardDone userChallenge={userChallengeData[0]} /> @@ -53,7 +53,7 @@ describe('ChallengeCardDone component', () => { }) expect(mockUpdateUserChallenge).toHaveBeenCalledTimes(1) }) - it('should not reset challenge if another challenge is on going', async () => { + it('should not reset challenge if another challenge is on going', () => { mockAppDispatch.mockImplementationOnce(() => mockDispatch) const store = mockStore({ ecolyo: { @@ -72,7 +72,7 @@ describe('ChallengeCardDone component', () => { expect(mockDispatch).toHaveBeenCalledTimes(0) expect(mockUpdateUserChallenge).toHaveBeenCalledTimes(0) }) - it('should be primary button is challenge is lost', async () => { + it('should be primary button is challenge is lost', () => { render( <Provider store={storeNoCurrentChallenge}> <ChallengeCardDone userChallenge={userChallengeData[1]} /> @@ -83,7 +83,7 @@ describe('ChallengeCardDone component', () => { }) expect(resetBtn).toHaveClass('btnPrimaryNegative') }) - it('should be secondary button is challenge is won', async () => { + it('should be secondary button is challenge is won', () => { render( <Provider store={storeNoCurrentChallenge}> <ChallengeCardDone userChallenge={userChallengeData[0]} /> diff --git a/src/components/Challenge/ChallengeCardDone/ChallengeCardDone.tsx b/src/components/Challenge/ChallengeCardDone/ChallengeCardDone.tsx index 95d25f0f6ad5a180b1859385e33b80f73b1ffe14..6110f52d758899b5ef695230a7eb7f90580466ca 100644 --- a/src/components/Challenge/ChallengeCardDone/ChallengeCardDone.tsx +++ b/src/components/Challenge/ChallengeCardDone/ChallengeCardDone.tsx @@ -32,7 +32,7 @@ const ChallengeCardDone = ({ const isSuccess = userChallenge.success === UserChallengeSuccess.WIN - const goDuel = async () => { + const goDuel = () => { navigate('/challenges/duel?id=' + userChallenge.id) } diff --git a/src/components/Challenge/ChallengeCardLast/ChallengeCardLast.spec.tsx b/src/components/Challenge/ChallengeCardLast/ChallengeCardLast.spec.tsx index f4825300a48bdda50d2ff32104a254aaca806a28..989c92034e5d09f8569b48831ba6376fb7f8d0c2 100644 --- a/src/components/Challenge/ChallengeCardLast/ChallengeCardLast.spec.tsx +++ b/src/components/Challenge/ChallengeCardLast/ChallengeCardLast.spec.tsx @@ -7,7 +7,7 @@ import ChallengeCardLast from './ChallengeCardLast' declare let __SAU_IDEA_DIRECT_LINK__: string describe('ChallengeCardLast component', () => { - it('should be rendered correctly', async () => { + it('should be rendered correctly', () => { const { container } = render(<ChallengeCardLast />) expect(container).toMatchSnapshot() }) diff --git a/src/components/Challenge/ChallengeCardLast/challengeCardLast.scss b/src/components/Challenge/ChallengeCardLast/challengeCardLast.scss index b4cdf2d1b932027afd3d32d79af767911ecb5f3b..9ca4dec3af5403e01836936d31fc80e0016ae748 100644 --- a/src/components/Challenge/ChallengeCardLast/challengeCardLast.scss +++ b/src/components/Challenge/ChallengeCardLast/challengeCardLast.scss @@ -13,6 +13,7 @@ flex-direction: column; align-items: center; justify-content: space-between; + flex: 1; svg { max-height: 150px; diff --git a/src/components/Challenge/ChallengeCardOnGoing/ChallengeCardOnGoing.tsx b/src/components/Challenge/ChallengeCardOnGoing/ChallengeCardOnGoing.tsx index 1c094a952788b64c16f803b22e305dbdf17c6369..aea83c8f9988c6f7207b6c50fb7ef7089efea679 100644 --- a/src/components/Challenge/ChallengeCardOnGoing/ChallengeCardOnGoing.tsx +++ b/src/components/Challenge/ChallengeCardOnGoing/ChallengeCardOnGoing.tsx @@ -101,7 +101,7 @@ const ChallengeCardOnGoing = ({ useEffect(() => { let subscribed = true - async function importIcon() { + function importIcon() { importIconById(userChallenge.id, 'challenge').then(icon => { icon ? setChallengeIcon(icon) : setChallengeIcon(defaultChallengeIcon) }) @@ -114,7 +114,7 @@ const ChallengeCardOnGoing = ({ useEffect(() => { let subscribed = true - async function setChallengeResult() { + function setChallengeResult() { const isChallengeDone = challengeService.isChallengeDone( userChallenge, currentDataload diff --git a/src/components/Challenge/ChallengeCardUnlocked/ChallengeCardUnlocked.spec.tsx b/src/components/Challenge/ChallengeCardUnlocked/ChallengeCardUnlocked.spec.tsx index d02503c690b73319c86aee791c6279877c05bf2a..99ed04a63287af471dcb82c5f20411a1bc1a5025 100644 --- a/src/components/Challenge/ChallengeCardUnlocked/ChallengeCardUnlocked.spec.tsx +++ b/src/components/Challenge/ChallengeCardUnlocked/ChallengeCardUnlocked.spec.tsx @@ -73,7 +73,7 @@ describe('ChallengeCardUnlocked component', () => { expect(mockStartUserChallenge).toHaveBeenCalledWith(userChallengeData[0]) }) - it('should not be able to launch challenge if another one is active', async () => { + it('should not be able to launch challenge if another one is active', () => { mockStartUserChallenge.mockResolvedValue(userChallengeData[0]) const store = createMockEcolyoStore({ global: mockGlobalState, diff --git a/src/components/Connection/ExpiredConsentModal/ExpiredConsentModal.spec.tsx b/src/components/Connection/ExpiredConsentModal/ExpiredConsentModal.spec.tsx index ced72db8dd3a83d8f0eaac2b3dead084f6b10684..c50602afd0333899a595edeea61b110b58e4c025 100644 --- a/src/components/Connection/ExpiredConsentModal/ExpiredConsentModal.spec.tsx +++ b/src/components/Connection/ExpiredConsentModal/ExpiredConsentModal.spec.tsx @@ -68,7 +68,7 @@ describe('ExpiredConsentModal component', () => { await act(async () => { await userEvent.click(screen.getByText('consent_outdated.yes')) }) - expect(mockAppDispatch).toHaveBeenCalledTimes(2) + expect(mockAppDispatch).toHaveBeenCalledTimes(1) expect(mockedNavigate).toHaveBeenCalledTimes(1) }) it('should click on close modal', async () => { diff --git a/src/components/Connection/ExpiredConsentModal/ExpiredConsentModal.tsx b/src/components/Connection/ExpiredConsentModal/ExpiredConsentModal.tsx index 8959ba88e9b18f3140b4072bc5456eb694895012..b01fcb166947dbfe4bcb9629077a5bc18dbae137 100644 --- a/src/components/Connection/ExpiredConsentModal/ExpiredConsentModal.tsx +++ b/src/components/Connection/ExpiredConsentModal/ExpiredConsentModal.tsx @@ -7,14 +7,10 @@ import StyledIcon from 'components/CommonKit/Icon/StyledIcon' import StyledIconButton from 'components/CommonKit/IconButton/StyledIconButton' import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n' import { FluidType } from 'enums' -import { AccountSgeData } from 'models' import React, { useCallback } from 'react' import { useNavigate } from 'react-router-dom' -import { - setShouldRefreshConsent, - updateSgeStore, -} from 'store/global/global.slice' -import { useAppDispatch, useAppSelector } from 'store/hooks' +import { setShouldRefreshConsent } from 'store/global/global.slice' +import { useAppDispatch } from 'store/hooks' import { getFluidName } from 'utils/utils' import './expiredConsentModal.scss' @@ -34,26 +30,8 @@ const ExpiredConsentModal = ({ const { t } = useI18n() const navigate = useNavigate() const dispatch = useAppDispatch() - const { fluidStatus } = useAppSelector(state => state.ecolyo.global) const launchUpdateConsent = useCallback(() => { if (fluidType === FluidType.ELECTRICITY) { - const accountData = fluidStatus[FluidType.ELECTRICITY].connection.account - ?.auth as AccountSgeData - // store the previous account data since the onDelete will remove account from DB - dispatch( - updateSgeStore({ - currentStep: 0, - firstName: accountData.firstname, - lastName: accountData.lastname, - pdl: parseInt(accountData.pointId), - address: accountData.address, - zipCode: parseInt(accountData.postalCode), - city: accountData.city, - dataConsent: true, - pdlConfirm: true, - shouldLaunchAccount: true, - }) - ) dispatch(setShouldRefreshConsent(true)) toggleModal() navigate(`/consumption/${FluidType[fluidType].toLocaleLowerCase()}`) @@ -62,7 +40,7 @@ const ExpiredConsentModal = ({ toggleModal() navigate(`/connect/${FluidType[fluidType].toLocaleLowerCase()}`) } - }, [dispatch, fluidStatus, fluidType, navigate, toggleModal]) + }, [dispatch, fluidType, navigate, toggleModal]) return ( <Dialog diff --git a/src/components/Connection/GRDFConnect/GrdfConnectView.tsx b/src/components/Connection/GRDFConnect/GrdfConnectView.tsx index 9661de4e119ec9755beda38b9197897849c2b1c8..814ca90071e866715b1af485de8572aa0bc60c5a 100644 --- a/src/components/Connection/GRDFConnect/GrdfConnectView.tsx +++ b/src/components/Connection/GRDFConnect/GrdfConnectView.tsx @@ -5,12 +5,15 @@ import CozyBar from 'components/Header/CozyBar' import Header from 'components/Header/Header' import useKonnectorAuth from 'components/Hooks/useKonnectorAuth' import useUserInstanceSettings from 'components/Hooks/useUserInstanceSettings' +import { useClient } from 'cozy-client' +import { FORM_DOCTYPE } from 'doctypes' import { FluidType } from 'enums' import { AccountGRDFData } from 'models' import React, { useCallback, useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { useAppSelector } from 'store/hooks' import '../connection.scss' +import { createInitialGrdfState, useFormData } from '../useForm' import StepConsent from './StepConsent' import { StepIdentity } from './StepIdentity' @@ -18,11 +21,14 @@ export enum GrdfStep { Identity, Consent, } + /** * http://ecolyo.cozy.tools:8080/#/connect/gas */ export const GrdfConnectView = () => { + const client = useClient() const navigate = useNavigate() + const { formData } = useFormData() const { instanceSettings } = useUserInstanceSettings() const { fluidStatus } = useAppSelector(state => state.ecolyo.global) const currentFluidStatus = fluidStatus[FluidType.GAS] @@ -30,13 +36,9 @@ export const GrdfConnectView = () => { const [launchConnection, setLaunchConnection] = useState(false) const [currentStep, setCurrentStep] = useState<GrdfStep>(GrdfStep.Identity) - const [formData, setFormData] = useState<AccountGRDFData>({ - lastname: '', - firstname: '', - email: '', - postalCode: '', - pce: '', - }) + const [grdfState, setGrdfState] = useState<AccountGRDFData>( + createInitialGrdfState() + ) const [formConsent, setFormConsent] = useState({ dataConsent: false, pceConfirm: false, @@ -48,23 +50,27 @@ export const GrdfConnectView = () => { } const [connect, update] = useKonnectorAuth(FluidType.GAS, { - grdfAuthData: formData, + grdfAuthData: grdfState, }) useEffect(() => { - setFormData(prev => ({ ...prev, email: instanceSettings.email ?? '' })) - }, [instanceSettings]) + setGrdfState(prev => ({ + ...prev, + ...createInitialGrdfState(formData), + email: instanceSettings.email ?? '', + })) + }, [instanceSettings, formData]) useEffect(() => { async function launchConnect() { if (launchConnection) { + setLaunchConnection(false) if (!account) { await connect() } else { await update() } - setLaunchConnection(false) navigate('/consumption/gas') } } @@ -74,11 +80,11 @@ export const GrdfConnectView = () => { const isNextValid = useCallback(() => { if (currentStep === GrdfStep.Identity) { return ( - formData.firstname !== '' && - formData.lastname !== '' && - formData.postalCode !== '' && - formData.email.includes('@') && - formData.pce.length === 14 + grdfState.firstname !== '' && + grdfState.lastname !== '' && + grdfState.postalCode !== '' && + grdfState.email.includes('@') && + grdfState.pce.length === 14 ) } else if (currentStep === GrdfStep.Consent) { return formConsent.dataConsent && formConsent.pceConfirm @@ -88,23 +94,40 @@ export const GrdfConnectView = () => { currentStep, formConsent.dataConsent, formConsent.pceConfirm, - formData.email, - formData.firstname, - formData.lastname, - formData.pce, - formData.postalCode, + grdfState.email, + grdfState.firstname, + grdfState.lastname, + grdfState.pce, + grdfState.postalCode, ]) const handleNext = useCallback(() => { if (!isNextValid()) return if (currentStep < GrdfStep.Consent) { setCurrentStep(prev => prev + 1) + client.save({ + ...formData, + _type: FORM_DOCTYPE, + firstName: grdfState.firstname, + lastName: grdfState.lastname, + pce: grdfState.pce, + zipCode: grdfState.postalCode, + }) } if (currentStep === GrdfStep.Consent) { setLaunchConnection(true) } focusMainContent() - }, [currentStep, isNextValid]) + }, [ + client, + currentStep, + formData, + grdfState.firstname, + grdfState.lastname, + grdfState.pce, + grdfState.postalCode, + isNextValid, + ]) const handlePrev = () => { setCurrentStep(prev => prev - 1) @@ -113,7 +136,7 @@ export const GrdfConnectView = () => { const renderStep = (step: GrdfStep) => { if (step === GrdfStep.Identity) { - return <StepIdentity formData={formData} setFormData={setFormData} /> + return <StepIdentity formData={grdfState} setFormData={setGrdfState} /> } else { return ( <StepConsent diff --git a/src/components/Connection/GRDFConnect/GrdfWaitConsent.tsx b/src/components/Connection/GRDFConnect/GrdfWaitConsent.tsx index 185dd4bdbdb8ea6d456972a8a0106ed14c6795f0..fd12330c01ad87beaed3e3b1bc43fd42a21cd073 100644 --- a/src/components/Connection/GRDFConnect/GrdfWaitConsent.tsx +++ b/src/components/Connection/GRDFConnect/GrdfWaitConsent.tsx @@ -17,7 +17,7 @@ export const GrdfWaitConsent = () => { const authData = currentFluidStatus.connection.account ?.auth as AccountGRDFData - const updateKonnector = async () => { + const updateKonnector = () => { const updatedConnection: FluidConnection = { ...currentFluidStatus.connection, shouldLaunchKonnector: true, diff --git a/src/components/Connection/SGEConnect/SgeConnectView.spec.tsx b/src/components/Connection/SGEConnect/SgeConnectView.spec.tsx index 8cb9c8bb0363de72c8d0abfff8243b9301abce37..ad4f5fc21d0e96cdce9852cbe861331542471d78 100644 --- a/src/components/Connection/SGEConnect/SgeConnectView.spec.tsx +++ b/src/components/Connection/SGEConnect/SgeConnectView.spec.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react' +import { SgeStore } from 'models' import React from 'react' import { Provider } from 'react-redux' import { BrowserRouter } from 'react-router-dom' @@ -18,6 +19,23 @@ jest.mock('components/Hooks/useKonnectorAuth', () => jest.fn(() => [mockConnect, mockUpdate]) ) +// mock sge state with shouldLaunchAccount set to true +jest.mock('components/Connection/useForm', () => ({ + useFormData: jest.fn().mockReturnValue({ formData: {} }), + createInitialSgeState: jest.fn<SgeStore, []>().mockReturnValue({ + address: '', + lastName: '', + firstName: '', + pdl: 0, + zipCode: 0, + city: '', + currentStep: 0, + dataConsent: false, + shouldLaunchAccount: true, + pdlConfirm: false, + }), +})) + describe('SgeConnectView component', () => { beforeEach(() => { jest.clearAllMocks() @@ -33,7 +51,7 @@ describe('SgeConnectView component', () => { expect(container).toMatchSnapshot() }) - it('should be on stepIdentity by default with button disabled', async () => { + it('should be on stepIdentity by default with button disabled', () => { render( <Provider store={store}> <BrowserRouter> @@ -56,17 +74,7 @@ describe('SgeConnectView component', () => { }) describe('should test methods from useKonnectorAuth hook', () => { - it('should launch account and trigger creation process', async () => { - const store = createMockEcolyoStore({ - global: { - ...mockGlobalState, - sgeConnect: { - ...mockGlobalState.sgeConnect, - shouldLaunchAccount: true, - }, - }, - }) - + it('should launch account and trigger creation process', () => { render( <Provider store={store}> <SgeConnectView /> @@ -74,15 +82,11 @@ describe('SgeConnectView component', () => { ) expect(mockConnect).toHaveBeenCalled() }) - it('should launch existing account update process', async () => { + it('should launch existing account update process', () => { const store = createMockEcolyoStore({ global: { ...mockGlobalState, fluidStatus: [SgeStatusWithAccount], - sgeConnect: { - ...mockGlobalState.sgeConnect, - shouldLaunchAccount: true, - }, }, }) render( diff --git a/src/components/Connection/SGEConnect/SgeConnectView.tsx b/src/components/Connection/SGEConnect/SgeConnectView.tsx index 8f0a00d20742337821624c9af82a24cdbb72fe19..0f179a07b9acc17e88d3189b1feaf96df8c630b8 100644 --- a/src/components/Connection/SGEConnect/SgeConnectView.tsx +++ b/src/components/Connection/SGEConnect/SgeConnectView.tsx @@ -4,16 +4,16 @@ import Content from 'components/Content/Content' import CozyBar from 'components/Header/CozyBar' import Header from 'components/Header/Header' import useKonnectorAuth from 'components/Hooks/useKonnectorAuth' +import { useClient } from 'cozy-client' +import { FORM_DOCTYPE } from 'doctypes' import { FluidType, SgeStep } from 'enums' import { SgeStore } from 'models' import React, { useCallback, useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' -import { - setShouldRefreshConsent, - updateSgeStore, -} from 'store/global/global.slice' +import { setShouldRefreshConsent } from 'store/global/global.slice' import { useAppDispatch, useAppSelector } from 'store/hooks' import '../connection.scss' +import { createInitialSgeState, useFormData } from '../useForm' import StepAddress from './StepAddress' import StepConsent from './StepConsent' import StepIdentityAndPdl from './StepIdentityAndPdl' @@ -32,11 +32,12 @@ export type SGEKeysForm = * http://ecolyo.cozy.tools:8080/#/connect/electricity */ const SgeConnectView = () => { + const client = useClient() const navigate = useNavigate() const dispatch = useAppDispatch() - const { sgeConnect } = useAppSelector(state => state.ecolyo.global) + const { formData } = useFormData() const [isLoading, setIsLoading] = useState(false) - const [currentSgeState, setCurrentSgeState] = useState<SgeStore>(sgeConnect) + const [sgeState, setSgeState] = useState<SgeStore>(createInitialSgeState()) const [currentStep, setCurrentStep] = useState<SgeStep>( SgeStep.IdentityAndPDL ) @@ -50,13 +51,25 @@ const SgeConnectView = () => { } const [connect, update] = useKonnectorAuth(FluidType.ELECTRICITY, { - sgeAuthData: sgeConnect, + sgeAuthData: sgeState, }) + useEffect( + function applyFormData() { + if (formData) { + setSgeState(prevState => ({ + ...prevState, + ...createInitialSgeState(formData), + })) + } + }, + [formData] + ) + useEffect(() => { async function launchConnect() { - if (sgeConnect.shouldLaunchAccount) { - dispatch(updateSgeStore({ ...sgeConnect, shouldLaunchAccount: false })) + if (sgeState.shouldLaunchAccount) { + setSgeState({ ...sgeState, shouldLaunchAccount: false }) dispatch(setShouldRefreshConsent(false)) if (!account) { await connect() @@ -67,66 +80,74 @@ const SgeConnectView = () => { } } launchConnect() - }, [account, connect, dispatch, navigate, sgeConnect, update]) + }, [account, connect, sgeState, dispatch, navigate, update]) const isNextValid = useCallback(() => { switch (currentStep) { case SgeStep.IdentityAndPDL: return ( - currentSgeState.firstName !== '' && - currentSgeState.lastName !== '' && - currentSgeState.pdl !== null && - currentSgeState.pdl.toString().length === 14 + sgeState.firstName !== '' && + sgeState.lastName !== '' && + sgeState.pdl !== null && + sgeState.pdl.toString().length === 14 ) case SgeStep.Address: return ( - currentSgeState.address !== '' && - currentSgeState.city !== '' && - currentSgeState.zipCode !== null && - currentSgeState.zipCode.toString().length === 5 + sgeState.address !== '' && + sgeState.city !== '' && + sgeState.zipCode !== null && + sgeState.zipCode.toString().length === 5 ) case SgeStep.Consent: - return currentSgeState.dataConsent && currentSgeState.pdlConfirm + return sgeState.dataConsent && sgeState.pdlConfirm default: return false } }, [ - currentSgeState.address, - currentSgeState.city, - currentSgeState.dataConsent, - currentSgeState.firstName, - currentSgeState.lastName, - currentSgeState.pdl, - currentSgeState.pdlConfirm, - currentSgeState.zipCode, + sgeState.address, + sgeState.city, + sgeState.dataConsent, + sgeState.firstName, + sgeState.lastName, + sgeState.pdl, + sgeState.pdlConfirm, + sgeState.zipCode, currentStep, ]) const handleNext = useCallback(() => { if (currentStep < SgeStep.Consent) { setCurrentStep(prev => prev + 1) - dispatch(updateSgeStore(currentSgeState)) + + client.save({ + ...formData, + _type: FORM_DOCTYPE, + firstName: sgeState.firstName, + lastName: sgeState.lastName, + pdl: sgeState.pdl, + address: sgeState.address, + city: sgeState.city, + zipCode: sgeState.zipCode, + }) } if (currentStep === SgeStep.Consent && !isLoading) { setIsLoading(true) const updatedState = { - ...currentSgeState, - city: currentSgeState.city.trim(), + ...sgeState, + city: sgeState.city.trim(), shouldLaunchAccount: true, } - setCurrentSgeState(updatedState) - dispatch(updateSgeStore(updatedState)) + setSgeState(updatedState) } focusMainContent() - }, [currentStep, isLoading, dispatch, currentSgeState]) + }, [currentStep, isLoading, client, formData, sgeState]) const handlePrev = useCallback(() => { if (currentStep !== SgeStep.IdentityAndPDL) { setCurrentStep(prev => prev - 1) } - dispatch(updateSgeStore(currentSgeState)) focusMainContent() - }, [currentSgeState, currentStep, dispatch]) + }, [currentStep]) const onChange = useCallback( ( @@ -137,25 +158,23 @@ const SgeConnectView = () => { if (maxLength && value.toString().length > maxLength) return const updatedState = { - ...currentSgeState, + ...sgeState, [key]: value, } - setCurrentSgeState(updatedState) + setSgeState(updatedState) }, - [currentSgeState] + [sgeState] ) const renderStep = (step: SgeStep) => { switch (step) { case SgeStep.Address: - return <StepAddress sgeState={currentSgeState} onChange={onChange} /> + return <StepAddress sgeState={sgeState} onChange={onChange} /> case SgeStep.Consent: - return <StepConsent sgeState={currentSgeState} onChange={onChange} /> + return <StepConsent sgeState={sgeState} onChange={onChange} /> case SgeStep.IdentityAndPDL: default: - return ( - <StepIdentityAndPdl sgeState={currentSgeState} onChange={onChange} /> - ) + return <StepIdentityAndPdl sgeState={sgeState} onChange={onChange} /> } } diff --git a/src/components/Connection/SGEConnect/StepAddress.spec.tsx b/src/components/Connection/SGEConnect/StepAddress.spec.tsx index e7e1326c249c7c046f6afce0d3856bfba13ee33a..8ba9d944327c3f733a1d564b4d57d41c2e13c8fd 100644 --- a/src/components/Connection/SGEConnect/StepAddress.spec.tsx +++ b/src/components/Connection/SGEConnect/StepAddress.spec.tsx @@ -1,7 +1,7 @@ import { act, render, screen } from '@testing-library/react' import { userEvent } from '@testing-library/user-event' import React from 'react' -import { mockGlobalState } from 'tests/__mocks__/store' +import { mockSgeState } from 'tests/__mocks__/forms.mock' import StepAddress from './StepAddress' const mockHandleChange = jest.fn() @@ -9,10 +9,7 @@ const mockHandleChange = jest.fn() describe('StepAddress component', () => { it('should be rendered correctly', () => { const { container } = render( - <StepAddress - sgeState={mockGlobalState.sgeConnect} - onChange={mockHandleChange} - /> + <StepAddress sgeState={mockSgeState} onChange={mockHandleChange} /> ) expect(container).toMatchSnapshot() }) @@ -20,10 +17,7 @@ describe('StepAddress component', () => { describe('should change inputs', () => { beforeEach(() => { render( - <StepAddress - sgeState={mockGlobalState.sgeConnect} - onChange={mockHandleChange} - /> + <StepAddress sgeState={mockSgeState} onChange={mockHandleChange} /> ) }) it('should change address value', async () => { @@ -40,9 +34,9 @@ describe('StepAddress component', () => { name: 'auth.enedissgegrandlyon.zipCode', }) await act(async () => { - await userEvent.type(input, '0') + await userEvent.type(input, '1') }) - expect(mockHandleChange).toHaveBeenCalledWith('zipCode', '0', 5) + expect(mockHandleChange).toHaveBeenCalledWith('zipCode', '1', 5) }) it('should change city value', async () => { @@ -58,7 +52,7 @@ describe('StepAddress component', () => { it('should have an existing zipCode value', () => { render( <StepAddress - sgeState={{ ...mockGlobalState.sgeConnect, zipCode: 69200 }} + sgeState={{ ...mockSgeState, zipCode: 69200 }} onChange={mockHandleChange} /> ) diff --git a/src/components/Connection/SGEConnect/StepConsent.spec.tsx b/src/components/Connection/SGEConnect/StepConsent.spec.tsx index 642ebccb39727a42e96b8fc4a45b59f19bb2d2d4..72fb901500d8ab61ec2e69eb3f6ea2373605df41 100644 --- a/src/components/Connection/SGEConnect/StepConsent.spec.tsx +++ b/src/components/Connection/SGEConnect/StepConsent.spec.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react' import { userEvent } from '@testing-library/user-event' import React from 'react' -import { mockGlobalState } from 'tests/__mocks__/store' +import { mockSgeState } from 'tests/__mocks__/forms.mock' import StepConsent from './StepConsent' const mockHandleChange = jest.fn() @@ -9,21 +9,13 @@ const mockHandleChange = jest.fn() describe('StepConsent component', () => { it('should be rendered correctly', () => { const { container } = render( - <StepConsent - sgeState={mockGlobalState.sgeConnect} - onChange={mockHandleChange} - /> + <StepConsent sgeState={mockSgeState} onChange={mockHandleChange} /> ) expect(container).toMatchSnapshot() }) it('should change dataConsent and pdlConfirm value', async () => { - render( - <StepConsent - sgeState={mockGlobalState.sgeConnect} - onChange={mockHandleChange} - /> - ) + render(<StepConsent sgeState={mockSgeState} onChange={mockHandleChange} />) const consentCheckbox = screen.getByLabelText( 'auth.enedissgegrandlyon.consentCheck1' ) diff --git a/src/components/Connection/SGEConnect/StepIdentityAndPdl.spec.tsx b/src/components/Connection/SGEConnect/StepIdentityAndPdl.spec.tsx index 42846fe60b69c7bbe8b135ec64e32e4bf1b00e53..db0bb7e1d6909e0773a57d14d1a092b7840fb256 100644 --- a/src/components/Connection/SGEConnect/StepIdentityAndPdl.spec.tsx +++ b/src/components/Connection/SGEConnect/StepIdentityAndPdl.spec.tsx @@ -1,7 +1,7 @@ import { act, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import React from 'react' -import { mockGlobalState } from 'tests/__mocks__/store' +import { mockSgeState } from 'tests/__mocks__/forms.mock' import StepIdentityAndPdl from './StepIdentityAndPdl' const mockHandleChange = jest.fn() @@ -9,20 +9,14 @@ const mockHandleChange = jest.fn() describe('StepIdentityAndPdl component', () => { it('should be rendered correctly', () => { const { container } = render( - <StepIdentityAndPdl - sgeState={mockGlobalState.sgeConnect} - onChange={mockHandleChange} - /> + <StepIdentityAndPdl sgeState={mockSgeState} onChange={mockHandleChange} /> ) expect(container).toMatchSnapshot() }) it('should be able to change fields', async () => { render( - <StepIdentityAndPdl - sgeState={mockGlobalState.sgeConnect} - onChange={mockHandleChange} - /> + <StepIdentityAndPdl sgeState={mockSgeState} onChange={mockHandleChange} /> ) const firstNameInput = screen.getByRole('textbox', { name: 'auth.enedissgegrandlyon.firstName', @@ -51,10 +45,7 @@ describe('StepIdentityAndPdl component', () => { it('should open hint modal', async () => { render( - <StepIdentityAndPdl - sgeState={mockGlobalState.sgeConnect} - onChange={mockHandleChange} - /> + <StepIdentityAndPdl sgeState={mockSgeState} onChange={mockHandleChange} /> ) await act(async () => { await userEvent.click( @@ -68,7 +59,7 @@ describe('StepIdentityAndPdl component', () => { render( <StepIdentityAndPdl sgeState={{ - ...mockGlobalState.sgeConnect, + ...mockSgeState, pdl: 11111111111111, firstName: 'Zack', lastName: 'Ichan', diff --git a/src/components/Connection/SGEConnect/__snapshots__/StepAddress.spec.tsx.snap b/src/components/Connection/SGEConnect/__snapshots__/StepAddress.spec.tsx.snap index 1e960609d47e012e634b5b704ae56d88290a8c40..310b53b3a7798f2e8bd9e439f4f92ecece842151 100644 --- a/src/components/Connection/SGEConnect/__snapshots__/StepAddress.spec.tsx.snap +++ b/src/components/Connection/SGEConnect/__snapshots__/StepAddress.spec.tsx.snap @@ -58,8 +58,8 @@ exports[`StepAddress component should be rendered correctly 1`] = ` class="MuiFormControl-root MuiTextField-root" > <label - class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required" - data-shrink="false" + class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled Mui-required Mui-required" + data-shrink="true" for="zipCode" id="zipCode-label" > @@ -81,14 +81,14 @@ exports[`StepAddress component should be rendered correctly 1`] = ` id="zipCode" required="" type="number" - value="" + value="0" /> <fieldset aria-hidden="true" class="PrivateNotchedOutline-root-1 MuiOutlinedInput-notchedOutline" > <legend - class="PrivateNotchedOutline-legendLabelled-3" + class="PrivateNotchedOutline-legendLabelled-3 PrivateNotchedOutline-legendNotched-4" > <span> auth.enedissgegrandlyon.zipCode diff --git a/src/components/Connection/useForm.tsx b/src/components/Connection/useForm.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1bf3be72089103ee22d8a0d8ac7efc1e38370a88 --- /dev/null +++ b/src/components/Connection/useForm.tsx @@ -0,0 +1,67 @@ +import { Q, QueryDefinition, useQuery } from 'cozy-client' +import { QueryOptions } from 'cozy-client/types/types' +import { FORM_DOCTYPE } from 'doctypes' +import { SgeStep } from 'enums' +import { AccountGRDFData, SgeStore } from 'models' + +export type QueryParams = (arg?: any) => { + definition: QueryDefinition + options: QueryOptions +} + +interface FormData { + firstName: string + lastName: string + pdl: string + pce: string + zipCode: string + city: string + address: string +} + +const getFormData: QueryParams = () => ({ + definition: Q(FORM_DOCTYPE), + options: { as: 'form' }, +}) + +/** Returns the form data from the form doctype */ +export const useFormData = () => { + const { definition, options } = getFormData() + // eslint-disable-next-line prefer-const + let { data, lastError, fetchStatus } = useQuery(definition, options) + + if (data === null) { + data = [] + } + + const formData = data as [FormData] + + return { + formData: formData[0], + isFetching: fetchStatus === 'loading', + lastError, + } +} + +export const createInitialSgeState = (formData?: FormData): SgeStore => ({ + address: formData?.address ?? '', + lastName: formData?.lastName ?? '', + firstName: formData?.firstName ?? '', + pdl: formData?.pdl ? parseInt(formData.pdl) : null, + zipCode: formData?.zipCode ? parseInt(formData.zipCode) : null, + city: formData?.city ?? '', + currentStep: SgeStep.Address, + dataConsent: false, + pdlConfirm: false, + shouldLaunchAccount: false, +}) + +export const createInitialGrdfState = ( + formData?: FormData +): AccountGRDFData => ({ + lastname: formData?.lastName ?? '', + firstname: formData?.firstName ?? '', + pce: formData?.pce ?? '', + postalCode: formData?.zipCode ?? '', + email: '', +}) diff --git a/src/components/Consumption/ConsumptionDetails/ConsumptionDetails.spec.tsx b/src/components/Consumption/ConsumptionDetails/ConsumptionDetails.spec.tsx index 78b2dc508218534047f0cde124a05df356038db7..3ffa25603c8e63bbbe5fdc34d66d4fb0d5050a56 100644 --- a/src/components/Consumption/ConsumptionDetails/ConsumptionDetails.spec.tsx +++ b/src/components/Consumption/ConsumptionDetails/ConsumptionDetails.spec.tsx @@ -8,7 +8,7 @@ import ConsumptionDetails from './ConsumptionDetails' describe('ConsumptionDetails component', () => { const store = createMockEcolyoStore() - it('should be rendered correctly', async () => { + it('should be rendered correctly', () => { const { container } = render( <Provider store={store}> <ConsumptionDetails fluidType={FluidType.ELECTRICITY} /> diff --git a/src/components/Consumption/FluidButtons/FluidButton.spec.tsx b/src/components/Consumption/FluidButtons/FluidButton.spec.tsx index 09d06751951cf026faba5835fd1c3c13ad34b78f..d78072dfc3ce56f080b1b1c5edba2cd9cf1c3714 100644 --- a/src/components/Consumption/FluidButtons/FluidButton.spec.tsx +++ b/src/components/Consumption/FluidButtons/FluidButton.spec.tsx @@ -9,7 +9,7 @@ import FluidButton from './FluidButton' describe('FluidButton component', () => { const store = createMockEcolyoStore() - it('should be rendered correctly', async () => { + it('should be rendered correctly', () => { const { container } = render( <Provider store={store}> <FluidButton fluidType={FluidType.ELECTRICITY} isActive={false} /> diff --git a/src/components/Consumption/FluidButtons/FluidButton.tsx b/src/components/Consumption/FluidButtons/FluidButton.tsx index f6d849de2d72211b5663408005fdf22e2f96f41c..f6aa3af5b01b22aba90ac4d28f55f3ca37c471ec 100644 --- a/src/components/Consumption/FluidButtons/FluidButton.tsx +++ b/src/components/Consumption/FluidButtons/FluidButton.tsx @@ -42,7 +42,7 @@ const FluidButton = ({ fluidType, isActive }: FluidButtonProps) => { const iconType = getNavPicto(fluidType, isActive, isConnected()) - const goToFluid = useCallback(async () => { + const goToFluid = useCallback(() => { navigate(isMulti ? '/consumption' : `/consumption/${fluidName}`) }, [navigate, isMulti, fluidName]) diff --git a/src/components/Consumption/FluidButtons/FluidButtons.spec.tsx b/src/components/Consumption/FluidButtons/FluidButtons.spec.tsx index 111456ecb5c280068e07928dbedfe1e66b91a0bc..3a59ea931847bb7002ce5011b90495a0efd06de2 100644 --- a/src/components/Consumption/FluidButtons/FluidButtons.spec.tsx +++ b/src/components/Consumption/FluidButtons/FluidButtons.spec.tsx @@ -8,7 +8,7 @@ import FluidButtons from './FluidButtons' describe('FluidButtons component', () => { const store = createMockEcolyoStore() - it('should be rendered correctly', async () => { + it('should be rendered correctly', () => { const { container } = render( <Provider store={store}> <FluidButtons activeFluid={FluidType.ELECTRICITY} /> diff --git a/src/components/ConsumptionVisualizer/DataloadConsumptionVisualizer.spec.tsx b/src/components/ConsumptionVisualizer/DataloadConsumptionVisualizer.spec.tsx index e0afcdfbd52fa876d4cb2a597f6d8055a1021657..72f8920ca8e556d755e766cfc62b8b47869cc575 100644 --- a/src/components/ConsumptionVisualizer/DataloadConsumptionVisualizer.spec.tsx +++ b/src/components/ConsumptionVisualizer/DataloadConsumptionVisualizer.spec.tsx @@ -44,7 +44,7 @@ const dataLoadWithValueDetail: Dataload = { describe('Dataload consumption visualizer component', () => { const store = createMockEcolyoStore() - it('should render with single fluid', async () => { + it('should render with single fluid', () => { const { container } = render( <Provider store={store}> <DataloadConsumptionVisualizer @@ -120,7 +120,7 @@ describe('Dataload consumption visualizer component', () => { expect(links.length).toBe(3) }) - it('should render with no value to compare available', async () => { + it('should render with no value to compare available', () => { const store = createMockEcolyoStore({ chart: { ...mockChartState, showCompare: true }, }) @@ -138,7 +138,7 @@ describe('Dataload consumption visualizer component', () => { .item(0) expect(element).toBeInTheDocument() }) - it('should render with water comparison data', async () => { + it('should render with water comparison data', () => { const store = createMockEcolyoStore({ chart: { ...mockChartState, showCompare: true }, }) @@ -154,7 +154,7 @@ describe('Dataload consumption visualizer component', () => { const element = container.getElementsByClassName('water-compare').item(0) expect(element).toBeInTheDocument() }) - it('should render multifluid with no compare and display estimation modal', async () => { + it('should render multifluid with no compare and display estimation modal', () => { const { container } = render( <Provider store={store}> <DataloadConsumptionVisualizer @@ -169,7 +169,7 @@ describe('Dataload consumption visualizer component', () => { .item(0) expect(element).toBeInTheDocument() }) - it('should render multifluid with euro conversions', async () => { + it('should render multifluid with euro conversions', () => { jest.mock('services/converter.service', () => { return jest.fn(() => ({ LoadToEuro: jest.fn(), diff --git a/src/components/ConsumptionVisualizer/DataloadSectionValue.tsx b/src/components/ConsumptionVisualizer/DataloadSectionValue.tsx index 7cdd9669b3ab4403feb7450489a8196995077192..61edc3c75d8459af18b034e3ab04228ca84495d2 100644 --- a/src/components/ConsumptionVisualizer/DataloadSectionValue.tsx +++ b/src/components/ConsumptionVisualizer/DataloadSectionValue.tsx @@ -49,21 +49,21 @@ const DataloadSectionValue = ({ const formattedValue = formatNumberValues(dataload.value, FLUIDNAME, true) + if (Number(formattedValue) >= 1000) { + return ( + <> + {formatNumberValues(dataload.value, FLUIDNAME)} + <span className="text-18-normal"> + {t(`FLUID.${FLUIDNAME}.MEGAUNIT`)} + </span> + </> + ) + } + return ( <> - {Number(formattedValue) >= 1000 ? ( - <> - {formatNumberValues(dataload.value, FLUIDNAME)} - <span className="text-18-normal"> - {t(`FLUID.${FLUIDNAME}.MEGAUNIT`)} - </span> - </> - ) : ( - <> - {formatNumberValues(dataload.value)} - <span className="text-18-normal">{t(`FLUID.${FLUIDNAME}.UNIT`)}</span> - </> - )} + {formatNumberValues(dataload.value)} + <span className="text-18-normal">{t(`FLUID.${FLUIDNAME}.UNIT`)}</span> </> ) } diff --git a/src/components/ConsumptionVisualizer/EstimatedConsumptionModal.spec.tsx b/src/components/ConsumptionVisualizer/EstimatedConsumptionModal.spec.tsx index a362aa740509e42e6792918e7353cada49f2e520..f3eb6da60b24e005fbff339b7cc2eedac4c7edac 100644 --- a/src/components/ConsumptionVisualizer/EstimatedConsumptionModal.spec.tsx +++ b/src/components/ConsumptionVisualizer/EstimatedConsumptionModal.spec.tsx @@ -10,7 +10,7 @@ jest.mock('services/fluidsPrices.service', () => { }) describe('EstimatedConsumptionModal component', () => { - it('should render correctly', async () => { + it('should render correctly', () => { const { baseElement } = render( <EstimatedConsumptionModal open={true} handleCloseClick={jest.fn()} /> ) diff --git a/src/components/ConsumptionVisualizer/InfoDataConsumptionVisualizer.tsx b/src/components/ConsumptionVisualizer/InfoDataConsumptionVisualizer.tsx index 4b7854661d29b38742a9b9d6b1058a370c7e729c..f623087cd5e8c8599c0bb66e8b6b1aa09fef1972 100644 --- a/src/components/ConsumptionVisualizer/InfoDataConsumptionVisualizer.tsx +++ b/src/components/ConsumptionVisualizer/InfoDataConsumptionVisualizer.tsx @@ -26,7 +26,7 @@ const InfoDataConsumptionVisualizer = ({ const { moveToLatestDate } = useMoveToLatestDate(lastDataDate) if (!dataload) { - return <></> + return null } if ( @@ -79,7 +79,7 @@ const InfoDataConsumptionVisualizer = ({ ) } - return <></> + return null } export default InfoDataConsumptionVisualizer diff --git a/src/components/ConsumptionVisualizer/NoDataModal.spec.tsx b/src/components/ConsumptionVisualizer/NoDataModal.spec.tsx index c9ff69e6525aa1d2e4a0dbf18c4f52017af31b9c..87dbacbb311b85ccedc09f0a03b1c62364506cf7 100644 --- a/src/components/ConsumptionVisualizer/NoDataModal.spec.tsx +++ b/src/components/ConsumptionVisualizer/NoDataModal.spec.tsx @@ -3,7 +3,7 @@ import React from 'react' import NoDataModal from './NoDataModal' describe('NoDataModal component', () => { - it('should render correctly', async () => { + it('should render correctly', () => { const { baseElement } = render( <NoDataModal open={true} handleCloseClick={jest.fn()} /> ) diff --git a/src/components/DateNavigator/DateNavigator.spec.tsx b/src/components/DateNavigator/DateNavigator.spec.tsx index d7f34079a2df5cf59dbaedfec0d3d826af3eaac7..29f83296380f5c16c5c913ba90c6e36beba16723 100644 --- a/src/components/DateNavigator/DateNavigator.spec.tsx +++ b/src/components/DateNavigator/DateNavigator.spec.tsx @@ -12,7 +12,7 @@ const mockedDate = DateTime.local(2021, 7, 1) .startOf('day') describe('DateNavigator component', () => { - it('should be rendered correctly', async () => { + it('should be rendered correctly', () => { const { container } = render( <DateNavigator disableNext={false} @@ -53,7 +53,7 @@ describe('DateNavigator component', () => { expect(mockHandleNextDate).toHaveBeenCalledTimes(1) }) - it('should not be able to click nav buttons', async () => { + it('should not be able to click nav buttons', () => { render( <DateNavigator disableNext={true} diff --git a/src/components/DateNavigator/DateNavigator.tsx b/src/components/DateNavigator/DateNavigator.tsx index 6a4f2ce5c79d924859ff9be674443d24820ff8c9..7a936ab06ef633f3474b43650153399f538c6456 100644 --- a/src/components/DateNavigator/DateNavigator.tsx +++ b/src/components/DateNavigator/DateNavigator.tsx @@ -48,15 +48,12 @@ const DateNavigator = ({ /> <div className="date-navigator-format"> - {inlineDateDisplay ? ( - <> - {formattedDate[0] && formattedDate[1] && ( - <div className="date-navigator-format-date text-16-bold"> - {formattedDate[0]} {formattedDate[1]} - </div> - )} - </> - ) : ( + {inlineDateDisplay && formattedDate[0] && formattedDate[1] && ( + <div className="date-navigator-format-date text-16-bold"> + {formattedDate[0]} {formattedDate[1]} + </div> + )} + {!inlineDateDisplay && ( <> {formattedDate[0] && ( <div className="date-navigator-format-date text-16-bold timeRange"> diff --git a/src/components/Duel/DuelOngoing/DuelOngoing.spec.tsx b/src/components/Duel/DuelOngoing/DuelOngoing.spec.tsx index 5e94655971924fe66cf8e420851f53cec0072d66..781ef84c653535a11162acf724ba689268c81caf 100644 --- a/src/components/Duel/DuelOngoing/DuelOngoing.spec.tsx +++ b/src/components/Duel/DuelOngoing/DuelOngoing.spec.tsx @@ -21,7 +21,7 @@ jest.mock('components/Duel/DuelChart/DuelChart', () => 'mock-duelchart') describe('DuelOngoing component', () => { const store = createMockEcolyoStore() - it('should be rendered correctly', async () => { + it('should be rendered correctly', () => { mockIsChallengeDone.mockReturnValue({ isDone: false, isWin: false, diff --git a/src/components/Duel/DuelResultModal/DuelResultModal.spec.tsx b/src/components/Duel/DuelResultModal/DuelResultModal.spec.tsx index 199878a12050aecaccad580936213290144de33a..c73a61c4cc3e5caaa0a790da0dc238a7118ecbd9 100644 --- a/src/components/Duel/DuelResultModal/DuelResultModal.spec.tsx +++ b/src/components/Duel/DuelResultModal/DuelResultModal.spec.tsx @@ -15,7 +15,7 @@ describe('DuelResultModal component', () => { ) expect(baseElement).toMatchSnapshot() }) - it('should render a loss modal', async () => { + it('should render a loss modal', () => { render( <DuelResultModal open={true} diff --git a/src/components/Duel/DuelUnlocked/DuelUnlocked.tsx b/src/components/Duel/DuelUnlocked/DuelUnlocked.tsx index c06090bbb02bd9a96dcb8d9e953fba447051249a..b8e32347e2ca2e98acd856ee07d2ecb53ca4656f 100644 --- a/src/components/Duel/DuelUnlocked/DuelUnlocked.tsx +++ b/src/components/Duel/DuelUnlocked/DuelUnlocked.tsx @@ -51,30 +51,28 @@ const DuelUnlocked = ({ userChallenge }: { userChallenge: UserChallenge }) => { }, [userChallenge]) return ( - <> - <div className="duel-unlocked-container"> - <StyledIcon className="duel-icon" icon={duelIcon} size={219} /> - <div className="duel-description text-20-italic">{`"${description}"`}</div> - <div className="duel-title text-16-normal"> - {userChallenge.duel.title} - </div> - <div className="duel-average-info text-18-normal"> - {t('duel.average_info', { - average, - smartCount: average, - })} - </div> - <div className="button-start"> - <Button - aria-label={t('duel.accessibility.button_start_duel')} - onClick={launchDuel} - className="btnSecondary" - > - {t('duel.button_start')} - </Button> - </div> + <div className="duel-unlocked-container"> + <StyledIcon className="duel-icon" icon={duelIcon} size={219} /> + <div className="duel-description text-20-italic">{`"${description}"`}</div> + <div className="duel-title text-16-normal"> + {userChallenge.duel.title} </div> - </> + <div className="duel-average-info text-18-normal"> + {t('duel.average_info', { + average, + smartCount: average, + })} + </div> + <div className="button-start"> + <Button + aria-label={t('duel.accessibility.button_start_duel')} + onClick={launchDuel} + className="btnSecondary" + > + {t('duel.button_start')} + </Button> + </div> + </div> ) } diff --git a/src/components/Ecogesture/EcogestureList/EcogestureList.spec.tsx b/src/components/Ecogesture/EcogestureList/EcogestureList.spec.tsx index d512de1a43de39e438432c03f18509d0537eca23..1e7f223c9b4436f8eb7dd1b6c1a7e9979d048c19 100644 --- a/src/components/Ecogesture/EcogestureList/EcogestureList.spec.tsx +++ b/src/components/Ecogesture/EcogestureList/EcogestureList.spec.tsx @@ -29,7 +29,7 @@ describe('EcogesturesList component', () => { beforeAll(() => { mockAppDispatch.mockClear() }) - it('should be rendered correctly', async () => { + it('should be rendered correctly', () => { const { container } = render( <Provider store={store}> <BrowserRouter> diff --git a/src/components/Ecogesture/EcogestureNotFound/EcogestureNotFound.spec.tsx b/src/components/Ecogesture/EcogestureNotFound/EcogestureNotFound.spec.tsx index 2ca68993812bf704ba4c9caa6ca2024f649beb1b..7ae9d83388a642af0afa226befd9d44c17467d89 100644 --- a/src/components/Ecogesture/EcogestureNotFound/EcogestureNotFound.spec.tsx +++ b/src/components/Ecogesture/EcogestureNotFound/EcogestureNotFound.spec.tsx @@ -14,7 +14,7 @@ jest.mock('components/Header/CozyBar', () => 'mock-cozybar') jest.mock('components/Content/Content', () => 'mock-content') describe('EcogestureNotFound component', () => { - it('should be rendered correctly', async () => { + it('should be rendered correctly', () => { const { container } = render( <EcogestureNotFound text="test" returnPage="ecogestures" /> ) diff --git a/src/components/Ecogesture/EcogestureResetModal/EcogestureResetModal.spec.tsx b/src/components/Ecogesture/EcogestureResetModal/EcogestureResetModal.spec.tsx index f7140298cd585f51a9e7d82a45099c20d4287217..c19a04ed6a29efe0a5df3c71adfaf7212de62498 100644 --- a/src/components/Ecogesture/EcogestureResetModal/EcogestureResetModal.spec.tsx +++ b/src/components/Ecogesture/EcogestureResetModal/EcogestureResetModal.spec.tsx @@ -3,7 +3,7 @@ import React from 'react' import EcogestureResetModal from './EcogestureResetModal' describe('EcogestureResetModal component', () => { - it('should be rendered correctly', async () => { + it('should be rendered correctly', () => { const { baseElement } = render( <EcogestureResetModal open={true} diff --git a/src/components/Ecogesture/EcogestureTabsView.tsx b/src/components/Ecogesture/EcogestureTabsView.tsx index 69e2658d60b8600a2f92c48a51b1b7bf290987b3..7b1ba5cb8c9ec07d7db39e18382d885e42c97532 100644 --- a/src/components/Ecogesture/EcogestureTabsView.tsx +++ b/src/components/Ecogesture/EcogestureTabsView.tsx @@ -51,7 +51,7 @@ const EcogestureTabsView = () => { const { profile, profileEcogesture, profileType } = useAppSelector( state => state.ecolyo ) - const [tabValue, setTabValue] = useState<EcogestureTab>( + const [tabValue, setTabValue] = useState<EcogestureTab>(() => tab ? parseInt(tab) : EcogestureTab.OBJECTIVE ) const [isLoading, setIsLoading] = useState<boolean>(true) diff --git a/src/components/EcogestureForm/EcogestureFormEquipment/EcogestureFormEquipment.spec.tsx b/src/components/EcogestureForm/EcogestureFormEquipment/EcogestureFormEquipment.spec.tsx index 716342473d720cc91fc9ac1f7f7078854e54a14e..5c3111db45fee5fe8e898f9f7b8d4847276226f1 100644 --- a/src/components/EcogestureForm/EcogestureFormEquipment/EcogestureFormEquipment.spec.tsx +++ b/src/components/EcogestureForm/EcogestureFormEquipment/EcogestureFormEquipment.spec.tsx @@ -10,7 +10,7 @@ jest.mock('../EquipmentIcon/EquipmentIcon', () => 'mock-equipment-icon') describe('EcogestureFormEquipment component', () => { const store = createMockEcolyoStore() - it('should be rendered correctly', async () => { + it('should be rendered correctly', () => { const { container } = render( <Provider store={store}> <EcogestureFormEquipment @@ -40,7 +40,7 @@ describe('EcogestureFormEquipment component', () => { expect(equipments.length).toBe(Object.keys(EquipmentType).length) }) - it('should click on disabled back button', async () => { + it('should click on disabled back button', () => { const { container } = render( <Provider store={store}> <EcogestureFormEquipment diff --git a/src/components/EcogestureForm/EcogestureFormSingleChoice/EcogestureFormSingleChoice.spec.tsx b/src/components/EcogestureForm/EcogestureFormSingleChoice/EcogestureFormSingleChoice.spec.tsx index 8cd8520a132205f8173e82b6ce502490c945a1bb..a60db8c120a72cd988cd862249a154a3137b604d 100644 --- a/src/components/EcogestureForm/EcogestureFormSingleChoice/EcogestureFormSingleChoice.spec.tsx +++ b/src/components/EcogestureForm/EcogestureFormSingleChoice/EcogestureFormSingleChoice.spec.tsx @@ -19,7 +19,7 @@ const mockHandlePreviousStep = jest.fn() describe('EcogestureFormSingleChoice component', () => { const store = createMockEcolyoStore() - it('should be rendered correctly', async () => { + it('should be rendered correctly', () => { const { container } = render( <Provider store={store}> <EcogestureFormSingleChoice @@ -76,7 +76,7 @@ describe('EcogestureFormSingleChoice component', () => { }) expect(mockHandlePreviousStep).toHaveBeenCalledTimes(1) }) - it('should keep previous answer', async () => { + it('should keep previous answer', () => { render( <Provider store={store}> <EcogestureFormSingleChoice diff --git a/src/components/EcogestureForm/EcogestureFormView.spec.tsx b/src/components/EcogestureForm/EcogestureFormView.spec.tsx index 1f30811639ed24790739b49eed3625682118ead8..82ec1fe642adf423f7ab5b75fac72f5635dbd77e 100644 --- a/src/components/EcogestureForm/EcogestureFormView.spec.tsx +++ b/src/components/EcogestureForm/EcogestureFormView.spec.tsx @@ -31,7 +31,7 @@ describe('EcogestureFormView component', () => { jest.clearAllMocks() }) - it('should be rendered correctly', async () => { + it('should be rendered correctly', () => { const { container } = render( <Provider store={store}> <EcogestureFormView /> @@ -39,7 +39,7 @@ describe('EcogestureFormView component', () => { ) expect(container).toMatchSnapshot() }) - it('should render singleChoice', async () => { + it('should render singleChoice', () => { const { container } = render( <Provider store={store}> <EcogestureFormView /> @@ -49,7 +49,7 @@ describe('EcogestureFormView component', () => { container.getElementsByClassName('ecogesture-form-single').length ).toBeTruthy() }) - it('should render profiletype form step because profiletype is completed', async () => { + it('should render profiletype form step because profiletype is completed', () => { const store = createMockEcolyoStore({ profile: { ...mockProfileState, isProfileTypeCompleted: true }, profileEcogesture: mockProfileEcogesture, @@ -80,7 +80,7 @@ describe('EcogestureFormView component', () => { ).toBeTruthy() }) - it('should handle form end', async () => { + it('should handle form end', () => { mockAppDispatch.mockReturnValue(jest.fn()) jest .spyOn(React, 'useState') diff --git a/src/components/Exploration/ExplorationOngoing.tsx b/src/components/Exploration/ExplorationOngoing.tsx index b07b34a2bf577f436b41f9d242110cdcb72cd22d..ec408df24b094ec7591a5881a80fed33842fa0e0 100644 --- a/src/components/Exploration/ExplorationOngoing.tsx +++ b/src/components/Exploration/ExplorationOngoing.tsx @@ -85,26 +85,24 @@ const ExplorationOngoing = ({ userChallenge }: ExplorationOngoingProps) => { } return ( - <> - <div className="exploration-container"> - <div className="exploration-begin-container"> - <StyledIcon - className="exploration-icon" - icon={explorationIcon} - size={180} - /> - <StarsContainer - result={userChallenge.progress.explorationProgress} - isQuizBegin={true} - /> - <div className="exploration-explanation text-18-bold"> - <div>{userChallenge.exploration.description}</div> - <div>{userChallenge.exploration.complementary_description}</div> - </div> - {renderButton()} + <div className="exploration-container"> + <div className="exploration-begin-container"> + <StyledIcon + className="exploration-icon" + icon={explorationIcon} + size={180} + /> + <StarsContainer + result={userChallenge.progress.explorationProgress} + isQuizBegin={true} + /> + <div className="exploration-explanation text-18-bold"> + <div>{userChallenge.exploration.description}</div> + <div>{userChallenge.exploration.complementary_description}</div> </div> + {renderButton()} </div> - </> + </div> ) } diff --git a/src/components/FluidChart/FluidChart.tsx b/src/components/FluidChart/FluidChart.tsx index 58927624825967be1dca7a1e1463fcf2ab61aebc..50da25f22e63e16d1815f4aa8ce3d8d3040abca1 100644 --- a/src/components/FluidChart/FluidChart.tsx +++ b/src/components/FluidChart/FluidChart.tsx @@ -42,7 +42,7 @@ const FluidChart = ({ fluidType }: { fluidType: FluidType }) => { const lowercaseTimeStep = TimeStep[currentTimeStep].toLowerCase() const lowercaseFluidType = getFluidName(fluidType) - const handleChangeSwitch = async () => { + const handleChangeSwitch = () => { dispatch(setShowCompare(!showCompare)) } @@ -112,6 +112,8 @@ const FluidChart = ({ fluidType }: { fluidType: FluidType }) => { case FluidType.WATER: dispatch(setShowOfflineData(false)) break + default: + throw new Error('Unexpected fluid type') } } diff --git a/src/components/FluidChart/TimeStepSelector/TimeStepSelector.spec.tsx b/src/components/FluidChart/TimeStepSelector/TimeStepSelector.spec.tsx index 77251e88230f53ba66b4eb48a65737c1c0d120fd..ab25fa0ef054088f668adbf8f7ec414cc5e77381 100644 --- a/src/components/FluidChart/TimeStepSelector/TimeStepSelector.spec.tsx +++ b/src/components/FluidChart/TimeStepSelector/TimeStepSelector.spec.tsx @@ -17,7 +17,7 @@ describe('TimeStepSelector component', () => { jest.clearAllMocks() }) - it('should render component properly with 4 timesteps', async () => { + it('should render component properly with 4 timesteps', () => { const store = createMockEcolyoStore({ chart: { ...mockChartState, diff --git a/src/components/FluidChart/TimeStepSelector/TimeStepSelector.tsx b/src/components/FluidChart/TimeStepSelector/TimeStepSelector.tsx index 4508ebfe2e8de9a61319c5e1397c526206f3104d..2d9a103d64c2a416d3ee4fcc3b31828d587ba69d 100644 --- a/src/components/FluidChart/TimeStepSelector/TimeStepSelector.tsx +++ b/src/components/FluidChart/TimeStepSelector/TimeStepSelector.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-noninteractive-tabindex */ +/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ import { Button } from '@material-ui/core' import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n' import { FluidType, TimeStep } from 'enums' diff --git a/src/components/Hooks/useExploration.tsx b/src/components/Hooks/useExploration.tsx index df533231dde079eb02c17eace6b677f7e7cd1c17..73eef4e582a5c55bfa35ddc126c0ad730fc1b095 100644 --- a/src/components/Hooks/useExploration.tsx +++ b/src/components/Hooks/useExploration.tsx @@ -23,7 +23,7 @@ const useExploration = (): [string, Dispatch<SetStateAction<string>>] => { currentChallenge?.exploration.id === explorationID && currentChallenge?.exploration.state === UserExplorationState.ONGOING ) { - const checkExplo = async () => { + const checkExplo = () => { const explorationService = new ExplorationService(client) explorationService .checkExploration(currentChallenge, explorationID) diff --git a/src/components/Konnector/ConnectionResult/ConnectionResult.tsx b/src/components/Konnector/ConnectionResult/ConnectionResult.tsx index 99f6356d30f55c48b0893bfb0772fa09f27a73f7..03db452ffbe8995d3f6954df16efebd6e72468e9 100644 --- a/src/components/Konnector/ConnectionResult/ConnectionResult.tsx +++ b/src/components/Konnector/ConnectionResult/ConnectionResult.tsx @@ -6,7 +6,7 @@ import { useClient } from 'cozy-client' import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n' import { FluidType, KonnectorUpdate } from 'enums' import { DateTime } from 'luxon' -import { AccountSgeData, FluidConnection, FluidStatus } from 'models' +import { FluidConnection, FluidStatus } from 'models' import React, { useCallback, useEffect, useState } from 'react' import AccountService from 'services/account.service' import DateChartService from 'services/dateChart.service' @@ -14,7 +14,6 @@ import TriggerService from 'services/triggers.service' import { setShouldRefreshConsent, updateFluidConnection, - updateSgeStore, } from 'store/global/global.slice' import { useAppDispatch, useAppSelector } from 'store/hooks' import { getKonnectorUpdateError } from 'utils/utils' @@ -44,7 +43,7 @@ const ConnectionResult = ({ const [status, setStatus] = useState<string>('') const [outDatedDataDays, setOutDatedDataDays] = useState<number | null>(null) - const updateKonnector = async () => { + const updateKonnector = () => { setStatus('') setLastExecutionDate('-') setKonnectorError('') @@ -108,33 +107,11 @@ const ConnectionResult = ({ const handleRefreshConsent = useCallback(() => { if (fluidType == FluidType.ELECTRICITY) { - const accountData = currentFluidStatus.connection.account - ?.auth as AccountSgeData - // store the previous account data since the onDelete will remove account from DB - dispatch( - updateSgeStore({ - currentStep: 0, - firstName: accountData.firstname, - lastName: accountData.lastname, - pdl: parseInt(accountData.pointId), - address: accountData.address, - zipCode: parseInt(accountData.postalCode), - city: accountData.city, - dataConsent: true, - pdlConfirm: true, - shouldLaunchAccount: true, - }) - ) dispatch(setShouldRefreshConsent(true)) } else { deleteAccountsAndTriggers() } - }, [ - fluidType, - currentFluidStatus.connection.account?.auth, - dispatch, - deleteAccountsAndTriggers, - ]) + }, [fluidType, dispatch, deleteAccountsAndTriggers]) useEffect(() => { if (currentFluidStatus.connection.triggerState?.last_success) { @@ -320,18 +297,16 @@ const DisplayDataOutdated = ({ return ( <div className="connection-caption text-16-normal"> <div className="text-16-normal"> - <> - {hasUpdatedToday ? ( - // If user has already ran an update today, display a message about energy provider issue - <DisplayAlreadyUpdatedToday - fluidStatus={fluidStatus} - fluidType={fluidType} - lastExecutionDate={lastExecutionDate.toLocaleString()} - /> - ) : ( - <DisplayManualUpdate /> - )} - </> + {hasUpdatedToday ? ( + // If user has already ran an update today, display a message about energy provider issue + <DisplayAlreadyUpdatedToday + fluidStatus={fluidStatus} + fluidType={fluidType} + lastExecutionDate={lastExecutionDate.toLocaleString()} + /> + ) : ( + <DisplayManualUpdate /> + )} </div> </div> ) diff --git a/src/components/Konnector/KonnectorModal.spec.tsx b/src/components/Konnector/KonnectorModal.spec.tsx index bd2f5cacac53519318ffdbdbc6cc4952b35729b3..32d0a9b8bad35cfaba93c98984b7e483e1d38bbc 100644 --- a/src/components/Konnector/KonnectorModal.spec.tsx +++ b/src/components/Konnector/KonnectorModal.spec.tsx @@ -69,7 +69,7 @@ describe('KonnectorModal component', () => { }) expect(mockHandleCloseClick).toHaveBeenCalled() }) - it('should render login error', async () => { + it('should render login error', () => { const { baseElement } = render( <Provider store={store}> <KonnectorModal @@ -88,7 +88,7 @@ describe('KonnectorModal component', () => { baseElement.getElementsByClassName('headerError')[0] ).toBeInTheDocument() }) - it('should render unknown error', async () => { + it('should render unknown error', () => { render( <Provider store={store}> <KonnectorModal @@ -105,7 +105,7 @@ describe('KonnectorModal component', () => { ) expect(screen.getByText('konnector_modal.error_data_2')).toBeInTheDocument() }) - it('should render update error', async () => { + it('should render update error', () => { const { baseElement } = render( <Provider store={store}> <KonnectorModal diff --git a/src/components/Konnector/KonnectorModal.tsx b/src/components/Konnector/KonnectorModal.tsx index 862fe6a1b71a2d30c854763479f96a888f348d10..b11873d3d6f9eb5c94fe3574f3f39089f1c8b6ce 100644 --- a/src/components/Konnector/KonnectorModal.tsx +++ b/src/components/Konnector/KonnectorModal.tsx @@ -208,27 +208,26 @@ const KonnectorModal = ({ </div> )} {/* Show common errors for enedis */} - {fluidType === FluidType.ELECTRICITY && ( - <> - {!showCommonErrors ? ( - <Button - className="btnText" - onClick={() => setShowCommonErrors(true)} - > - {t('konnector_modal.show_common_error')} - </Button> - ) : ( - <div - className="commonErrorsList" - dangerouslySetInnerHTML={{ - __html: t( - 'konnector_modal.show_common_error_list' - ), - }} - /> - )} - </> - )} + {fluidType === FluidType.ELECTRICITY && + showCommonErrors && ( + <div + className="commonErrorsList" + dangerouslySetInnerHTML={{ + __html: t( + 'konnector_modal.show_common_error_list' + ), + }} + /> + )} + {fluidType === FluidType.ELECTRICITY && + !showCommonErrors && ( + <Button + className="btnText" + onClick={() => setShowCommonErrors(true)} + > + {t('konnector_modal.show_common_error')} + </Button> + )} </div> )} {error === KonnectorError.TERMS_VERSION_MISMATCH && diff --git a/src/components/Konnector/KonnectorViewerList.spec.tsx b/src/components/Konnector/KonnectorViewerList.spec.tsx index d1b3e7928c956e147b4925c503bd2f3710306cce..ec1693348682457abc3d37507be0785984157e98 100644 --- a/src/components/Konnector/KonnectorViewerList.spec.tsx +++ b/src/components/Konnector/KonnectorViewerList.spec.tsx @@ -14,7 +14,7 @@ jest.mock('react-router-dom', () => ({ describe('KonnectorViewerList component', () => { const store = createMockEcolyoStore() - it('should be rendered correctly', async () => { + it('should be rendered correctly', () => { const { container } = render( <Provider store={store}> <KonnectorViewerList /> diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx index 2b9a41a680d94b53c83d4e60acfbeda66abd24aa..b39445ec6c3fd1bfa706a883d776e3b2fb7cf396 100644 --- a/src/components/Loader/Loader.tsx +++ b/src/components/Loader/Loader.tsx @@ -18,16 +18,21 @@ const Loader = ({ color = 'gold', fluidType, text }: LoaderProps) => { const { t } = useI18n() let variant = color - switch (fluidType) { - case FluidType.ELECTRICITY: - variant = 'elec' - break - case FluidType.GAS: - variant = 'gaz' - break - case FluidType.WATER: - variant = 'water' - break + if (fluidType !== undefined) { + switch (fluidType) { + case FluidType.ELECTRICITY: + variant = 'elec' + break + case FluidType.GAS: + variant = 'gaz' + break + case FluidType.WATER: + variant = 'water' + break + case FluidType.MULTIFLUID: + variant = 'gold' + break + } } return ( diff --git a/src/components/Options/ExportData/Modals/exportLoadingModal.tsx b/src/components/Options/ExportData/Modals/exportLoadingModal.tsx index 3daa53c4a523cd26888830a95702506b1327607b..1f0f560992bb6d6d792ed8f8b2249627942ee36c 100644 --- a/src/components/Options/ExportData/Modals/exportLoadingModal.tsx +++ b/src/components/Options/ExportData/Modals/exportLoadingModal.tsx @@ -65,11 +65,11 @@ const ExportLoadingModal = ({ fluidType: FluidType ): Promise<ExportDataRow> => { const dataRow: ExportDataRow = {} - const fluidName = getFluidName(fluidType) + const FLUIDNAME = getFluidName(fluidType).toUpperCase() dataRow[t('export.month')] = formatTwoDigits(dataload.date.month) dataRow[t('export.year')] = dataload.date.year dataRow[ - `${t('export.consumption')} (${t('FLUID.' + fluidName + '.UNIT')})` + `${t('export.consumption')} (${t('FLUID.' + FLUIDNAME + '.UNIT')})` ] = dataload.value if (fluidType === FluidType.ELECTRICITY) { const emas = new EnedisMonthlyAnalysisDataService(client) @@ -137,6 +137,7 @@ const ExportLoadingModal = ({ useEffect(() => { let subscribed = true const date = new Date() + let timeout: ReturnType<typeof setTimeout> const exportData = async (): Promise<void> => { try { @@ -147,14 +148,15 @@ const ExportLoadingModal = ({ exportDataSheets.push(exportDataFluid) } } - await new Promise(r => setTimeout(r, 2000)) - if (subscribed) { - exportToXlsx( - exportDataSheets, - 'ecolyo_data_' + date.toLocaleDateString() - ) - handleDone() - } + timeout = setTimeout(() => { + if (subscribed) { + exportToXlsx( + exportDataSheets, + 'ecolyo_data_' + date.toLocaleDateString() + ) + handleDone() + } + }, 2000) } catch (e) { Sentry.captureException(e) handleDone(e) @@ -166,6 +168,7 @@ const ExportLoadingModal = ({ } return () => { subscribed = false + clearTimeout(timeout) } }, [getExportDataSheet, handleDone, open, selectedFluids]) diff --git a/src/components/Options/GCU/GCUContent.tsx b/src/components/Options/GCU/GCUContent.tsx index 74e8497044e1e57410fb1f6e055a93e6e219c703..d28c1813b5b6330e9bf58379bda337d192c878f6 100644 --- a/src/components/Options/GCU/GCUContent.tsx +++ b/src/components/Options/GCU/GCUContent.tsx @@ -33,7 +33,6 @@ const GCUContent = (): JSX.Element => { <p className="text-14-normal">{t('gcu.content.part3_1')}</p> <p className="text-14-normal">{t('gcu.content.part3_2')}</p> <p className="text-14-normal">{t('gcu.content.part3_3')}</p> - <p className="text-14-normal">{t('gcu.content.part3_4')}</p> <div className="gcu-content-part-title text-15-normal"> {t('gcu.content.title4')} </div> diff --git a/src/components/Options/GCU/__snapshots__/GCUContent.spec.tsx.snap b/src/components/Options/GCU/__snapshots__/GCUContent.spec.tsx.snap index dfdcf4910c46f89db3fa3bebeeada71b8e3fc2e6..aae53698025d3288e876649d6e5c290f9511ecfa 100644 --- a/src/components/Options/GCU/__snapshots__/GCUContent.spec.tsx.snap +++ b/src/components/Options/GCU/__snapshots__/GCUContent.spec.tsx.snap @@ -91,11 +91,6 @@ exports[`GCUContent component should be rendered correctly 1`] = ` > gcu.content.part3_3 </p> - <p - class="text-14-normal" - > - gcu.content.part3_4 - </p> <div class="gcu-content-part-title text-15-normal" > diff --git a/src/components/Options/GCU/__snapshots__/GCUView.spec.tsx.snap b/src/components/Options/GCU/__snapshots__/GCUView.spec.tsx.snap index 918984408f738a1e931094280581679954ed37ce..ccef8cdf4a8f02fc4a960aa2982fd905e54f8ece 100644 --- a/src/components/Options/GCU/__snapshots__/GCUView.spec.tsx.snap +++ b/src/components/Options/GCU/__snapshots__/GCUView.spec.tsx.snap @@ -100,11 +100,6 @@ exports[`GCUView component should be rendered correctly 1`] = ` > gcu.content.part3_3 </p> - <p - class="text-14-normal" - > - gcu.content.part3_4 - </p> <div class="gcu-content-part-title text-15-normal" > diff --git a/src/components/Options/LegalNotice/LegalNoticeContent.tsx b/src/components/Options/LegalNotice/LegalNoticeContent.tsx index 8239f216706e9e733c4fe262860c9f3c8b94aead..c2c804f2ae8f39e918b8a63a3bdc7bd5edf8a846 100644 --- a/src/components/Options/LegalNotice/LegalNoticeContent.tsx +++ b/src/components/Options/LegalNotice/LegalNoticeContent.tsx @@ -5,116 +5,114 @@ import './legalNoticeView.scss' const LegalNoticeContent = () => { const { t } = useI18n() return ( - <> - <div className="legal-notice-root"> - <div className="legal-notice-content"> - <p className="version">{t('legal.version')}</p> - <p dangerouslySetInnerHTML={{ __html: t('legal.site') }} /> - <p>{t('legal.adress')}</p> - <p>{t('legal.phone')}</p> - <p - className="ln-contact" - dangerouslySetInnerHTML={{ __html: t('legal.mail') }} - /> - <div className="text-16-normal"> - <div className="legal-notice-oneline"> - <span className="text-14-normal">{t('legal.p1b')}</span> - {t('legal.p1')} - </div> - <div className="legal-notice-oneline"> - <span className="text-14-normal">{t('legal.p2b')}</span> - {t('legal.p2')} - </div> - <div className="legal-notice-oneline"> - <span className="text-14-normal">{t('legal.p3b')}</span> - {t('legal.p3')} - </div> - <div className="legal-notice-oneline"> - <span className="text-14-normal">{t('legal.p4b')}</span> - {t('legal.p4')} - </div> - <div className="legal-notice-oneline"> - <span className="text-14-normal">{t('legal.p5b')}</span> - {t('legal.p5')} - </div> - <div className="legal-notice-oneline"> - <span className="text-14-normal">{t('legal.p6b')}</span> - {t('legal.p6')} - </div> - <div className="legal-notice-part"> - <h3> {t('legal.title1')}</h3> - <p>{t('legal.part1')}</p> - </div> - <div className="legal-notice-part"> - <h3>{t('legal.title2')}</h3> - <p>{t('legal.part2')}</p> - <ul> - <li>{t('legal.part2-1')}</li> - <li>{t('legal.part2-2')}</li> - <li> - {t('legal.part2-3')} - <ul> - <li>{t('legal.part2-3-1')}</li> - <li>{t('legal.part2-3-2')}</li> - <li>{t('legal.part2-3-3')}</li> - <li>{t('legal.part2-3-4')}</li> - <li - dangerouslySetInnerHTML={{ __html: t('legal.part2-3-5') }} - /> - </ul> - </li> - <li>{t('legal.part2-4')}</li> - </ul> - <p>{t('legal.part2-5')}</p> - <p>{t('legal.part2-6')}</p> - <ul> - <li>{t('legal.part2-6-1')}</li> - <li>{t('legal.part2-6-2')}</li> - <li>{t('legal.part2-6-3')}</li> - </ul> - <p dangerouslySetInnerHTML={{ __html: t('legal.part2-7') }} /> - <p>{t('legal.part2-8')}</p> - <p>{t('legal.part2-9')}</p> - <p dangerouslySetInnerHTML={{ __html: t('legal.part2-10') }} /> - <p>{t('legal.part2-11')}</p> - </div> - <div className="legal-notice-part"> - <h3>{t('legal.title3')}</h3> - <p>{t('legal.part3-1')}</p> - <p>{t('legal.part3-2')}</p> - </div> - <div className="legal-notice-part"> - <h3>{t('legal.title4')}</h3> - <p>{t('legal.part4-1')}</p> - <p>{t('legal.part4-2')}</p> - </div> - <div className="legal-notice-part"> - <h3>{t('legal.title5')}</h3> - <p>{t('legal.part5')}</p> - </div> - <div className="legal-notice-part"> - <h3>{t('legal.title6')}</h3> - <p>{t('legal.part6')}</p> - </div> - <div className="legal-notice-part"> - <h3>{t('legal.title7')}</h3> - <p>{t('legal.part7-1')}</p> - <p dangerouslySetInnerHTML={{ __html: t('legal.part7-2') }} /> - <p>{t('legal.part7-3')}</p> - </div> - <div className="legal-notice-part"> - <h3>{t('legal.title8')}</h3> - <p>{t('legal.part8')}</p> - </div> - <div className="legal-notice-part"> - <h3>{t('legal.title9')}</h3> - <p>{t('legal.part9-1')}</p> - <p>{t('legal.part9-2')}</p> - </div> + <div className="legal-notice-root"> + <div className="legal-notice-content"> + <p className="version">{t('legal.version')}</p> + <p dangerouslySetInnerHTML={{ __html: t('legal.site') }} /> + <p>{t('legal.adress')}</p> + <p>{t('legal.phone')}</p> + <p + className="ln-contact" + dangerouslySetInnerHTML={{ __html: t('legal.mail') }} + /> + <div className="text-16-normal"> + <div className="legal-notice-oneline"> + <span className="text-14-normal">{t('legal.p1b')}</span> + {t('legal.p1')} + </div> + <div className="legal-notice-oneline"> + <span className="text-14-normal">{t('legal.p2b')}</span> + {t('legal.p2')} + </div> + <div className="legal-notice-oneline"> + <span className="text-14-normal">{t('legal.p3b')}</span> + {t('legal.p3')} + </div> + <div className="legal-notice-oneline"> + <span className="text-14-normal">{t('legal.p4b')}</span> + {t('legal.p4')} + </div> + <div className="legal-notice-oneline"> + <span className="text-14-normal">{t('legal.p5b')}</span> + {t('legal.p5')} + </div> + <div className="legal-notice-oneline"> + <span className="text-14-normal">{t('legal.p6b')}</span> + {t('legal.p6')} + </div> + <div className="legal-notice-part"> + <h3> {t('legal.title1')}</h3> + <p>{t('legal.part1')}</p> + </div> + <div className="legal-notice-part"> + <h3>{t('legal.title2')}</h3> + <p>{t('legal.part2')}</p> + <ul> + <li>{t('legal.part2-1')}</li> + <li>{t('legal.part2-2')}</li> + <li> + {t('legal.part2-3')} + <ul> + <li>{t('legal.part2-3-1')}</li> + <li>{t('legal.part2-3-2')}</li> + <li>{t('legal.part2-3-3')}</li> + <li>{t('legal.part2-3-4')}</li> + <li + dangerouslySetInnerHTML={{ __html: t('legal.part2-3-5') }} + /> + </ul> + </li> + <li>{t('legal.part2-4')}</li> + </ul> + <p>{t('legal.part2-5')}</p> + <p>{t('legal.part2-6')}</p> + <ul> + <li>{t('legal.part2-6-1')}</li> + <li>{t('legal.part2-6-2')}</li> + <li>{t('legal.part2-6-3')}</li> + </ul> + <p dangerouslySetInnerHTML={{ __html: t('legal.part2-7') }} /> + <p>{t('legal.part2-8')}</p> + <p>{t('legal.part2-9')}</p> + <p dangerouslySetInnerHTML={{ __html: t('legal.part2-10') }} /> + <p>{t('legal.part2-11')}</p> + </div> + <div className="legal-notice-part"> + <h3>{t('legal.title3')}</h3> + <p>{t('legal.part3-1')}</p> + <p>{t('legal.part3-2')}</p> + </div> + <div className="legal-notice-part"> + <h3>{t('legal.title4')}</h3> + <p>{t('legal.part4-1')}</p> + <p>{t('legal.part4-2')}</p> + </div> + <div className="legal-notice-part"> + <h3>{t('legal.title5')}</h3> + <p>{t('legal.part5')}</p> + </div> + <div className="legal-notice-part"> + <h3>{t('legal.title6')}</h3> + <p>{t('legal.part6')}</p> + </div> + <div className="legal-notice-part"> + <h3>{t('legal.title7')}</h3> + <p>{t('legal.part7-1')}</p> + <p dangerouslySetInnerHTML={{ __html: t('legal.part7-2') }} /> + <p>{t('legal.part7-3')}</p> + </div> + <div className="legal-notice-part"> + <h3>{t('legal.title8')}</h3> + <p>{t('legal.part8')}</p> + </div> + <div className="legal-notice-part"> + <h3>{t('legal.title9')}</h3> + <p>{t('legal.part9-1')}</p> + <p>{t('legal.part9-2')}</p> </div> </div> </div> - </> + </div> ) } diff --git a/src/components/Options/MatomoOptOut/MatomoOptOut.tsx b/src/components/Options/MatomoOptOut/MatomoOptOut.tsx index e1a6ab75cc19abb47fc44331caaf141b080b2b6c..c787a17476fde71ce606d87dda3bdb04e489dd9b 100644 --- a/src/components/Options/MatomoOptOut/MatomoOptOut.tsx +++ b/src/components/Options/MatomoOptOut/MatomoOptOut.tsx @@ -15,6 +15,7 @@ const MatomoOptOut = () => { {t('matomo.matomo_title')} </div> <iframe + sandbox="allow-popups allow-scripts" title="opt-out" style={{ height: '250px' }} className="matomo-content" diff --git a/src/components/Options/MatomoOptOut/__snapshots__/MatomoOptOut.spec.tsx.snap b/src/components/Options/MatomoOptOut/__snapshots__/MatomoOptOut.spec.tsx.snap index f9b02a664fa3d91d7e805b657ecf85d19f39ec3e..0c8a9df6b85b5593437e86d378b766fc46b093ca 100644 --- a/src/components/Options/MatomoOptOut/__snapshots__/MatomoOptOut.spec.tsx.snap +++ b/src/components/Options/MatomoOptOut/__snapshots__/MatomoOptOut.spec.tsx.snap @@ -15,6 +15,7 @@ exports[`MatomoOptOut component should be rendered correctly 1`] = ` </div> <iframe class="matomo-content" + sandbox="allow-popups allow-scripts" src="http://localhost:9800/index.php?module=CoreAdminHome&action=optOut&language=fr&backgroundColor=121212&fontColor=e0e0e0&fontSize=&fontFamily=sans-serif" style="height: 250px;" title="opt-out" diff --git a/src/components/Options/ReportOptions/ReportOptions.spec.tsx b/src/components/Options/ReportOptions/ReportOptions.spec.tsx index 747eb7aa3384e0e9c2c486a6ee2245c4bfb9c210..ff979961be610940b17fc79bc0c10b065714caaa 100644 --- a/src/components/Options/ReportOptions/ReportOptions.spec.tsx +++ b/src/components/Options/ReportOptions/ReportOptions.spec.tsx @@ -89,7 +89,7 @@ describe('ReportOptions component', () => { }) }) - it('should render waterLimit to 100', async () => { + it('should render waterLimit to 100', () => { const storeWaterAlert = createMockEcolyoStore({ global: { ...mockGlobalState, fluidStatus: fluidStatusConnectedData }, profile: { diff --git a/src/components/Options/__snapshots__/OptionsView.spec.tsx.snap b/src/components/Options/__snapshots__/OptionsView.spec.tsx.snap index 135c90533a5d6542b27fd3bc6861a129656661a8..493580dda04352001b7b0a3620b31a1b525b66fe 100644 --- a/src/components/Options/__snapshots__/OptionsView.spec.tsx.snap +++ b/src/components/Options/__snapshots__/OptionsView.spec.tsx.snap @@ -378,6 +378,7 @@ exports[`OptionsView component should be rendered correctly 1`] = ` </div> <iframe class="matomo-content" + sandbox="allow-popups allow-scripts" src="http://localhost:9800/index.php?module=CoreAdminHome&action=optOut&language=fr&backgroundColor=121212&fontColor=e0e0e0&fontSize=&fontFamily=sans-serif" style="height: 250px;" title="opt-out" diff --git a/src/components/PartnerIssue/PartnerIssueModal.tsx b/src/components/PartnerIssue/PartnerIssueModal.tsx index 3d8540225226727f34f872c2ea8829b4931d9781..1e295b2933607f0d70748e704890e4809d7e4506 100644 --- a/src/components/PartnerIssue/PartnerIssueModal.tsx +++ b/src/components/PartnerIssue/PartnerIssueModal.tsx @@ -7,6 +7,7 @@ import StyledIconButton from 'components/CommonKit/IconButton/StyledIconButton' import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n' import { FluidType } from 'enums' import React from 'react' +import { getFluidLabel } from 'utils/utils' import './partnerIssueModal.scss' interface PartnerIssueModalProps { @@ -22,17 +23,6 @@ const PartnerIssueModal = ({ }: PartnerIssueModalProps) => { const { t } = useI18n() - const getFluidTypeLabel = () => { - switch (issuedFluid) { - case FluidType.ELECTRICITY: - return 'elec' - case FluidType.WATER: - return 'water' - case FluidType.GAS: - return 'gaz' - } - } - return ( <Dialog open={open} @@ -66,7 +56,9 @@ const PartnerIssueModal = ({ className="partner-issue-content text-16-normal" dangerouslySetInnerHTML={{ __html: t( - `consumption.partner_issue_modal.error_connect_${getFluidTypeLabel()}` + `consumption.partner_issue_modal.error_connect_${getFluidLabel( + issuedFluid + )}` ), }} /> diff --git a/src/components/ProfileType/ProfileTypeFinished/ProfileTypeFinished.spec.tsx b/src/components/ProfileType/ProfileTypeFinished/ProfileTypeFinished.spec.tsx index 129114f86fac29eaba1d227ef002015e062b966e..7c697496455ff16cdba2c8e3a05572b776a743a8 100644 --- a/src/components/ProfileType/ProfileTypeFinished/ProfileTypeFinished.spec.tsx +++ b/src/components/ProfileType/ProfileTypeFinished/ProfileTypeFinished.spec.tsx @@ -29,7 +29,7 @@ jest.mock('services/profileTypeEntity.service', () => { describe('ProfileTypeFinished component', () => { const store = createMockEcolyoStore() - it('should be rendered correctly', async () => { + it('should be rendered correctly', () => { const { container } = render( <Provider store={store}> <ProfileTypeFinished profileType={mockProfileType} /> diff --git a/src/components/ProfileType/ProfileTypeFormDateSelection/ProfileTypeFormDateSelection.tsx b/src/components/ProfileType/ProfileTypeFormDateSelection/ProfileTypeFormDateSelection.tsx index 51a7ccf531283f2b4d618b353162fadc81ee7868..20eacbf8817c589113da15c9166d469722e6971b 100644 --- a/src/components/ProfileType/ProfileTypeFormDateSelection/ProfileTypeFormDateSelection.tsx +++ b/src/components/ProfileType/ProfileTypeFormDateSelection/ProfileTypeFormDateSelection.tsx @@ -30,14 +30,14 @@ const ProfileTypeFormDateSelection = ({ }: ProfileTypeFormDateSelectionProps) => { const { t } = useI18n() const [selectedYear, setSelectedYear] = useState<number>(DateTime.now().year) - const [selectedMonth, setSelectedMonth] = useState<SelectionMonth>({ + const [selectedMonth, setSelectedMonth] = useState<SelectionMonth>(() => ({ label: DateTime.now().toLocaleString({ month: 'long' }), value: formatTwoDigits(DateTime.now().month), // Date.getMonth starts at 0 - }) + })) const buildISODate = (year: string, month: string) => DateTime.fromISO(`${year}-${month}-01`) - const [answer, setAnswer] = useState<ProfileTypeValues>( + const [answer, setAnswer] = useState<ProfileTypeValues>(() => buildISODate(selectedYear.toString(), selectedMonth.value) ) @@ -104,17 +104,17 @@ const ProfileTypeFormDateSelection = ({ setNextStep({ ...profileType, [answerType.attribute]: answer }) } - function handleSelectMonth(event: any) { + function handleSelectMonth(value: string) { setSelectedMonth({ - value: event.target.value, - label: getMonthFullName(parseInt(event.target.value)), + value: value, + label: getMonthFullName(parseInt(value)), }) - setAnswer(buildISODate(selectedYear.toString(), event.target.value)) + setAnswer(buildISODate(selectedYear.toString(), value)) } - function handleSelectYear(event: any) { - setSelectedYear(parseInt(event.target.value)) - setAnswer(buildISODate(event.target.value, selectedMonth.value)) + function handleSelectYear(value: string) { + setSelectedYear(parseInt(value)) + setAnswer(buildISODate(value, selectedMonth.value)) } /** If current year, only show past and present months else show full months */ @@ -139,7 +139,7 @@ const ProfileTypeFormDateSelection = ({ className="month" defaultValue={selectedMonth.value} value={selectedMonth.value} - onChange={e => handleSelectMonth(e)} + onChange={e => handleSelectMonth(e.target.value as string)} > {renderMonths.map(month => ( <MenuItem @@ -158,7 +158,7 @@ const ProfileTypeFormDateSelection = ({ className="year" defaultValue={selectedYear} value={selectedYear} - onChange={e => handleSelectYear(e)} + onChange={e => handleSelectYear(e.target.value as string)} > {selectYears.map(year => ( <MenuItem value={year} key={year} className="date-option"> diff --git a/src/components/ProfileType/ProfileTypeFormNumberSelection/ProfileTypeFormNumberSelection.spec.tsx b/src/components/ProfileType/ProfileTypeFormNumberSelection/ProfileTypeFormNumberSelection.spec.tsx index 0b4d386c5eb7da75cddb76107b5d482f6cbd7844..b3f119d22801b8db2a6b0ffb2e42588239d01f7c 100644 --- a/src/components/ProfileType/ProfileTypeFormNumberSelection/ProfileTypeFormNumberSelection.spec.tsx +++ b/src/components/ProfileType/ProfileTypeFormNumberSelection/ProfileTypeFormNumberSelection.spec.tsx @@ -12,7 +12,7 @@ import ProfileTypeFormNumberSelection from './ProfileTypeFormNumberSelection' describe('ProfileTypeFormNumberSelection component', () => { const store = createMockEcolyoStore() - it('should be rendered correctly', async () => { + it('should be rendered correctly', () => { const { container } = render( <Provider store={store}> <ProfileTypeFormNumberSelection diff --git a/src/components/Quiz/QuizQuestion/QuizQuestion.spec.tsx b/src/components/Quiz/QuizQuestion/QuizQuestion.spec.tsx index c2b25bde8fbdf246856f7f925deb83c96ecd31d3..93bf18196d0beb418f7972788b0070d55df3b93c 100644 --- a/src/components/Quiz/QuizQuestion/QuizQuestion.spec.tsx +++ b/src/components/Quiz/QuizQuestion/QuizQuestion.spec.tsx @@ -27,7 +27,7 @@ jest.mock( describe('QuizQuestion component', () => { const store = createMockEcolyoStore() - it('should be rendered correctly with question', async () => { + it('should be rendered correctly with question', () => { const { container } = render( <Provider store={store}> <QuizQuestion userChallenge={userChallengeData[0]} /> diff --git a/src/components/Quiz/QuizQuestion/QuizQuestion.tsx b/src/components/Quiz/QuizQuestion/QuizQuestion.tsx index c75a4404fd1d2a967d13d37159ac0e76729b821d..d38fb0d51d186537354c5c097a67d6ca2afd4b9d 100644 --- a/src/components/Quiz/QuizQuestion/QuizQuestion.tsx +++ b/src/components/Quiz/QuizQuestion/QuizQuestion.tsx @@ -47,30 +47,36 @@ const QuizQuestion = ({ userChallenge }: { userChallenge: UserChallenge }) => { } }, [client, fluidTypes, isCustomQuest, userChallenge.quiz.customQuestion]) - return ( - <div ref={mainContentRef} className="quiz-content" tabIndex={-1}> - {isCustomQuest ? ( - <> - {!customQuestion ? ( - <div className="question-loading"> - <Loader /> - </div> - ) : ( - <QuizQuestionContentCustom - userChallenge={userChallenge} - goBack={goBack} - customQuestion={customQuestion} - /> - )} - </> - ) : ( - <QuizQuestionContent + if (isCustomQuest && !customQuestion) { + return ( + <div ref={mainContentRef} className="quiz-content" tabIndex={-1}> + <div className="question-loading"> + <Loader /> + </div> + </div> + ) + } + + if (isCustomQuest && customQuestion) { + return ( + <div ref={mainContentRef} className="quiz-content" tabIndex={-1}> + <QuizQuestionContentCustom userChallenge={userChallenge} - setIsCustomQuest={setIsCustomQuest} goBack={goBack} - focusCallback={focusMainContent} + customQuestion={customQuestion} /> - )} + </div> + ) + } + + return ( + <div ref={mainContentRef} className="quiz-content" tabIndex={-1}> + <QuizQuestionContent + userChallenge={userChallenge} + setIsCustomQuest={setIsCustomQuest} + goBack={goBack} + focusCallback={focusMainContent} + /> </div> ) } diff --git a/src/components/SkipLink/SkipLink.tsx b/src/components/SkipLink/SkipLink.tsx index 1f1942d343563da5b5700a1923ed40ea9c63c4aa..a12d6807b38ae2bc6256d4584954271a9efa0a77 100644 --- a/src/components/SkipLink/SkipLink.tsx +++ b/src/components/SkipLink/SkipLink.tsx @@ -14,7 +14,7 @@ const SkipLink = () => { } return ( - <button className="skip-link" onClick={handleSkip}> + <button type="button" className="skip-link" onClick={handleSkip}> {t('common.accessibility.skip_link')} </button> ) diff --git a/src/components/Splash/SplashRoot.spec.tsx b/src/components/Splash/SplashRoot.spec.tsx index 3f57b513afae955128380a203e95c612e23b5b07..710e35610f799a1c78a8450de3e00b4812446428 100644 --- a/src/components/Splash/SplashRoot.spec.tsx +++ b/src/components/Splash/SplashRoot.spec.tsx @@ -12,7 +12,7 @@ jest.mock('@sentry/react', () => ({ describe('SplashRoot component', () => { const store = createMockEcolyoStore() - it('should be rendered correctly', async () => { + it('should be rendered correctly', () => { const { container } = render( <Provider store={store}> <SplashRoot>children</SplashRoot> diff --git a/src/components/Splash/SplashRoot.tsx b/src/components/Splash/SplashRoot.tsx index 0a4cd8b72bcd6eed32f8d10ab9843876844c627b..c3eba12ff61cd7cd2fc3e7e98d3eb2d5523b5cc8 100644 --- a/src/components/Splash/SplashRoot.tsx +++ b/src/components/Splash/SplashRoot.tsx @@ -104,7 +104,7 @@ const SplashRoot = ({ fadeTimer = 1000, children }: SplashRootProps) => { /** Process customPopup and enable it if activated */ const processCustomPopup = useCallback( - async (profile: Profile, customPopup: CustomPopup) => { + (profile: Profile, customPopup: CustomPopup) => { try { if ( today !== profile?.customPopupDate.toISO() && @@ -123,7 +123,7 @@ const SplashRoot = ({ fadeTimer = 1000, children }: SplashRootProps) => { * For each fluid, set partnersIssue to true if notification is activated and seenDate < today */ const processPartnersStatus = useCallback( - async (profile: Profile, partnersInfo: PartnersInfo) => { + (profile: Profile, partnersInfo: PartnersInfo) => { try { if ( partnersInfo.notification_activated && @@ -181,7 +181,7 @@ const SplashRoot = ({ fadeTimer = 1000, children }: SplashRootProps) => { useEffect(() => { let subscribed = true - async function loadData() { + function loadData() { const startTime = performance.now() Sentry.startSpan({ name: 'Initialize app' }, async () => { const initializationService = new InitializationService( @@ -192,7 +192,6 @@ const SplashRoot = ({ fadeTimer = 1000, children }: SplashRootProps) => { const partnersInfoService = new PartnersInfoService(client) const ms = new MigrationService(client, setInitStepErrors) try { - console.group('Initialization logs') // Run init steps in parallel setInitStep(InitSteps.PROFILE) const [ @@ -338,15 +337,15 @@ const SplashRoot = ({ fadeTimer = 1000, children }: SplashRootProps) => { /** * Load custom popup and partners info synchronously so these treatments don't block the loading */ - customPopupService.getCustomPopup().then(async customPopup => { + customPopupService.getCustomPopup().then(customPopup => { if (profile && customPopup) { - await processCustomPopup(profile, customPopup) + processCustomPopup(profile, customPopup) } }) partnersInfoService.getPartnersInfo().then(async partnersInfo => { if (profile && partnersInfo) { await processFluidsStatus(profile, partnersInfo) - await processPartnersStatus(profile, partnersInfo) + processPartnersStatus(profile, partnersInfo) } }) @@ -363,8 +362,6 @@ const SplashRoot = ({ fadeTimer = 1000, children }: SplashRootProps) => { } logApp.error(`[Initialization] Error : ${error}`) Sentry.captureException(error) - } finally { - console.groupEnd() } }) } diff --git a/src/components/Terms/DataShareConsentContent.tsx b/src/components/Terms/DataShareConsentContent.tsx index fe3d44045b16cdfbdd1ae3c4d17227077430940e..4f93b0955b8abecd581cdfc11ade2d8cb344f32c 100644 --- a/src/components/Terms/DataShareConsentContent.tsx +++ b/src/components/Terms/DataShareConsentContent.tsx @@ -15,9 +15,12 @@ const DataShareConsentContent = () => { ? t('dataShare.title1') : t('dataShare.title1Update')} </h1> - {!isFirstConnection && ( - <p className="text-14-normal">{t('dataShare.title2Update')}</p> - )} + <p className="text-14-normal"> + {isFirstConnection + ? t('dataShare.title2') + : t('dataShare.title2Update')} + </p> + <p className="text-14-normal">{t('dataShare.part1')}</p> <p className="text-14-normal">{t('dataShare.part2')}</p> <p className="text-14-normal">{t('dataShare.part3')}</p> diff --git a/src/components/Terms/TermsView.spec.tsx b/src/components/Terms/TermsView.spec.tsx index 35a4a72802616b357522dfbdc25fab9f96c7a4dd..0c9e0a9c5e324ea38e73d2918bd4f424bfc2aab5 100644 --- a/src/components/Terms/TermsView.spec.tsx +++ b/src/components/Terms/TermsView.spec.tsx @@ -3,7 +3,11 @@ import { userEvent } from '@testing-library/user-event' import React from 'react' import { Provider } from 'react-redux' import * as storeHooks from 'store/hooks' -import { createMockEcolyoStore } from 'tests/__mocks__/store' +import { + createMockEcolyoStore, + mockGlobalState, + mockProfileState, +} from 'tests/__mocks__/store' import { mockUpToDateTerm } from 'tests/__mocks__/termsData.mock' import TermsView from './TermsView' @@ -94,4 +98,43 @@ describe('TermsView component', () => { expect(mockAppDispatch).toHaveBeenCalledTimes(4) }) + + it('should not display the newsletter checkbox if already subscribed', async () => { + const storeSubscribed = createMockEcolyoStore({ + global: { + ...mockGlobalState, + termsStatus: { accepted: false, versionType: 'init' }, + }, + profile: { ...mockProfileState, sendAnalysisNotification: true }, + }) + + const { container } = render( + <Provider store={storeSubscribed}> + <TermsView /> + </Provider> + ) + await waitFor(() => null, { container }) + + const [, , boxNewsletter] = screen.getAllByRole('checkbox') + expect(boxNewsletter).toBeUndefined() + }) + + it('should display the newsletter checkbox if not subscribed', async () => { + const storeNotSubscribed = createMockEcolyoStore({ + global: { + ...mockGlobalState, + termsStatus: { accepted: false, versionType: 'init' }, + }, + profile: { ...mockProfileState, sendAnalysisNotification: false }, + }) + const { container } = render( + <Provider store={storeNotSubscribed}> + <TermsView /> + </Provider> + ) + await waitFor(() => null, { container }) + + const [, , boxNewsletter] = screen.getAllByRole('checkbox') + expect(boxNewsletter).toBeInTheDocument() + }) }) diff --git a/src/components/Terms/TermsView.tsx b/src/components/Terms/TermsView.tsx index ed963d848ddae17dfec7f0d121e7bfe56c464036..2fe23fa17f37b8dd1f728184ef9850f3a4540526 100644 --- a/src/components/Terms/TermsView.tsx +++ b/src/components/Terms/TermsView.tsx @@ -6,6 +6,7 @@ import { useNavigate } from 'react-router-dom' import TermsService from 'services/terms.service' import { updateTermsStatus } from 'store/global/global.slice' import { useAppDispatch, useAppSelector } from 'store/hooks' +import { updateProfile } from 'store/profile/profile.slice' import CGUModal from './CGUModal' import DataShareConsentContent from './DataShareConsentContent' import LegalNoticeModal from './LegalNoticeModal' @@ -17,7 +18,11 @@ const TermsView = () => { const client = useClient() const navigate = useNavigate() const dispatch = useAppDispatch() - const { termsStatus } = useAppSelector(state => state.ecolyo.global) + const { + global: { termsStatus }, + profile: { sendAnalysisNotification }, + } = useAppSelector(state => state.ecolyo) + const [acceptNewsletter, setAcceptNewsletter] = useState(false) const [GCUValidation, setGCUValidation] = useState(false) const [dataConsentValidation, setDataConsentValidation] = useState(false) const [openCGUModal, setOpenCGUModal] = useState(false) @@ -34,8 +39,12 @@ const TermsView = () => { }) ) } + + if (acceptNewsletter) { + dispatch(updateProfile({ sendAnalysisNotification: true })) + } navigate('/consumption') - }, [dispatch, client, navigate]) + }, [client, acceptNewsletter, navigate, dispatch]) return ( <div className="terms-wrapper"> @@ -85,6 +94,20 @@ const TermsView = () => { <span>{t('dataShare.validLegal2')}</span> </div> </label> + {!sendAnalysisNotification && ( + <label htmlFor="newsletter" className="inline"> + <input + id="newsletter" + type="checkbox" + className="inputCheckbox" + onChange={e => setAcceptNewsletter(e.target.checked)} + checked={acceptNewsletter} + /> + <div> + <span>{t('dataShare.acceptNewsletter')}</span> + </div> + </label> + )} </div> <div className="terms-footer"> <Button diff --git a/src/components/Terms/__snapshots__/CGUModal.spec.tsx.snap b/src/components/Terms/__snapshots__/CGUModal.spec.tsx.snap index f69973105e2cbc74de85d13dc090441c4242e447..fba795cb09444cde38151b2c62da860d749f20c1 100644 --- a/src/components/Terms/__snapshots__/CGUModal.spec.tsx.snap +++ b/src/components/Terms/__snapshots__/CGUModal.spec.tsx.snap @@ -150,11 +150,6 @@ exports[`CGUModal component should be rendered correctly 1`] = ` > gcu.content.part3_3 </p> - <p - class="text-14-normal" - > - gcu.content.part3_4 - </p> <div class="gcu-content-part-title text-15-normal" > diff --git a/src/components/Terms/__snapshots__/DataShareConsentContent.spec.tsx.snap b/src/components/Terms/__snapshots__/DataShareConsentContent.spec.tsx.snap index 69141d0536a316b3162f78c838aab7612e51e4a4..1a3d6f325acb6475bb6df19011318675a2c9a2f8 100644 --- a/src/components/Terms/__snapshots__/DataShareConsentContent.spec.tsx.snap +++ b/src/components/Terms/__snapshots__/DataShareConsentContent.spec.tsx.snap @@ -13,6 +13,11 @@ exports[`DataShareConsentContent component should be rendered correctly with fir > dataShare.title1 </h1> + <p + class="text-14-normal" + > + dataShare.title2 + </p> <p class="text-14-normal" > diff --git a/src/components/WelcomeModal/WelcomeModal.tsx b/src/components/WelcomeModal/WelcomeModal.tsx index 14a653d821ed18d7f326dbcd8880f0e3d5df9164..b1fdea23c244792b15525c0951ef1675d336ac79 100644 --- a/src/components/WelcomeModal/WelcomeModal.tsx +++ b/src/components/WelcomeModal/WelcomeModal.tsx @@ -22,7 +22,7 @@ const WelcomeModal = ({ open }: { open: boolean }) => { const dispatch = useAppDispatch() const { instanceSettings } = useUserInstanceSettings() - const setWelcomeModalViewed = useCallback(async () => { + const setWelcomeModalViewed = useCallback(() => { const mailService = new MailService() let username = '' diff --git a/src/db/profileData.ts b/src/db/profileData.ts index e6b02f585913c10d51b800de352c1a0bc86018ea..e045626a4857bb80e5da8c779f4c60c79d82ec6e 100644 --- a/src/db/profileData.ts +++ b/src/db/profileData.ts @@ -9,22 +9,23 @@ const profileData: Omit<ProfileEntity, 'id'> = { explorationHash: '', haveSeenEcogestureModal: false, haveSeenLastAnalysis: true, + isAnalysisReminderEnabled: true, isFirstConnection: true, isProfileEcogestureCompleted: false, isProfileTypeCompleted: false, lastConnectionDate: '0000-01-01T00:00:00.000Z', mailToken: '', monthlyAnalysisDate: '0000-01-01T00:00:00.000Z', + onboarding: { + isWelcomeSeen: false, + }, partnersIssueSeenDate: { enedis: '0000-01-01T00:00:00.000Z', egl: '0000-01-01T00:00:00.000Z', grdf: '0000-01-01T00:00:00.000Z', }, quizHash: '', - onboarding: { - isWelcomeSeen: false, - }, - sendAnalysisNotification: true, + sendAnalysisNotification: false, sendConsumptionAlert: false, waterDailyConsumptionLimit: 0, } diff --git a/src/db/profileTypeData.json b/src/db/profileTypeData.json index 63b94873cec67f27366b8d0095e9dd892ac01630..4722cac419bfb9fbd7476a28e8f77c9ed372f831 100644 --- a/src/db/profileTypeData.json +++ b/src/db/profileTypeData.json @@ -17,7 +17,8 @@ "hasReplacedHeater": "unknown", "warmingFluid": 0, "hotWaterFluid": 0, - "cookingFluid": 0 + "cookingFluid": 0, + "equipments": [] } } ] diff --git a/src/doctypes/com-grandlyon-ecolyo-form.ts b/src/doctypes/com-grandlyon-ecolyo-form.ts new file mode 100644 index 0000000000000000000000000000000000000000..45b9b49f6dfc98cfd379815bf9ef0e759e344899 --- /dev/null +++ b/src/doctypes/com-grandlyon-ecolyo-form.ts @@ -0,0 +1 @@ +export const FORM_DOCTYPE = 'com.grandlyon.ecolyo.form' diff --git a/src/doctypes/index.ts b/src/doctypes/index.ts index 3eded16b7eb5ed8c96f57ab34952b702f8c94a03..96ab63b223bd8b006bd039a29844cc4467659002 100644 --- a/src/doctypes/index.ts +++ b/src/doctypes/index.ts @@ -3,6 +3,7 @@ import { DUEL_DOCTYPE } from './com-grandlyon-ecolyo-duel' import { ECOGESTURE_DOCTYPE } from './com-grandlyon-ecolyo-ecogesture' import { EXPLORATION_DOCTYPE } from './com-grandlyon-ecolyo-exploration' import { FLUIDSPRICES_DOCTYPE } from './com-grandlyon-ecolyo-fluidsprices' +import { FORM_DOCTYPE } from './com-grandlyon-ecolyo-form' import { PROFILE_DOCTYPE } from './com-grandlyon-ecolyo-profile' import { PROFILEECOGESTURE_DOCTYPE } from './com-grandlyon-ecolyo-profileecogesture' import { PROFILETYPE_DOCTYPE } from './com-grandlyon-ecolyo-profiletype' @@ -182,6 +183,11 @@ const doctypes = { attributes: {}, relationships: {}, }, + form: { + doctype: FORM_DOCTYPE, + attributes: {}, + relationships: {}, + }, } export default doctypes @@ -192,6 +198,7 @@ export * from './com-grandlyon-ecolyo-duel' export * from './com-grandlyon-ecolyo-ecogesture' export * from './com-grandlyon-ecolyo-exploration' export * from './com-grandlyon-ecolyo-fluidsprices' +export * from './com-grandlyon-ecolyo-form' export * from './com-grandlyon-ecolyo-profile' export * from './com-grandlyon-ecolyo-profileecogesture' export * from './com-grandlyon-ecolyo-profiletype' diff --git a/src/locales/fr.json b/src/locales/fr.json index 7eb056210fce2394ec436182221745bda7ed6e8c..58d07d510a4a75785059d85d6fe0070a30d209f4 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -127,6 +127,13 @@ "data_info": "Données Météo France issues de la station météo Lyon Bron.", "close": "Fermer la fenêtre" } + }, + "newsletter_reminder": { + "title": "Envie d'être averti de votre dernier bilan\u00a0?", + "text": "Recevez chaque mois votre bilan ainsi que des conseils saisonniers par mail.", + "button": "Je m'inscris", + "stop_showing": "Ne plus afficher", + "close": "Fermer la fenêtre" } }, "analysis_error_modal": { @@ -241,7 +248,7 @@ "consentCheck2": "J’atteste être le titulaire du point de livraison (PCE) renseigné à l’étape précédente<span>*</span>", "waiting": { "mailSent": "Un mail vous a été envoyé...", - "mailDelay": "La réception du mail peut prendre 15min, l'envoi des mails se faisant tous les 1/4 d'heure", + "mailDelay": "cela peut prendre jusqu'à 3 minutes", "validate": "Merci de valider l'autorisation d'accès à vos données", "comeback": "Une fois ce clic effectué, revenez ici pour accéder à vos données", "button_done": "C’est fait !" @@ -697,16 +704,17 @@ "dataShare": { "title1": "Bienvenue !", "title1Update": "Ecolyo évolue !", + "title2": "Vous accédez pour la première fois à Ecolyo. Pour cela, nul besoin de vous créer un compte. ", "title2Update": "Pour continuer à utiliser Ecolyo, merci d’accepter les modalités de traitement des données ci-dessous.", - "part1": "Nous avons besoin de l’adresse email utilisée lors de la création de votre Cozy Métropole de Lyon, ci-après nommé cloud personnel.", + "part1": "Pour le bon fonctionnement du service, nous avons besoin de l’adresse email utilisée lors de la création de votre Cozy Métropole de Lyon.", "part2": "Cette donnée de compte est conservée dans Ecolyo le temps de l’utilisation de ce service.", "part3": "La Métropole de Lyon utilisera cet email afin de vous tenir informé·e\u00a0:", "item1": "En réponse à vos demandes, si vous avez pris l’initiative de nous contacter.", "item2": "En cas de problème majeur avec la gestion de votre compte.", - "item3": "De l’évolution de vos consommations, des nouveautés et de la qualité du service via une lettre mensuelle. Vous pouvez à tout moment vous désinscrire de cette lettre via la page Options du service.", + "item3": "De l’évolution de vos consommations, des nouveautés et de la qualité du service via une lettre mensuelle. Vous pouvez à tout moment vous inscrire ou désinscrire de cette lettre via la page Options du service.", "part4": "Vos données privées de consommation d’énergie et d’eau sont récupérées, sauvegardées et stockées dans votre cloud personnel à votre initiative sans visibilité de la Métropole de Lyon sur leur contenu.", - "part5": "Il en est de même pour les données privées de composition du logement et du foyer, fournies par vos soins. Elles restent également sans visibilité de la Métropole de Lyon sur leur contenu.\nLes données d’identification entrées lors de la connexion aux données de consommations d’électricité sont quant à elles conservées dans un espace sécurisé de la Métropole de Lyon et sont utilisées uniquement à des fins de contrôle du consentement par des organismes extérieurs.", - "part6": "Dans le cadre de l’évaluation et de l’amélioration du service, des données d’utilisation anonymisées et pseudonymisées seront remontées à des fins d’exploitation statistiques. La récupération de ces statistiques nous permettra de s’assurer du bon fonctionnement technique de la connexion à vos données de consommation, d’évaluer globalement l’usage de l’application via des mesures d’audience ainsi que d’évaluer à terme l’impact global en termes de baisse des consommations énergétiques de notre service.", + "part5": "Il en est de même pour les données privées de composition du logement et du foyer, fournies par vos soins. Elles restent également sans visibilité de la Métropole de Lyon sur leur contenu.\nLes données d’identification entrées lors de la connexion aux données de consommations d’électricité et de gaz sont quant à elles conservées dans un espace sécurisé de la Métropole de Lyon et sont utilisées uniquement à des fins de contrôle du consentement par des organismes extérieurs.", + "part6": "Dans le cadre de l’évaluation et de l’amélioration du service, des données d’utilisation anonymisées et pseudonymisées seront remontées à des fins d’exploitation statistiques. La récupération de ces statistiques nous permettra de garantir le bon fonctionnement technique de la connexion à vos données de consommation, d’évaluer globalement l’usage de l’application via des mesures d’audience ainsi que d’évaluer à terme l’impact global en termes de baisse des consommations énergétiques de notre service.", "part7": "Au sein de votre cloud personnel, vous pouvez à tout moment exercer vos droits d’accès, de rectification, de portabilité, de limitation et d’opposition en consultant notamment la page Options.", "part8": "Vous pouvez également exercer vos droits d’accès, de rectification, de limitation, d’opposition et d’effacement de vos données personnelles en contactant directement le Délégué à la Protection des Données par courrier en écrivant à l’adresse :", "part9": "Métropole de Lyon – Délégué à la Protection des Données - Direction des Assemblées, des Affaires Juridiques et des Assurances - 20, rue du Lac - BP 33569 - 69505 Lyon Cedex 03 ", @@ -718,6 +726,7 @@ "validLegal": " du service et ai pris connaissance des ", "validLegal_button": "Mentions Légales", "validLegal2": "de celui-ci. *", + "acceptNewsletter": "Je souhaite recevoir tous les mois un bilan de mes consommations ainsi que des conseils spécifiques.", "button_accept": "C'est parti !", "accessibility": { "button_accept": "Accepter les conditions générales d'utilisation" @@ -729,48 +738,47 @@ }, "gcu": { "title": "Conditions générales d’utilisation du service", - "version": "Version du 12.12.2022", + "version": "Version du 20.09.2024", "content": { "title1": "Ecolyo, késako\u00a0?", "part1_1": "Ecolyo est un service proposé par la Métropole de Lyon vous permettant de suivre au même endroit vos consommations d’électricité, de gaz et d’eau, en kWh, en litres, en euros et à différentes échelles de temps. Ce service vous permet également de participer à des défis individuels et vous offre une analyse de vos consommations. Des astuces vous sont aussi proposées afin de vous permettre de réduire vos consommations.", - "part1_2": "C’est un service gratuit qui prend la forme d’un site web dit « responsive », c’est-à-dire qu’il peut être consulté sur ordinateur ou sur mobile. Sur mobile, c’est une application que l’on a souhaitée ergonomique, réactive et esthétique pour votre plaisir de navigation et d’utilisation au quotidien.", + "part1_2": "C’est un service gratuit qui prend la forme d’un site web dit « responsive », c’est-à-dire qu’il peut être consulté sur ordinateur ou sur mobile. Sur mobile, c’est une application que la Métropole a souhaitée ergonomique, réactive et esthétique pour votre plaisir de navigation et d’utilisation au quotidien.", "part1_3": "Tous les termes « techniques » sont définis en bas de page.", "title2": "Quelles fonctionnalités le service propose-t-il\u00a0?", "part2_1": "La fonctionnalité principale d’Ecolyo est la visualisation, au même endroit, de vos consommations d’électricité, de gaz et d’eau et ce, à différents pas de temps (de la demi-heure – uniquement pour l’électricité, à plusieurs années, en passant par les pas de temps journaliers et mensuels). La visualisation des consommations se fait en kWh pour l’énergie et en L pour l’eau ainsi qu’en euros (euros résultant d’un prix moyenné).", - "part2_2": "Au-delà de la visualisation de vos consommations et parce que nous souhaitons vous aider à diminuer ces consommations vous retrouverez plusieurs autres pages\u00a0: ", - "part2_2_list1": "Défis : Des quiz, et actions à mettre en place vous seront proposés pour vous pousser à réduire vos consommations.", - "part2_2_list2": "Astuces : Une liste d’astuces pour maîtriser ses consommations d’énergie et d’eau à trier par usage, et avec une possibilité de les adapter plus précisément à votre profil.", - "part2_2_list3": "Analyse : Une analyse de vos consommations réelles en comparaison à celle d’un foyer étant proche d’une vôtre.", + "part2_2": "Au-delà de la visualisation de vos consommations et parce que la Métropole souhaite vous aider à diminuer ces consommations, vous retrouverez plusieurs autres pages\u00a0: ", + "part2_2_list1": "Défis : Des quiz, et actions à mettre en place vous seront proposés pour vous encourager à réduire vos consommations.", + "part2_2_list2": "Astuces : Une liste d’astuces pour maîtriser ses consommations d’énergie et d’eau à trier par usage, et avec la possibilité de les adapter plus précisément à votre profil.", + "part2_2_list3": "Analyse : Une analyse de vos consommations réelles comparées à celle d’un foyer étant proche du vôtre.", "part2_3": "Pour la page Analyse ainsi que pour la partie personnalisation des astuces, des éléments supplémentaires sur votre profil vous seront demandés. L’ensemble de ces informations récoltées à l’aide du formulaire peuvent être modifiées par la suite.", - "part2_4": "Dernière page : la page Options dans laquelle vous pourrez vous abonner à un service d’alerte sur vos consommations d’eau, de quoi ajuster votre profil ou encore vous désabonner de la lettre mensuelle.", + "part2_4": "La dernière page est la page Options dans laquelle vous pourrez vous abonner à un service d’alerte sur vos consommations d’eau, ajuster votre profil ou encore vous abonner ou désabonner à la lettre mensuelle.", "title3": "Comment ai-je accès à mes données d’électricité, de gaz et d’eau\u00a0?", "part3_1": "Pour visualiser vos consommations réelles et profiter pleinement du potentiel de notre service, il vous faut au minimum un des trois compteurs communicants suivants : Linky (pour l’électricité), Gazpar (pour le gaz), Téléo (pour l’eau).", - "part3_2": "Ces compteurs sont opérés par les gestionnaires de réseaux. Pour Linky, c’est Enedis, le gestionnaire de réseau de distribution d’électricité. Pour Gazpar, GRDF est responsable de cette gestion. Et pour Téléo, c’est Eau Publique du Grand Lyon.", - "part3_3": "Ces acteurs sont responsables de la relève de vos données. Ces données servent notamment à votre fournisseur d’électricité, de gaz ou d’eau pour permettre la facturation de vos consommations d’énergie. Des fournisseurs d’électricité ou de gaz il y en a des dizaines. Les gestionnaires de réseaux (… et de votre compteur) ne sont qu’au nombre de trois. Nous avons donc décidé de travailler avec eux, au plus près de la donnée brute issue de vos compteurs.", - "part3_4": "Il vous faudra donc avoir un compte chez GRDF et Eau Publique du Grand Lyon pour accéder à vos données. Si vous n’en avez pas, il suffira de vous en créer un. Ceci ne sera à faire qu’une fois, au début.", + "part3_2": "Ces compteurs sont opérés par les gestionnaires de réseaux. Pour Linky, c’est Enedis, le gestionnaire de réseau de distribution d’électricité. Pour Gazpar, GRDF est responsable de cette gestion. Quant à Téléo, c’est Eau Publique du Grand Lyon.", + "part3_3": "Ces acteurs sont responsables de la relève de vos données. Ces données servent notamment à votre fournisseur d’électricité, de gaz ou d’eau pour permettre la facturation de vos consommations. Il existe des dizaines de fournisseurs d’électricité ou de gaz, mais seulement trois gestionnaires de réseaux. La Métropole a donc décidé de travailler avec ces derniers, au plus près de la donnée brute issue de vos compteurs.", "title4": "Ecolyo se trouve dans un cloud personnel Métropole de Lyon, qu’est-ce que cela signifie\u00a0?", - "part4_1": "Comme vous avez dû le remarquer, lors de votre première connexion à Ecolyo vous avez dû vous créer un compte Cozy Métropole de Lyon. Ce cloud personnel est un espace sécurisé porté par l’ambition de vous apporter visibilité, transparence et maîtrise sur l’usage de vos données personnelles, et dont les fonctionnalités vous permettant de récupérer, synchroniser, stocker et partager vos données avec les destinataires de votre choix. Le service Ecolyo se déploie à l’intérieur de cet espace protégé. Dans ce cloud personnel, vous pourrez accéder également à d’autres services. Toutes les données traitées par Ecolyo, mais aussi les autres services que vous seriez amenés à utiliser dans ce cloud personnel restent dans ce Cloud Personnel Grand Lyon et n’en sortent pas, sauf si vous décidez vous-même de partager vos données avec des tiers.", + "part4_1": "Comme vous avez dû le remarquer, lors de votre première connexion à Ecolyo vous avez dû vous créer un Cozy Métropole de Lyon. Ce cloud personnel est un espace sécurisé conçu pour vous apporter visibilité, transparence et maîtrise sur l’usage de vos données personnelles, et dont les fonctionnalités vous permettent de récupérer, synchroniser, stocker et partager vos données avec les destinataires de votre choix. Le service Ecolyo se déploie à l’intérieur de cet espace protégé. Dans ce cloud personnel, vous pourrez accéder également à d’autres services. Toutes les données traitées par Ecolyo, ainsi que celles des autres services que vous pourriez utiliser dans ce cloud personnel restent dans ce Cloud Personnel Grand Lyon et n’en sortent pas, sauf si vous décidez vous-même de partager vos données avec des tiers.", "part4_2": "Pour en savoir plus sur ce cloud et son utilisation, ainsi que la durée de conservation de vos données, vous pouvez lire les conditions générales d’utilisation du service <a href=\"https://manager.cozygrandlyon.cloud/tos/266b4226-8417-42fb-b911-41e86dae8581.pdf?locale=fr\">ici</a>.", "title5": "Et donc concrètement pour Ecolyo, quelles données sont collectées et qui y a accès\u00a0?", - "part5_1": "Pour qu’Ecolyo ait accès à vos données de consommations, vous devrez activer vos différents connecteurs. À ce moment-là, pour la connexion aux données de gaz et d’eau à travers un parcours qui vous conduira de manière intuitive sur le site de chacun des gestionnaires de réseaux concerné, vous pourrez donner votre consentement à partager ces données avec le Service Ecolyo, et ce pour une durée limitée dans le temps. Pour l’électricité, le don du consentement et la connexion aux données se fait directement dans notre service. Quid de la durée de ce consentement\u00a0? Pour les données électriques, elle est par défaut d'un an. Pour les données gaz, cela sera à vous de la définir (nous vous recommandons 1 an pour une expérience optimale). Les données utilisées pour vérifier la bonne connexion de vos données d’électricité sont stockées du côté de la Métropole de Lyon sur des serveurs sécurisés.", + "part5_1": "Pour qu’Ecolyo ait accès à vos données de consommations, vous devrez activer vos différents connecteurs. Via les parcours proposés par l’application, vous pourrez progressivement donner votre consentement et ainsi accéder à vos données de consommations. Une facture sera nécessaire afin de préciser vos numéros de compteurs. Le consentement est donné pour une durée limitée précisée dans les écrans. Les données utilisées pour vérifier la bonne connexion de vos données d’électricité sont stockées du côté de la Métropole de Lyon sur des serveurs sécurisés pendant 5 ans, ce qui relève de la prérogative des gestionnaires de réseau.", "part5_2": "Vos connecteurs, une fois configurés, permettent le transfert de vos données de consommations au service Ecolyo. Le transfert de ces données de consommation se fait sans que personne n’accède à leur contenu, pas même la Métropole de Lyon. En effet, ces données sont stockées et traitées directement dans votre cloud personnel qui en assure la protection. Tous les calculs, analyses et traitements sur ces données sont faites DANS votre espace personnel sans visibilité sur le contenu des données par la Métropole de Lyon.", "part5_3": "Vous pouvez bien sûr mettre fin à la récupération/ au transfert de vos données de consommation sur votre espace cloud personnel à tout moment et à plusieurs niveaux : ", "part5_3_list1": "Vous pouvez supprimer le transfert quotidien de vos données en supprimant tout simplement le connecteur dans la page Consos (en bas).", - "part5_3_list2": "Pour supprimer l’ensemble de vos données ainsi que votre cloud personnel Métropole de Lyon, vous devez demander la suppression de votre cloud. Pour faire cela, rendez-vous dans les paramètres de votre cloud personnel via la barre blanche en haut d’Ecolyo et appuyez sur le bouton « Supprimer mon compte » dans la partie paramètres.", - "part5_4": "Des données sur le profil de votre foyer peuvent être également remplies au sein de l’application. Ces données s’enrichiront au fur et à mesure de votre utilisation du service avec d’autres informations : défis réalisés, étoiles gagnées, etc. L’ensemble de ces données restent à l’intérieur de votre cloud personnel, sans que personne n’y ait accès.", - "part5_5": "Enfin, afin de permettre une mise à jour quotidienne de vos données de consommations, vos identifiants Eau Publique du Grand Lyon, un jeton d’identification GRDF ainsi que vos numéros de compteurs sont stockés.", + "part5_3_list2": "Pour supprimer l’ensemble de vos données ainsi que votre cloud personnel Métropole de Lyon, vous devez demander la suppression de votre cloud. Pour faire cela, rendez-vous dans les paramètres de votre cloud personnel via la barre blanche en haut d’Ecolyo et appuyez sur le bouton « Supprimer mon compte » dans la page Paramètres.", + "part5_4": "Des données sur le profil de votre foyer peuvent être également remplies au sein de l’application. Ces données s’enrichiront au fur et à mesure de votre utilisation du service avec d’autres informations : défis réalisés, étoiles gagnées, etc. L’ensemble de ces données restent à l’intérieur de votre cloud personnel, sans droit de regard de la Métropole de Lyon.", + "part5_5": "Enfin, afin de permettre une mise à jour quotidienne de vos données de consommations, vos identifiants Eau Publique du Grand Lyon, dans le cas de l’eau, ainsi que vos informations d’identification entrées lors de votre première connexion sont stockées et utilisées dans le service.", "title6": "Personne n’a donc accès à mes données, pas même la Métropole de Lyon, vraiment\u00a0?", - "part6_1": "Conformément aux indications du paragraphe précédent, la Métropole de Lyon n’accède ni à vos données de consommations ni aux données utilisées via certaines fonctionnalités du service comme le formulaire (celui-là même qui permet l’analyse de vos consommations personnalisée ainsi qu’une sélection d’éco-gestes personnalisés). Toutes ces informations sont bien gardées au chaud dans votre cloud personnel Métropole de Lyon", + "part6_1": "Conformément aux indications du paragraphe précédent, la Métropole de Lyon n’accède ni à vos données de consommations ni aux données utilisées via certaines fonctionnalités du service comme le formulaire (celui-là même qui permet l’analyse de vos consommations personnalisée ainsi qu’une sélection d’astuces personnalisées). Toutes ces informations sont bien gardées au chaud dans votre cloud personnel Métropole de Lyon", "part6_2": "Seul vous pouvez accepter de partager vos données, documents ou fichiers privés avec la Métropole de Lyon, ses partenaires ou d’autres utilisateurs ou acteurs (publics ou privés) après recueil de votre consentement éclairé et par une action expresse de votre part.", "part6_3": "La Métropole de Lyon n'a cependant accès qu’à l’adresse email utilisée lors la création de votre cloud personnel Métropole de Lyon. Elle utilise cet email afin de vous tenir informé·e :", - "part6_3_list1": "En réponse à vos demandes, si vous avez pris l’initiative de nous contacter. ", + "part6_3_list1": "En réponse à vos demandes, si vous avez pris l’initiative de contacter la Métropole de Lyon. ", "part6_3_list2": "En cas de problème majeur avec la gestion de votre compte.", - "part6_3_list3": "De l’évolution de vos consommations, des nouveautés et de la qualité du service via une lettre mensuelle. Vous pouvez à tout moment vous désinscrire de cette lettre via la page Options du service.", - "part6_4": "Elle stocke également les informations transmises lors de la connexion à l’électricité (nom, adresse postale). Ces informations doivent être stockées car la Métropole de Lyon est régulièrement contrôlée par Enedis et doit prouver détenir le consentement des personnes pour lesquelles elle va chercher la donnée.", - "part6_5": "Par ailleurs, et dans le cadre de l’évaluation et de l’amélioration du service, des données d’utilisation anonymisées seront remontées à des fins d’exploitation statistiques. La récupération de ces statistiques anonymisées nous permettra de s’assurer du bon fonctionnement technique de la connexion à vos données de consommation ainsi que d’évaluer l’impact global en termes de baisse des consommations énergétiques de notre service. Parmi ces statistiques, des mesures d’audience de la fréquentation des différentes pages du service sont réalisées. Vous pouvez, via la page Options, à tout instant, décider de vous opposer à la récupération de ces données d’utilisation.", + "part6_3_list3": "De l’évolution de vos consommations, des nouveautés et de la qualité du service via une lettre mensuelle si vous y êtes inscrit. Vous pouvez à tout moment vous inscrire ou désinscrire de cette lettre via la page Options du service.", + "part6_4": "Elle stocke également les informations transmises lors de la connexion à l’électricité (nom, adresse postale) et au gaz (nom, code postal). Ces informations doivent être stockées car la Métropole de Lyon est régulièrement contrôlée par Enedis et GRDF et doit prouver détenir le consentement des personnes pour lesquelles elle va chercher la donnée.", + "part6_5": "Par ailleurs, et dans le cadre de l’évaluation et de l’amélioration du service, des données d’utilisation anonymisées seront remontées à des fins d’exploitation statistiques. La récupération de ces statistiques anonymisées permettra à la Métropole de LYon de s’assurer du bon fonctionnement technique de la connexion à vos données de consommation ainsi que d’évaluer l’impact global en termes de baisse des consommations énergétiques de notre service. Parmi ces statistiques, des mesures d’audience de la fréquentation des différentes pages du service sont réalisées. Vous pouvez, via la page Options, à tout instant, décider de vous opposer à la récupération de ces données d’utilisation.", "title8": "Encore des questions\u00a0?", - "part8_1": "N’hésitez pas à consulter la FAQ ou à nous contacter via le formulaire de contact présente sur l’ensemble des pages.", - "part8_2": "Dernière option, contactez-nous directement à <a href=\"mailto:ecolyo@grandlyon.com\">ecolyo(at)grandlyon.com</a>.", + "part8_1": "Il est possible de consulter la FAQ ou de contacter l’équipe via le formulaire de contact présent sur l’ensemble des pages.", + "part8_2": "Sinon, il est également possible de joindre directement à <a href=\"mailto:ecolyo@grandlyon.com\">ecolyo(at)grandlyon.com</a>.", "title9": "LEXIQUE", "part9_1_title": "Cloud personnel Métropole de Lyon : ", "part9_1_content": "Cloud personnel : appelé aussi le « domicile numérique », le cloud personnel est souvent réduit à un simple espace de stockage de documents mais il est bien plus que ça. C'est un espace individuel et sécurisé où vous pouvez accéder à des services sans exposer aucune donnée à l’extérieur. Vous seul pouvez y accéder, personne d'autre.", @@ -924,7 +932,7 @@ "legal": { "read_legal": "Lire les mentions légales", "title_legal": "Mentions légales & CGU", - "version": "Version du 12.12.2022", + "version": "Version du 20.09.2024", "site": "Site du service Ecolyo : <a href=\"https://ecolyo.com/\"> https://ecolyo.com/</a>", "adress": "Métropole de Lyon - 20, rue du Lac – CS 33569 - 69505 Lyon cedex 03", "phone": "Tél : (33) 4 78 63 40 40", @@ -936,29 +944,29 @@ "p3b": "Photographies : ", "p3": "sauf mention contraire, les photos sont la propriété de la Métropole de Lyon", "p4b": "Conception et Charte graphique : ", - "p4": "Florent Dufier", + "p4": "Métropole de Lyon, Sopra Steria", "p5b": "Réalisation technique : ", "p5": "Métropole de Lyon, Sopra Steria", "p6b": "Maintenance technique : ", "p6": "Délégation Développement économique, emploi & savoirs - Innovation numérique & systèmes d’information - Usages et services numériques - Développement des services numériques", "title1": "Crédits", - "part1": "Ce site est le résultat de développements spécifiques réalisés dans les langages Go, TypeScript, HTML et Sass. Les développements s’appuient sur plusieurs bibliothèques et frameworks libres : axios, cozy-bar, cozy-client, cozy-harvest-lib, cozy-scripts, cozy-ui, d3, global, lodash, luxon, node-sass, object-hash, react, react-dom, react-redux, react-router-dom, react-swipeable-views, redux-devtools-extension, sass-loader. Les tests de l’application s’appuient sur les bibliothèques et frameworks libres suivants: jest-junit, react-test-renderer, redux-mock-store. La pile technique intègre également les applications Cozy stack, Yarn, Docker, ACH. Les déploiements sont réalisés sur le registre hébergé chez Cozy. L’équipe de réalisation utilise au quotidien les applications GitLab, IceScrum, RocketChat, SonarQube.", + "part1": "Ce site est le résultat de développements spécifiques réalisés dans les langages Go, TypeScript, HTML et Sass. Les développements s’appuient sur plusieurs bibliothèques et frameworks libres : axios, cozy-bar, cozy-client, cozy-harvest-lib, cozy-scripts, cozy-ui, d3, global, lodash, luxon, node-sass, object-hash, react, react-dom, react-redux, react-router-dom, react-swipeable-views, redux-devtools-extension, sass-loader. Les tests de l’application s’appuient sur les bibliothèques et frameworks libres suivants: jest-junit, redux-mock-store. La pile technique intègre également les applications Cozy stack, Yarn, Docker, ACH. Les déploiements sont réalisés sur le registre hébergé chez Cozy. L’équipe de réalisation utilise au quotidien les applications GitLab, RocketChat, SonarQube.", "title2": "Traitement des données personnelles et droit d’accès, de modification et de suppression", - "part2": "Conformément à la réglementation en vigueur en matière de protection des données personnelles, le service Ecolyo a fait l’objet d’une inscription au registre des traitements de la Métropole de Lyon. Ecolyo fait partie de l’écosystème de services orientés « self data » déployés par la Métropole de Lyon avec l’ambition d’offrir aux usagers métropolitains les outils et les services leur permettant d’exercer directement leur droit à la portabilité, dans un cadre apte à garantir aussi bien la transparence et le contrôle sur l’usage de leurs données personnelles que l’exploitation directe du contenu de ces données selon leurs libres choix. Le self data est en effet selon la Fondation Internet Nouvelle Génération (FING) « la production, l’exploitation et le partage de données personnelles par les individus, sous leur contrôle et à leurs propres fins ». Au sein de cet environnement self data, la gestion des données s’appuie sur l’organisation suivante des rôles et responsabilités associées : ", - "part2-1": "Les partenaires du service GRDF et Eau Publique du Grand Lyon sont responsables exclusivement des seuls traitements de Données Personnelles relatifs à la collecte des données de consommation de gaz et d’eau de l’utilisateur et à leur transmission sur la plateforme de cloud personnel, après consentement de l’utilisateur.", - "part2-2": "Enedis est responsable de la collecte des données de consommation d’électricité et de leur mise à disposition à la Métropole de Lyon qui, elle en gère la transmission sur la plateforme du cloud personnel de l’utilisateur, après avoir récupéré le consentement de l’utilisateur.", + "part2": "Conformément à la réglementation en vigueur en matière de protection des données personnelles, le service Ecolyo a fait l’objet d’une inscription au registre des traitements de la Métropole de Lyon. Ecolyo fait partie de l’écosystème de services orientés « self data » déployés par la Métropole de Lyon avec l’ambition d’offrir aux usagers métropolitains les outils et les services leur permettant d’exercer directement leur droit à la portabilité, dans un cadre apte à garantir aussi bien la transparence et le contrôle sur l’usage de leurs données personnelles que l’exploitation directe du contenu de ces données selon leurs libres choix. Le self data était défini selon la Fondation Internet Nouvelle Génération (FING) « la production, l’exploitation et le partage de données personnelles par les individus, sous leur contrôle et à leurs propres fins ». Au sein de cet environnement self data, la gestion des données s’appuie sur l’organisation suivante des rôles et responsabilités associées : ", + "part2-1": "Eau Publique du Grand Lyon est responsable exclusivement des seuls traitements de Données Personnelles relatifs à la collecte des données de consommation de gaz et d’eau de l’utilisateur et à leur transmission sur la plateforme de cloud personnel, après consentement de l’utilisateur.", + "part2-2": "Enedis et GRDF sont responsables de la collecte des données de consommation d’électricité et de gaz et de leur mise à disposition à la Métropole de Lyon qui, elle en gère la transmission sur la plateforme du cloud personnel de l’utilisateur, après avoir récupéré le consentement de l’utilisateur.", "part2-3": "La Métropole de Lyon est responsable de traitement sur le périmètre du service Ecolyo qu’elle propose à l’usager, ainsi que des traitements nécessaires à la fourniture de la plateforme de cloud personnel qu’elle met à disposition de l’usager pour accéder au service Ecolyo. En sa qualité de responsable de ces traitements, elle collecte et traite : ", "part2-3-1": "Les données de compte de l’usager renseignées par l’usager au sein de son espace de cloud personnel Grand Lyon à des fins de gestion du compte et de communication avec l’usager ;", "part2-3-2": "Les données de compte de l’utilisateur lui permettant de connecter ses données d’électricité à Ecolyo ;", "part2-3-3": "Les données privées de consommation d’énergie et d’eau dont la récupération, la sauvegarde, le stockage, la synchronisation et le partage sur la plateforme de cloud personnel sont initiés par l’usager sans visibilité de la Métropole de Lyon sur leur contenu.", "part2-3-4": "Les données privées sur la composition du logement et du foyer de l’utilisateur, fournies par l’utilisateur lui-même au sein du service, sont traitées par le service sans visibilité de la Métropole de Lyon sur leur contenu.", "part2-3-5": "Des métriques d’usage du service anonymisées et remontées périodiquement afin d’améliorer la qualité du service et d’évaluer son impact (Plus d’informations sur la manière dont votre anonymat est bien préservé dans ce processus <a href=\"https://ecolyo.com/cloud_statistiques.html\">ici</a>).", - "part2-4": "La Métropole réalise également des mesures d’audience à l’aide de la solution Matomo. Afin de vous fournir un meilleur service et d’améliorer votre expérience d'utilisateur, nous utilisons des solutions de mesure d’audience qui utilisent la technologie des « cookies », des fichiers texte qui sont enregistrés sur votre ordinateur et qui permettent de générer des informations envoyées aux serveurs de mesure d’audience. Sur ces serveurs, l’adresse IP est anonymisée. Les données recueillies ont uniquement pour finalité de permettre d'analyser la fréquentation de nos pages afin d'en améliorer le contenu. Il s'agit de statistiques agrégées permettant de connaître le nombre de visites et de visiteurs différents, les pages les plus populaires, les chemins préférés, les niveaux d'activité par jour de la semaine et par heure de la journée, les principales erreurs etc. Vous avez cependant le droit de vous opposer à l’utilisation de ces cookies, et donc au traitement de vos données personnelles de navigation, en vous rendant dans la page Options de notre service.", - "part2-5": "L’usager est seul décisionnaire des finalités d’utilisation qu’il souhaite définir pour le traitement de ses données personnelles de consommation, à la suite de leur transmission par les partenaires du service Ecolyo sur son cloud personnel. Ainsi, dans le cadre de l’utilisation d’Ecolyo, l’usager ne recevra les données des partenaires du service : Enedis, GRDF et Eau Publique du Grand Lyon seulement qu’à sa demande expresse après la saisie de ses identifiants.", + "part2-4": "La Métropole réalise également des mesures d’audience à l’aide de la solution Matomo. Afin de vous fournir un meilleur service et d’améliorer votre expérience d'utilisateur, elle utilise des solutions de mesure d’audience qui utilisent la technologie des « cookies », des fichiers texte qui sont enregistrés sur votre ordinateur et qui permettent de générer des informations envoyées aux serveurs de mesure d’audience. Sur ces serveurs, l’adresse IP est anonymisée. Les données recueillies ont uniquement pour finalité de permettre d'analyser la fréquentation de nos pages afin d'en améliorer le contenu. Il s'agit de statistiques agrégées permettant de connaître le nombre de visites et de visiteurs différents, les pages les plus populaires, les chemins préférés, les niveaux d'activité par jour de la semaine et par heure de la journée, les principales erreurs etc. Vous avez cependant le droit de vous opposer à l’utilisation de ces cookies, et donc au traitement de vos données personnelles de navigation, en vous rendant dans la page Options de notre service.", + "part2-5": "L’usager est seul décisionnaire des finalités d’utilisation qu’il souhaite définir pour le traitement de ses données personnelles de consommation, à la suite de leur transmission par les partenaires du service Ecolyo sur son cloud personnel. Ainsi, dans le cadre de l’utilisation d’Ecolyo, l’usager ne recevra les données des partenaires du service : Enedis, GRDF et Eau Publique du Grand Lyon qu’à sa demande expresse après la saisie de ses identifiants.", "part2-6": "L’utilisateur est donc le seul à accéder :", "part2-6-1": "À ses données de consommation d’électricité horaires, journalières, hebdomadaires, mensuelles et annuelles.", "part2-6-2": "À ses données de consommation de gaz journalières, hebdomadaires, mensuelles et annuelles.", - "part2-6-3": "À ses données de consommation eau journalières, hebdomadaires, mensuelles et annuelles.", + "part2-6-3": "À ses données de consommation d'eau journalières, hebdomadaires, mensuelles et annuelles.", "part2-7": "Les engagements et responsabilités de la Métropole de Lyon concernant la protection des données et la confidentialité des données Ecolyo sont précisés dans les Mentions légales et les conditions d’utilisation du cloud personnel Grand Lyon qui accueille aujourd’hui le service Ecolyo et sans lequel le service ne peut pas fonctionner. Pour plus d’informations sur les engagements et responsabilités de la Métropole de Lyon concernant la protection et la confidentialité dans le cloud personnel, nous vous invitons à vous référer <a href=\"https://manager.cozygrandlyon.cloud/tos/266b4226-8417-42fb-b911-41e86dae8581.pdf?locale=fr\">aux mentions légales et CGU du cloud personnel métropolitain.</a> ", "part2-8": "Conformément à la loi 78-17 du 6 janvier 1978 modifiée relative à l’information, aux fichiers et aux libertés, vous disposez d’un droit d’accès, de rectification et d’opposition au traitement de vos données à caractère personnel. Votre cloud personnel vous permet d’exercer ces droits directement dans cet espace sur vos données de compte. S’agissant des données de consommations d’électricité et de gaz, l’utilisateur peut supprimer son consentement à partager ses données en supprimant son connecteur dans la page Consos.", "part2-9": "Vous pouvez également exercer vos droits d’accès, de rectification, de limitation, d’opposition et d’effacement de vos données personnelles en contactant directement le Délégué à la Protection des Données par courrier en écrivant à l’adresse :", diff --git a/src/migrations/migration.data.ts b/src/migrations/migration.data.ts index a5ec647e75ca998ecccff3f7c10d51b12689cfff..3fcd8c6a72a1e622a8a56708e2eac84f47674444 100644 --- a/src/migrations/migration.data.ts +++ b/src/migrations/migration.data.ts @@ -43,7 +43,7 @@ export const migrations: Migration[] = [ 'Removes old profileType artifacts from users database : \n - Oldest profileType is deleted \n - Removes insulation work form fields that were prone to errors \n - Changes area and outsideFacingWalls form field to strings \n - Changes updateDate values of all existing profileType to match "created_at" entry (former updateDate values got corrupted and hold no meanings).', releaseNotes: null, docTypes: PROFILETYPE_DOCTYPE, - run: async (_client: Client, docs: any[]): Promise<ProfileType[]> => { + run: (_client: Client, docs: any[]): ProfileType[] => { docs.sort(function (a, b) { const c = DateTime.fromISO(a.cozyMetadata.createdAt, { zone: 'utc', @@ -103,7 +103,7 @@ export const migrations: Migration[] = [ description: 'Removes old profileType and GCUApprovalDate from profile.', docTypes: PROFILE_DOCTYPE, releaseNotes: null, - run: async (_client: Client, docs: any[]): Promise<Profile[]> => { + run: (_client: Client, docs: any[]): Profile[] => { return docs.map(doc => { if (doc.GCUApprovalDate) { delete doc.GCUApprovalDate @@ -123,7 +123,7 @@ export const migrations: Migration[] = [ 'Updates userChallenges to make sure no quiz results are overflowing.', releaseNotes: null, docTypes: USERCHALLENGE_DOCTYPE, - run: async (_client: Client, docs: any[]): Promise<UserChallenge[]> => { + run: (_client: Client, docs: any[]): UserChallenge[] => { return docs.map(doc => { if (doc.quiz.result > 5) { doc.quiz.result = 5 @@ -155,7 +155,7 @@ export const migrations: Migration[] = [ tag: 'day', limit: 120, }, - run: async (_client: Client, docs: any[]): Promise<any[]> => { + run: (_client: Client, docs: any[]): any[] => { return docs.map(doc => { doc.deleteAction = true return doc @@ -174,7 +174,7 @@ export const migrations: Migration[] = [ tag: 'month', limit: 4, }, - run: async (_client: Client, docs: any[]): Promise<any[]> => { + run: (_client: Client, docs: any[]): any[] => { return docs.map(doc => { doc.deleteAction = true return doc @@ -193,7 +193,7 @@ export const migrations: Migration[] = [ tag: 'year', limit: 1, }, - run: async (_client: Client, docs: any[]): Promise<any[]> => { + run: (_client: Client, docs: any[]): any[] => { return docs.map(doc => { doc.deleteAction = true return doc @@ -207,7 +207,7 @@ export const migrations: Migration[] = [ description: 'Corrects individual insulation work field on profileType.', releaseNotes: null, docTypes: PROFILETYPE_DOCTYPE, - run: async (_client: Client, docs: any[]): Promise<ProfileType[]> => { + run: (_client: Client, docs: any[]): ProfileType[] => { return docs.map(doc => { if (!Array.isArray(doc.individualInsulationWork)) { doc.individualInsulationWork = [doc.individualInsulationWork] @@ -256,7 +256,7 @@ export const migrations: Migration[] = [ 'ProfileTypes start now at the begining of the month, no duplications can exist over the same month.', releaseNotes: null, docTypes: PROFILETYPE_DOCTYPE, - run: async (_client: Client, docs: any[]): Promise<any[]> => { + run: (_client: Client, docs: any[]): any[] => { function checkDate(d1: string, d2: string) { const dtd1 = DateTime.fromISO(d1) const dtd2 = DateTime.fromISO(d2) @@ -289,7 +289,7 @@ export const migrations: Migration[] = [ docTypes: FLUIDSPRICES_DOCTYPE, isCreate: true, isDeprecated: true, - run: async (): Promise<any> => { + run: (): any => { return [] }, }, @@ -301,7 +301,7 @@ export const migrations: Migration[] = [ "Profil now contains partnersIssueDate in order to handle partners' issue display", releaseNotes: null, docTypes: PROFILE_DOCTYPE, - run: async (_client: Client, docs: any[]): Promise<Profile[]> => { + run: (_client: Client, docs: any[]): Profile[] => { return docs.map(doc => { doc.partnersIssueDate = DateTime.local() .minus({ day: 1 }) @@ -318,7 +318,7 @@ export const migrations: Migration[] = [ 'Rename tutorial to onboaring in ecolyo profile, remove isLastTermAccepted', releaseNotes: null, docTypes: PROFILE_DOCTYPE, - run: async (_client: Client, docs: any[]): Promise<ProfileType[]> => { + run: (_client: Client, docs: any[]): ProfileType[] => { return docs.map(doc => { if (doc.tutorial) { doc.onboarding = { ...doc.tutorial } @@ -343,7 +343,7 @@ export const migrations: Migration[] = [ tag: 'day', limit: 1000, }, - run: async (_client: Client, docs: any[]): Promise<DataloadEntity[]> => { + run: (_client: Client, docs: any[]): DataloadEntity[] => { let prevData: DataloadEntity = { id: '', day: 0, @@ -381,7 +381,7 @@ export const migrations: Migration[] = [ tag: 'day', limit: 1000, }, - run: async (_client: Client, docs: any[]): Promise<DataloadEntity[]> => { + run: (_client: Client, docs: any[]): DataloadEntity[] => { let prevData: DataloadEntity = { id: '', day: 0, @@ -419,7 +419,7 @@ export const migrations: Migration[] = [ tag: 'month', limit: 17, }, - run: async (_client: Client, docs: any[]): Promise<DataloadEntity[]> => { + run: (_client: Client, docs: any[]): DataloadEntity[] => { return docs.map(doc => { if (doc.price) { delete doc.price @@ -440,7 +440,7 @@ export const migrations: Migration[] = [ tag: 'year', limit: 3, }, - run: async (_client: Client, docs: any[]): Promise<DataloadEntity[]> => { + run: (_client: Client, docs: any[]): DataloadEntity[] => { return docs.map(doc => { if (doc.price) { delete doc.price @@ -461,7 +461,7 @@ export const migrations: Migration[] = [ tag: 'month', limit: 17, }, - run: async (_client: Client, docs: any[]): Promise<DataloadEntity[]> => { + run: (_client: Client, docs: any[]): DataloadEntity[] => { return docs.map(doc => { if (doc.price) { delete doc.price @@ -482,7 +482,7 @@ export const migrations: Migration[] = [ tag: 'year', limit: 3, }, - run: async (_client: Client, docs: any[]): Promise<DataloadEntity[]> => { + run: (_client: Client, docs: any[]): DataloadEntity[] => { return docs.map(doc => { if (doc.price) { delete doc.price @@ -499,7 +499,7 @@ export const migrations: Migration[] = [ releaseNotes: null, docTypes: FLUIDSPRICES_DOCTYPE, isDeprecated: true, - run: async (): Promise<any> => { + run: (): any => { return [] }, }, @@ -510,7 +510,7 @@ export const migrations: Migration[] = [ description: 'Replace old minCons with the new calculation', releaseNotes: null, docTypes: ENEDIS_MONTHLY_ANALYSIS_DATA_DOCTYPE, - run: async (_client: Client, docs: any[]): Promise<any> => { + run: (_client: Client, docs: any[]): any => { return docs.map(doc => { if (doc.minLoad) { const numberofDaysInMonth = DateTime.fromObject({ @@ -532,7 +532,7 @@ export const migrations: Migration[] = [ 'Empty fluidPrices db so it can be fetched with right format from remote doctype', releaseNotes: null, docTypes: FLUIDSPRICES_DOCTYPE, - run: async (_client: Client, docs: any[]): Promise<any> => { + run: (_client: Client, docs: any[]): any => { return docs.map(doc => { doc.deleteAction = true return doc @@ -552,7 +552,7 @@ export const migrations: Migration[] = [ }, redirectLink: '/consumption/electricity', docTypes: '', - run: async (): Promise<any> => undefined, + run: (): any => undefined, isEmpty: true, }, { @@ -563,7 +563,7 @@ export const migrations: Migration[] = [ 'Profil now contains partnersIssueSeenDates in order to handle each partners issue date. Also removes previous partnersIssueDate', releaseNotes: null, docTypes: PROFILE_DOCTYPE, - run: async (_client: Client, docs: any[]): Promise<Profile[]> => { + run: (_client: Client, docs: any[]): Profile[] => { return docs.map(doc => { doc.partnersIssueSeenDate = { enedis: DateTime.local().minus({ day: 1 }).startOf('day'), @@ -584,7 +584,7 @@ export const migrations: Migration[] = [ description: 'Fix apartment typo', releaseNotes: null, docTypes: PROFILETYPE_DOCTYPE, - run: async (_client: Client, docs: any[]) => { + run: (_client: Client, docs: any[]) => { return docs.map(doc => { if (doc.housingType === 'appartment') { doc.housingType = 'apartment' @@ -600,7 +600,7 @@ export const migrations: Migration[] = [ description: 'Add garden room & equipment type', releaseNotes: null, docTypes: ECOGESTURE_DOCTYPE, - run: async (_client: Client, ecogestures: Ecogesture[]) => { + run: (_client: Client, ecogestures: Ecogesture[]) => { return ecogestures.map(ecogesture => { const ecData = ecogestureData.find( ec => ec._id === ecogesture.id @@ -615,4 +615,18 @@ export const migrations: Migration[] = [ }) }, }, + { + baseSchemaVersion: 24, + targetSchemaVersion: 25, + appVersion: '3.1.0', + description: 'Initialize isAnalysisReminderEnabled in profile', + releaseNotes: null, + docTypes: PROFILE_DOCTYPE, + run: (_client: Client, docs: Profile[]) => { + return docs.map(doc => { + doc.isAnalysisReminderEnabled = true + return doc + }) + }, + }, ] diff --git a/src/migrations/migration.service.spec.ts b/src/migrations/migration.service.spec.ts index c865c5d7adff26fcd6ee1131e4f6f751a3ac0a6c..ade8d9556fd312b63756b8ee55d1dcce3fb255bb 100644 --- a/src/migrations/migration.service.spec.ts +++ b/src/migrations/migration.service.spec.ts @@ -36,7 +36,7 @@ describe('Migration service', () => { description: 'Removing mailToken from profil', docTypes: PROFILE_DOCTYPE, releaseNotes: releaseNotes, - run: async (client: Client, docs: any[]): Promise<Profile[]> => { + run: (client: Client, docs: any[]): Profile[] => { return docs.map(doc => { if (doc.mailToken) { delete doc.mailToken @@ -67,7 +67,7 @@ describe('Migration service', () => { description: 'Removing mailToken from profil', docTypes: PROFILE_DOCTYPE, releaseNotes: releaseNotes, - run: async (client: Client, docs: any[]): Promise<Profile[]> => { + run: (client: Client, docs: any[]): Profile[] => { return [] }, }, @@ -99,7 +99,7 @@ describe('Migration service', () => { description: 'Removing mailToken from profil', docTypes: PROFILE_DOCTYPE, releaseNotes: releaseNotes, - run: async (client: Client, docs: any[]): Promise<Profile[]> => { + run: (client: Client, docs: any[]): Profile[] => { return [] }, }, @@ -135,7 +135,7 @@ describe('Migration service', () => { description: 'Removing mailToken from profil', docTypes: PROFILE_DOCTYPE, releaseNotes: releaseNotes, - run: async (client: Client, docs: any[]): Promise<Profile[]> => { + run: (client: Client, docs: any[]): Profile[] => { return [] }, }, @@ -161,7 +161,7 @@ describe('Migration service', () => { description: 'Removing mailToken from profil', docTypes: PROFILE_DOCTYPE, releaseNotes: releaseNotes, - run: async (client: Client, docs: any[]): Promise<Profile[]> => { + run: (client: Client, docs: any[]): Profile[] => { return [] }, }, @@ -172,7 +172,7 @@ describe('Migration service', () => { description: 'Removing mailToken from profil', docTypes: PROFILE_DOCTYPE, releaseNotes: releaseNotes, - run: async (client: Client, docs: any[]): Promise<Profile[]> => { + run: (client: Client, docs: any[]): Profile[] => { return [] }, }, diff --git a/src/migrations/migration.spec.ts b/src/migrations/migration.spec.ts index 03ae98d6611370f73800a205c7c26fea9b5aae44..a7535b3301b7fb48831633adf5f2e18d9fada632 100644 --- a/src/migrations/migration.spec.ts +++ b/src/migrations/migration.spec.ts @@ -20,7 +20,7 @@ describe('migration logger', () => { description: 'Removing mailToken from profil', docTypes: PROFILE_DOCTYPE, releaseNotes: null, - run: async (mockClient, docs: any[]): Promise<Profile[]> => { + run: (mockClient, docs: any[]): Profile[] => { return docs.map(doc => { if (doc.mailToken) { delete doc.mailToken @@ -63,7 +63,7 @@ describe('migration', () => { description: 'Removing mailToken from profil', docTypes: PROFILE_DOCTYPE, releaseNotes: null, - run: async (mockClient, docs: any[]): Promise<Profile[]> => { + run: (mockClient, docs: any[]): Profile[] => { return docs.map(doc => { if (doc.GCUApprovalDate) { delete doc.GCUApprovalDate @@ -160,7 +160,7 @@ describe('migration', () => { description: 'Removing mailToken from profil', docTypes: PROFILE_DOCTYPE, releaseNotes: null, - run: async (mockClient, docs: any[]): Promise<Profile[]> => { + run: (mockClient, docs: any[]): Profile[] => { return [] }, } @@ -206,7 +206,7 @@ describe('migration create', () => { docTypes: FLUIDSPRICES_DOCTYPE, releaseNotes: null, isCreate: true, - run: async (): Promise<FluidPrice[]> => { + run: (): FluidPrice[] => { return [] }, } diff --git a/src/migrations/migration.ts b/src/migrations/migration.ts index 05fa3685fc0ba9629f125248b70630e22fbf8787..0e0dea00ca76941f03ae901e8ed4c7d890b71030 100644 --- a/src/migrations/migration.ts +++ b/src/migrations/migration.ts @@ -74,6 +74,7 @@ async function updateSchemaVersion( * Save updated docs * @returns Promise<MigrationResult> */ +// eslint-disable-next-line @typescript-eslint/require-await async function save(_client: Client, docs: any[]): Promise<MigrationResult> { logApp.info('[Migration] Saving docs...') const migrationResult = migrationNoop() @@ -149,7 +150,7 @@ export async function migrate( if (migration.isDeprecated) { result = migrationNoop() } else if (docToUpdate.length && !migration.isCreate) { - const migratedDocs = await migration.run(_client, docToUpdate) + const migratedDocs = migration.run(_client, docToUpdate) if (migratedDocs.length) { result = await save(_client, migratedDocs) } else { @@ -161,11 +162,13 @@ export async function migrate( // Handle new doctype creation if (migration.isCreate && !migration.isDeprecated) { - await migration.run(_client, docToUpdate) + migration.run(_client, docToUpdate) result = { type: MIGRATION_RESULT_COMPLETE, errors: [] } } switch (result.type) { + case MIGRATION_RESULT_FAILED: + throw new Error('Migration failed') case MIGRATION_RESULT_NOOP: case MIGRATION_RESULT_COMPLETE: await updateSchemaVersion(_client, migration.targetSchemaVersion) diff --git a/src/migrations/migration.type.ts b/src/migrations/migration.type.ts index c168aeb842885ca9d3c248cb876d6e8d7b90d560..dca5840cc02c01d7360d652ee1ecbc3e9972d0c8 100644 --- a/src/migrations/migration.type.ts +++ b/src/migrations/migration.type.ts @@ -33,7 +33,7 @@ export interface Migration { queryOptions?: MigrationQueryOptions isEmpty?: boolean appVersion: string - run: (_client: Client, docs: any[]) => Promise<any[]> + run: (_client: Client, docs: any[]) => any[] } export interface MigrationQueryOptions { diff --git a/src/models/account.model.ts b/src/models/account.model.ts index 484f659fa8653174a3c5fb8917f04ce15b041d62..5aeb90f261db9443fc8b12808942dbc6fee12e4b 100644 --- a/src/models/account.model.ts +++ b/src/models/account.model.ts @@ -12,7 +12,7 @@ export interface Account extends AccountAttributes { id?: string _rev?: string _type?: string - cozyMetadata?: Record<string, any> + cozyMetadata?: Record<string, unknown> } export interface SgeAccountData { consentId: number diff --git a/src/models/analysis.model.ts b/src/models/analysis.model.ts index 62a9391f40499daef1d5aa106ff40703e5a6f729..5eb6c6cb7945998d550ee2f4e7ff3d7ae2c35485 100644 --- a/src/models/analysis.model.ts +++ b/src/models/analysis.model.ts @@ -3,4 +3,5 @@ import { DateTime } from 'luxon' export interface AnalysisState { period: 'month' | 'year' analysisMonth: DateTime + haveSeenNewsletterReminder: boolean } diff --git a/src/models/global.model.ts b/src/models/global.model.ts index d7ebab168224f8e4696089d1959a1c2638ff9bc4..43ef96a50929efdae113a512d7b283ee9459c2a3 100644 --- a/src/models/global.model.ts +++ b/src/models/global.model.ts @@ -3,7 +3,6 @@ import { TermsStatus } from 'models' import { FluidStatus } from './fluid.model' import { PartnersInfo } from './partnersInfo.model' import { ReleaseNotes } from './releaseNotes.model' -import { SgeStore } from './sgeStore.model' export interface GlobalState { screenType: ScreenType @@ -16,7 +15,6 @@ export interface GlobalState { fluidStatus: FluidStatus[] fluidTypes: FluidType[] shouldRefreshConsent: boolean - sgeConnect: SgeStore partnersInfo: PartnersInfo ecogestureFilter: Usage headerHeight: number diff --git a/src/models/profile.model.ts b/src/models/profile.model.ts index c041fb23a5f8acd5a20d4cf83dc48a518374bad1..0b0abd58b8f62f998260e44b19df0ebbc18a70df 100644 --- a/src/models/profile.model.ts +++ b/src/models/profile.model.ts @@ -15,6 +15,7 @@ export interface ProfileEntity { lastConnectionDate: string haveSeenLastAnalysis: boolean sendAnalysisNotification: boolean + isAnalysisReminderEnabled: boolean monthlyAnalysisDate: string sendConsumptionAlert: boolean waterDailyConsumptionLimit: number diff --git a/src/notifications/monthlyReport.hbs b/src/notifications/monthlyReport.hbs index bce1313b6182d34ed9da8b18b1962314be2458ec..e2030c55f045c7ba13c1ae869792f1d385df669f 100644 --- a/src/notifications/monthlyReport.hbs +++ b/src/notifications/monthlyReport.hbs @@ -24,6 +24,9 @@ <mj-section background-color="#121212"> <mj-column padding="0 32px" vertical-align="middle"> <mj-image src={{consoImageUrl}} width="132px" align="center" alt="consommation"></mj-image> + {{#unless comparisonExist }} + <mj-text color="#E3B82A" font-weight="900" font-size="20px" align="center">Oups !</mj-text> + {{/unless}} <mj-text color="white" font-weight="900" font-size="24px"> Bonjour {{username}}, </mj-text> @@ -42,12 +45,22 @@ </mj-text> {{/if}} {{/if}} - <mj-text color="#A0A0A0" font-weight="400" font-size="18px" align="center">Retrouvez le détail de vos consommations et plus d’informations dans votre bilan Ecolyo.</mj-text> - <mj-social css-class="button-with-icon" icon-size="32px" mode="horizontal" font-size="20px" font-weight="700"> - <mj-social-element src="{{baseUrl}}/assets/ecolyo-icon.png" name="ecolyo" padding="0 10px 0 0" href="{{clientUrl}}"> - J’ouvre mon Ecolyo - </mj-social-element> - </mj-social> + {{#if comparisonExist }} + <mj-text color="#A0A0A0" font-weight="400" font-size="18px" align="center">Retrouvez le détail de vos consommations et plus d’informations dans votre bilan Ecolyo.</mj-text> + <mj-social css-class="button-with-icon" icon-size="32px" mode="horizontal" font-size="20px" font-weight="700"> + <mj-social-element src="{{baseUrl}}/assets/ecolyo-icon.png" name="ecolyo" padding="0 10px 0 0" href="{{clientUrl}}"> + J’ouvre mon Ecolyo + </mj-social-element> + </mj-social> + {{/if}} + {{#unless comparisonExist }} + <mj-text color="white" font-weight="400" font-size="18px">Nous avons constaté que vous n'avez aucun compteur connecté. Sans accès à vos données de consommation, Ecolyo ne peut pas établir de bilan individuel de celles-ci.</mj-text> + <mj-social css-class="button-with-icon" icon-size="32px" mode="horizontal" font-size="20px" font-weight="700"> + <mj-social-element src="{{baseUrl}}/assets/ecolyo-icon.png" name="ecolyo" padding="0 10px 0 0" href="{{clientUrl}}"> + Je connecte un compteur + </mj-social-element> + </mj-social> + {{/unless}} </mj-column> </mj-section> {{#if isContent}} diff --git a/src/notifications/style.hbs b/src/notifications/style.hbs index b1b78eb982639ca0ea1dcfda5871e2063e0c14be..542bdaceb0a1b503862cde676b64799520aa1ef3 100644 --- a/src/notifications/style.hbs +++ b/src/notifications/style.hbs @@ -17,10 +17,10 @@ .button table { background-color: #F1C017 !important; margin: 10px !important; } .button-with-icon td { padding-right: 1px !important; } .button-with-icon table { background-color: #F1C017 !important; margin-left: 10px !important; - margin-right: 10px !important; } .button-with-icon span { vertical-align: - middle !important; } .button-with-icon a { vertical-align: middle !important; - padding-right: 10px !important;} .button-with-icon a img { padding-top: 20px - !important;} + margin-right: 10px !important; border-radius: 2px !important; } + .button-with-icon span { vertical-align: middle !important; } + .button-with-icon a { vertical-align: middle !important; padding-right: 10px + !important;} .button-with-icon a img { padding-top: 20px !important;} </mj-style> <mj-style> .custom-link a { color: #F1C017 !important; text-decoration: none !important; diff --git a/src/services/account.service.ts b/src/services/account.service.ts index 7481dbd448b5ac3d97cb0e004a0b95633b82fa6a..e7687c35facc6e9b69a1ca831b97b8f1643ce6ca 100644 --- a/src/services/account.service.ts +++ b/src/services/account.service.ts @@ -36,7 +36,10 @@ export default class AccountService { konnector: Konnector, authData: AccountEPGLData | AccountSgeData | AccountGRDFData ): Promise<Account> { - const accountAttributes: AccountAttributes = build(konnector, authData) + const accountAttributes: AccountAttributes = await build( + konnector, + authData + ) return createAccount(this._client, konnector, accountAttributes) } diff --git a/src/services/challenge.service.ts b/src/services/challenge.service.ts index d7ec39709a219a70a68148e4d140345db1c5b1b4..f6e37588a45e69972ef7fed8ad46a7b40674fbee 100644 --- a/src/services/challenge.service.ts +++ b/src/services/challenge.service.ts @@ -24,7 +24,6 @@ import { Relation, RelationEntitiesObject, TimePeriod, - UserAction, UserChallenge, UserChallengeEntity, UserDuel, @@ -169,12 +168,11 @@ export default class ChallengeService { } /** - * * @param {ChallengeEntity} challenge - get all relations entities of a challenge */ - public async getRelationEntities( + public getRelationEntities( challenge: ChallengeEntity - ): Promise<RelationEntitiesObject> { + ): RelationEntitiesObject { const duelEntityRelation = getRelationship(challenge, 'duel') const quizEntityRelation = getRelationship(challenge, 'quiz') const explorationEntityRelation = getRelationshipHasMany( @@ -343,7 +341,7 @@ export default class ChallengeService { // Case UserChallengeList is empty if (challengeEntityList.length > 0 && userChallengeList.length === 0) { for (const challenge of challengeEntityList) { - const relationEntities = await this.getRelationEntities(challenge) + const relationEntities = this.getRelationEntities(challenge) const duel = duelService.getDuelFromDuelEntities( duelEntities || [], relationEntities.duelEntityRelation._id @@ -397,7 +395,7 @@ export default class ChallengeService { ) buildList.push(userChallenge) } else { - const relationEntities = await this.getRelationEntities(challenge) + const relationEntities = this.getRelationEntities(challenge) const duel = duelService.getDuelFromDuelEntities( duelEntities || [], relationEntities.duelEntityRelation._id @@ -719,13 +717,11 @@ export default class ChallengeService { } break case UserChallengeUpdateFlag.ACTION_START: - let userAction: UserAction = userChallenge.action - if (action) { - userAction = actionService.launchAction(action) - } updatedUserChallenge = { ...userChallenge, - action: userAction, + action: action + ? actionService.launchAction(action) + : userChallenge.action, } break case UserChallengeUpdateFlag.ACTION_NOTIFICATION: diff --git a/src/services/consumption.service.spec.ts b/src/services/consumption.service.spec.ts index 149ae17033714183500eca4af8991f82b783df1a..49815f316f32121d1995426a8ab1e8512b4119b8 100644 --- a/src/services/consumption.service.spec.ts +++ b/src/services/consumption.service.spec.ts @@ -538,6 +538,7 @@ describe('Consumption service', () => { it('should throw an error when no data is available', async () => { mockClient .getStackClient() + // eslint-disable-next-line camelcase .fetchJSON.mockResolvedValueOnce({ nb_results: 0 }) const result = await consumptionDataManager.fetchAvgTemperature(2023, 5) expect(result).toBe(null) diff --git a/src/services/consumptionFormatter.service.spec.ts b/src/services/consumptionFormatter.service.spec.ts index 2c07788f299d6ea224d3b7d0bb93a5f43c52a2c5..f73ee0939489837f11662a6e7089bd09d0e9fd84 100644 --- a/src/services/consumptionFormatter.service.spec.ts +++ b/src/services/consumptionFormatter.service.spec.ts @@ -251,7 +251,7 @@ describe('ConsumptionFormatter service', () => { expect(result).toEqual(mockResult) }) it('should return an error because of unknown TimeStep', async () => { - const error = await getError(async () => + const error = await getError(() => consumptionFormatterService.formatGraphData( mockDataLoad, mockTimePeriod, diff --git a/src/services/dateChart.service.spec.ts b/src/services/dateChart.service.spec.ts index 59c9d2d5b8cad3a3e4f8b7e347809c057cc24a2c..d799abd280316dafccc2756fe95cc660db705535 100644 --- a/src/services/dateChart.service.spec.ts +++ b/src/services/dateChart.service.spec.ts @@ -296,7 +296,7 @@ describe('dateChart service', () => { }) it('should throw error for unknown TimeStep', async () => { - const error = await getError(async () => + const error = await getError(() => dateChartService.defineTimePeriod(refDate, unknownTimeStep, -1) ) expect(error).toEqual(new Error('TimeStep unknown')) @@ -693,7 +693,7 @@ describe('dateChart service', () => { const secondDate = DateTime.fromISO('2020-10-31T00:30:00.000Z', { zone: 'utc', }) - const error = await getError(async () => + const error = await getError(() => dateChartService.compareStepDate(unknownTimeStep, firstDate, secondDate) ) expect(error).toEqual(new Error('TimeStep unknown')) diff --git a/src/services/ecogesture.service.spec.ts b/src/services/ecogesture.service.spec.ts index d04e0116ba03cc6c37e8374d7974f7a9cda2d8c6..8511c0c081d43b60f88a85aa840923031a7f6fa0 100644 --- a/src/services/ecogesture.service.spec.ts +++ b/src/services/ecogesture.service.spec.ts @@ -113,7 +113,7 @@ describe('Ecogesture service', () => { }) describe('filterByUsage', () => { - it('should return ecogesture list including ECS ecogestures', async () => { + it('should return ecogesture list including ECS ecogestures', () => { const mockEcogestureList: Ecogesture[] = ecogesturesECSData const mockProfileEcogesture1: ProfileEcogesture = { ...mockProfileEcogesture, @@ -125,7 +125,7 @@ describe('Ecogesture service', () => { ) expect(result.includes(ecogesturesECSData[0])).toBeTruthy() }) - it('should return ecogesture list excluding ECS ecogestures', async () => { + it('should return ecogesture list excluding ECS ecogestures', () => { const mockEcogestureList: Ecogesture[] = ecogesturesECSData const mockProfileEcogesture1: ProfileEcogesture = { ...mockProfileEcogesture, @@ -137,7 +137,7 @@ describe('Ecogesture service', () => { ) expect(result.includes(ecogesturesECSData[1])).toBeFalsy() }) - it('should return ecogesture list including HEATING ecogestures', async () => { + it('should return ecogesture list including HEATING ecogestures', () => { const mockEcogestureList: Ecogesture[] = ecogesturesHeatingData const mockProfileEcogesture2: ProfileEcogesture = { ...mockProfileEcogesture, @@ -150,7 +150,7 @@ describe('Ecogesture service', () => { ) expect(result.includes(ecogesturesHeatingData[0])).toBeTruthy() }) - it('should return ecogesture list excluding HEATING ecogestures', async () => { + it('should return ecogesture list excluding HEATING ecogestures', () => { const mockEcogestureList: Ecogesture[] = ecogesturesHeatingData const mockProfileEcogesture2: ProfileEcogesture = { ...mockProfileEcogesture, @@ -164,7 +164,7 @@ describe('Ecogesture service', () => { }) }) describe('filterByEquipment', () => { - it('should return ecogesture list including BOILER equipment and equipment verification to true', async () => { + it('should return ecogesture list including BOILER equipment and equipment verification to true', () => { const mockProfileEcogestureBOILER: ProfileEcogesture = { ...mockProfileEcogesture, equipments: [EquipmentType.BOILER], @@ -175,7 +175,7 @@ describe('Ecogesture service', () => { ) expect(result.includes(BoilerEcogesture[0])).toBeTruthy() }) - it('should return ecogesture list excluding BOILER equipment and equipment verification to true', async () => { + it('should return ecogesture list excluding BOILER equipment and equipment verification to true', () => { const mockProfileEcogestureBOILER: ProfileEcogesture = { ...mockProfileEcogesture, } @@ -185,7 +185,7 @@ describe('Ecogesture service', () => { ) expect(result.includes(BoilerEcogesture[0])).toBeFalsy() }) - it('should return ecogesture list including BOILER equipment with equipment verification to false but equipment to BOILER', async () => { + it('should return ecogesture list including BOILER equipment with equipment verification to false but equipment to BOILER', () => { const mockProfileEcogestureBOILER: ProfileEcogesture = { ...mockProfileEcogesture, equipments: [EquipmentType.BOILER], diff --git a/src/services/environement.service.spec.ts b/src/services/environement.service.spec.ts index 4b747400619f7c59534ccfaf0cb749a3a3610ea8..7dc4afb27efb7c1570dd7823f5e8f1ca668b2779 100644 --- a/src/services/environement.service.spec.ts +++ b/src/services/environement.service.spec.ts @@ -5,11 +5,11 @@ declare const global: { __DEVELOPMENT__: boolean } -describe('Environement service', () => { +describe('Environment service', () => { const environmentService = new EnvironmentService() describe('isProduction()', () => { - it('should return true and prod url', async () => { + it('should return true and prod url', () => { global.__IS_ALPHA__ = false const result = environmentService.isProduction() expect(result).toEqual(true) @@ -17,7 +17,7 @@ describe('Environement service', () => { expect(url).toEqual('https://ecolyo-agent.apps.grandlyon.com') }) - it('should return false and rec url, alpha case', async () => { + it('should return false and rec url, alpha case', () => { global.__IS_ALPHA__ = true const result = environmentService.isProduction() expect(result).toEqual(false) diff --git a/src/services/exploration.service.spec.ts b/src/services/exploration.service.spec.ts index da61c3b29e4a2336430934e3faaaeb7a711c53c3..1a4fe2acb0a5e4035e2c4270cafce140efc2a69b 100644 --- a/src/services/exploration.service.spec.ts +++ b/src/services/exploration.service.spec.ts @@ -118,7 +118,7 @@ describe('Exploration service', () => { }) }) describe('endUserExploration Method', () => { - it('should return the finished userExploration', async () => { + it('should return the finished userExploration', () => { const result = explorationService.endUserExploration( userExplorationStarted ) diff --git a/src/services/fluid.service.spec.ts b/src/services/fluid.service.spec.ts index 8de4bf3ba466ab01c64883ba3becd9418b7ff858..5428343428c42cb7c021d6d33874de4ffb9330e4 100644 --- a/src/services/fluid.service.spec.ts +++ b/src/services/fluid.service.spec.ts @@ -529,145 +529,4 @@ describe('Fluid service', () => { expect(fluidStatus[2].maintenance).toBeTruthy() }) }) - - describe('getOldFluidData method', () => { - it('should return Electricity as old fluid', async () => { - const mockFluidStatus: FluidStatus[] = [ - { - fluidType: FluidType.ELECTRICITY, - status: FluidState.DONE, - maintenance: false, - firstDataDate: DateTime.fromISO('2019-01-01').setZone('utc', { - keepLocalTime: true, - }), - lastDataDate: DateTime.fromISO('2020-01-01').setZone('utc', { - keepLocalTime: true, - }), - connection: { - konnector: konnectorsData[0], - account: accountsData[0], - trigger: triggersData[0], - triggerState: triggerStateData, - shouldLaunchKonnector: false, - isUpdating: false, - konnectorConfig: { - name: 'Enedis', - oauth: false, - slug: FluidSlugType.ELECTRICITY, - siteLink: 'https://mon-compte-client.enedis.fr/', - activation: 'https://mon-compte-particulier.enedis.fr/donnees/', - }, - }, - }, - ] - const result = await FluidService.getOldFluidData(mockFluidStatus) - expect(result).toEqual([FluidType.ELECTRICITY]) - }) - - it('should return empty array as lastdatadate < 5 days', async () => { - const mockFluidStatus: FluidStatus[] = [ - { - fluidType: FluidType.ELECTRICITY, - status: FluidState.DONE, - maintenance: false, - firstDataDate: DateTime.local().minus({ day: 31 }).setZone('utc', { - keepLocalTime: true, - }), - lastDataDate: DateTime.local().minus({ day: 1 }).setZone('utc', { - keepLocalTime: true, - }), - connection: { - konnector: konnectorsData[0], - account: accountsData[0], - trigger: triggersData[0], - triggerState: triggerStateData, - shouldLaunchKonnector: false, - isUpdating: false, - konnectorConfig: { - name: 'Enedis', - oauth: false, - slug: FluidSlugType.ELECTRICITY, - siteLink: 'https://mon-compte-client.enedis.fr/', - activation: 'https://mon-compte-particulier.enedis.fr/donnees/', - }, - }, - }, - ] - const result = await FluidService.getOldFluidData(mockFluidStatus) - expect(result).toEqual([]) - }) - - it('should return empty array as status is NOT_CONNECTED', async () => { - const mockFluidStatus: FluidStatus[] = [ - { - fluidType: FluidType.ELECTRICITY, - status: FluidState.NOT_CONNECTED, - maintenance: false, - firstDataDate: DateTime.fromISO('2019-01-01').setZone('utc', { - keepLocalTime: true, - }), - lastDataDate: DateTime.fromISO('2020-01-01').setZone('utc', { - keepLocalTime: true, - }), - connection: { - konnector: konnectorsData[0], - account: accountsData[0], - trigger: triggersData[0], - triggerState: triggerStateData, - shouldLaunchKonnector: false, - isUpdating: false, - konnectorConfig: { - name: 'Enedis', - oauth: false, - slug: FluidSlugType.ELECTRICITY, - siteLink: 'https://mon-compte-client.enedis.fr/', - activation: 'https://mon-compte-particulier.enedis.fr/donnees/', - }, - }, - }, - ] - const result: FluidType[] = - await FluidService.getOldFluidData(mockFluidStatus) - expect(result).toEqual([]) - }) - - it('should return empty array as status is KONNECTOR_NOT_FOUND', async () => { - const mockFluidStatus: FluidStatus[] = [ - { - fluidType: FluidType.ELECTRICITY, - status: FluidState.KONNECTOR_NOT_FOUND, - maintenance: false, - firstDataDate: DateTime.fromISO('2019-01-01').setZone('utc', { - keepLocalTime: true, - }), - lastDataDate: DateTime.fromISO('2020-01-01').setZone('utc', { - keepLocalTime: true, - }), - connection: { - konnector: konnectorsData[0], - account: accountsData[0], - trigger: triggersData[0], - triggerState: triggerStateData, - shouldLaunchKonnector: false, - isUpdating: false, - konnectorConfig: { - name: 'Enedis', - oauth: false, - slug: FluidSlugType.ELECTRICITY, - siteLink: 'https://mon-compte-client.enedis.fr/', - activation: 'https://mon-compte-particulier.enedis.fr/donnees/', - }, - }, - }, - ] - const result: FluidType[] = - await FluidService.getOldFluidData(mockFluidStatus) - expect(result).toEqual([]) - }) - - it('should return empty array', async () => { - const result: FluidType[] = await FluidService.getOldFluidData([]) - expect(result).toEqual([]) - }) - }) }) diff --git a/src/services/fluid.service.ts b/src/services/fluid.service.ts index f22c17dc550c959396a9838018e09139865a5837..4cf603519df382e60d464e71951e6b7689b00a34 100644 --- a/src/services/fluid.service.ts +++ b/src/services/fluid.service.ts @@ -157,31 +157,4 @@ export default class FluidService { ] return result } - - /** - * Return fluids with data older than 5 days - */ - static getOldFluidData = async ( - fluidStatus: FluidStatus[] - ): Promise<FluidType[]> => { - const fluidOldData: FluidType[] = [] - if (fluidStatus.length > 0) { - for (const fluid of fluidStatus) { - let diffInDays = 0 - if (fluid?.lastDataDate) { - const dateToCompare = fluid.lastDataDate - diffInDays = dateToCompare.diffNow('days').toObject().days || 0 - if ( - diffInDays < -5 && - fluid.status !== FluidState.KONNECTOR_NOT_FOUND && - fluid.status !== FluidState.NOT_CONNECTED - ) { - !fluidOldData.includes(fluid.fluidType) && - fluidOldData.push(fluid.fluidType) - } - } - } - } - return fluidOldData - } } diff --git a/src/services/mail.service.ts b/src/services/mail.service.ts index 1e425524b7d7e39e1eb68e9ba873d4f726bbe7dc..534cfa4365b824709743b32b0a733e9b16daf7c7 100644 --- a/src/services/mail.service.ts +++ b/src/services/mail.service.ts @@ -11,7 +11,7 @@ export default class MailService { ): Promise<void> { try { const jobCollection = client.collection('io.cozy.jobs') - jobCollection.create('sendmail', mailInfo) + await jobCollection.create('sendmail', mailInfo) } catch (error) { const errorMessage = `Failed to send mail` logStack('error', errorMessage) diff --git a/src/services/profileTypeEntity.service.spec.ts b/src/services/profileTypeEntity.service.spec.ts index b47674c0a5f6af43d71d2ed062632255d9c1b412..fadb1a63dec7db6d95ad399e3ca75007db0987b4 100644 --- a/src/services/profileTypeEntity.service.spec.ts +++ b/src/services/profileTypeEntity.service.spec.ts @@ -1,4 +1,6 @@ import { QueryResult } from 'cozy-client' +import profileTypeDataJson from 'db/profileTypeData.json' +import { DateTime } from 'luxon' import { ProfileType } from 'models' import mockClient from 'tests/__mocks__/client.mock' import { profileTypeData } from 'tests/__mocks__/profileType.mock' @@ -8,32 +10,53 @@ describe('UserProfileTypeEntity service', () => { const pteService = new ProfileTypeEntityService(mockClient) describe('getUserProfileType', () => { + const mockProfileQueryResult: QueryResult<ProfileType[]> = { + data: [profileTypeData], + bookmark: '', + next: false, + skip: 0, + } + const mockEmptyQueryResult: QueryResult<ProfileType[]> = { + data: [], + bookmark: '', + next: false, + skip: 0, + } + it('should return the closest profileType according to passed argument updateDate', async () => { + mockClient.query.mockResolvedValueOnce(mockProfileQueryResult) + const result = await pteService.getProfileType(DateTime.local(2022, 1, 1)) + expect(result).toEqual(profileTypeData) + }) + + it('should not find a profile corresponding to the passed argument updateDate', async () => { + mockClient.query.mockResolvedValueOnce(mockEmptyQueryResult) + mockClient.query.mockResolvedValueOnce(mockProfileQueryResult) + const result = await pteService.getProfileType(DateTime.local(2022, 1, 1)) + expect(result).toEqual(profileTypeData) + }) + + it('should return default profileType if no profile found when date is given', async () => { + const date = DateTime.local(2022, 1, 1) + const defaultProfileType = { + ...profileTypeDataJson[0].profileType, + updateDate: date, + } as ProfileType + + mockClient.query.mockResolvedValueOnce(mockEmptyQueryResult) + mockClient.query.mockResolvedValueOnce(mockEmptyQueryResult) + const result = await pteService.getProfileType(date) + expect(result).toEqual(defaultProfileType) + }) it('should return the last profileType in base', async () => { - const mockQueryResult: QueryResult<ProfileType[]> = { - data: [profileTypeData], - bookmark: '', - next: false, - skip: 0, - } - mockClient.query.mockResolvedValueOnce(mockQueryResult) + mockClient.query.mockResolvedValueOnce(mockProfileQueryResult) const result = await pteService.getProfileType() expect(result).toEqual(profileTypeData) }) - // it('should return the closest profileType according to passed argument updateDate', async () => {}) - it('should return null if no user profile found', async () => { - const mockQueryResult: QueryResult<ProfileType[]> = { - data: [], - bookmark: '', - next: false, - skip: 0, - } - mockClient.query.mockResolvedValueOnce(mockQueryResult) + mockClient.query.mockResolvedValueOnce(mockEmptyQueryResult) const result = await pteService.getProfileType() expect(result).toBeNull() }) }) - - // describe('updateUserProfileType', () => {}) }) diff --git a/src/services/profileTypeEntity.service.ts b/src/services/profileTypeEntity.service.ts index b54335ba3214caa5d8c6574aceb238bbf11d4490..bda52c3ad8df0394914bf5a9b824dbbc40c3de38 100644 --- a/src/services/profileTypeEntity.service.ts +++ b/src/services/profileTypeEntity.service.ts @@ -40,10 +40,9 @@ export default class ProfileTypeEntityService { logApp.debug( 'Checking if user has already filled a profileType and uses it as default' ) - const query = Q(PROFILETYPE_DOCTYPE) const data: QueryResult<ProfileType[]> = await this._client.query(query) - if (data.data.length) { - const loadedProfileType: ProfileType = data.data[0] + if (data?.data.length) { + const loadedProfileType = data.data[0] logApp.debug( 'found oldest profileType filled by user : ', loadedProfileType @@ -51,10 +50,10 @@ export default class ProfileTypeEntityService { return loadedProfileType } else { // return default profiletype - const loadedProfileType: any = { + const loadedProfileType = { ...profileTypeData[0].profileType, updateDate: date, - } + } as ProfileType logApp.debug('No profileType were found, loading default profileType') return loadedProfileType } diff --git a/src/services/queryRunner.service.ts b/src/services/queryRunner.service.ts index 441f4a27a6dbc261c6312ffaca3078597bcfe3b1..efd0abdeb3f53c2d24da9202c395a269a43e8a59 100644 --- a/src/services/queryRunner.service.ts +++ b/src/services/queryRunner.service.ts @@ -250,6 +250,8 @@ export default class QueryRunner { }, } break + case TimeStep.HOUR: + throw new Error('Unexpected time step') } return predicate } diff --git a/src/services/quiz.service.ts b/src/services/quiz.service.ts index 6dac376e5b60fed7502cb3afa8b1003505b52916..cde3563d4bdb3933e542bce3c37a1b666864a2dd 100644 --- a/src/services/quiz.service.ts +++ b/src/services/quiz.service.ts @@ -347,6 +347,10 @@ export default class QuizService { let endTime = today const isPeriod = Object.keys(period).length !== 0 switch (interval) { + case TimeStep.HALF_AN_HOUR: + case TimeStep.HOUR: + case TimeStep.DAY: + throw new Error('Unexpected time step') case TimeStep.WEEK: startTime = isPeriod ? DateTime.fromObject(period).startOf('week') diff --git a/src/services/timePeriod.service.spec.ts b/src/services/timePeriod.service.spec.ts index abc6455fb1c27f99e61c5091ffd234a929f2193e..674c75a36f5084a0e8f95df6014af39915a25f83 100644 --- a/src/services/timePeriod.service.spec.ts +++ b/src/services/timePeriod.service.spec.ts @@ -263,7 +263,7 @@ describe('timePeriod service', () => { zone: 'utc', }), } - const error = await getError(async () => + const error = await getError(() => timePeriodService.getComparisonTimePeriod(timePeriod, unknownTimeStep) ) expect(error).toEqual(new Error('TimeStep unknown')) @@ -322,7 +322,7 @@ describe('timePeriod service', () => { expect(result).toEqual(expectedDate) }) it('should return an error because of unknown TimeStep', async () => { - const error = await getError(async () => + const error = await getError(() => timePeriodService.getLastDayOfCompletePeriod( randomDate, unknownTimeStep @@ -384,7 +384,7 @@ describe('timePeriod service', () => { expect(result).toEqual(expectedDate) }) it('should return unknown timestep', async () => { - const error = await getError(async () => + const error = await getError(() => timePeriodService.getLastDayOfTimePeriod(randomDate, unknownTimeStep) ) expect(error).toEqual(new Error('TimeStep unknown')) @@ -443,7 +443,7 @@ describe('timePeriod service', () => { expect(result).toEqual(expectedDate) }) it('should return the date of the last day of current period', async () => { - const error = await getError(async () => + const error = await getError(() => timePeriodService.getStartDateFromEndDateByTimeStep( randomDate, unknownTimeStep diff --git a/src/services/timePeriod.service.ts b/src/services/timePeriod.service.ts index 617b9b3e3b709e42295a9c5850e339560ff85e6d..263b793784686d92e266e29b34c58186c01c57df 100644 --- a/src/services/timePeriod.service.ts +++ b/src/services/timePeriod.service.ts @@ -194,7 +194,7 @@ export default class TimePeriodService { lastDay: DateTime, timeStep: TimeStep ): TimePeriod { - // calculate last day of the tobe coimpleted period + // calculate last day of the to be completed period const lastCompleteTimePeriod = { startDate: this.getStartDateFromEndDateByTimeStep(lastDay, timeStep), endDate: lastDay, diff --git a/src/store/analysis/analysis.slice.spec.ts b/src/store/analysis/analysis.slice.spec.ts index d8e8fec77c75d068a73da2098821f232de66d180..cfa64383ba5aea45ec3a75e69385264a731dc6a6 100644 --- a/src/store/analysis/analysis.slice.spec.ts +++ b/src/store/analysis/analysis.slice.spec.ts @@ -1,6 +1,11 @@ import { DateTime } from 'luxon' import { mockAnalysisState } from 'tests/__mocks__/store' -import { analysisSlice, setAnalysisMonth, setPeriod } from './analysis.slice' +import { + analysisSlice, + setAnalysisMonth, + setHaveSeenNewsletterReminder, + setPeriod, +} from './analysis.slice' describe('analysis reducer', () => { it('should return the initial state', () => { @@ -33,4 +38,17 @@ describe('analysis reducer', () => { }) }) }) + + describe('setHaveSeenNewsletterReminder', () => { + it('should handle setHaveSeenNewsletterReminder', () => { + const state = analysisSlice.reducer( + mockAnalysisState, + setHaveSeenNewsletterReminder(true) + ) + expect(state).toEqual({ + ...mockAnalysisState, + haveSeenNewsletterReminder: true, + }) + }) + }) }) diff --git a/src/store/analysis/analysis.slice.ts b/src/store/analysis/analysis.slice.ts index 11eabac17c89f295762c5bf02b5ecdc10c45a866..bc2b12deaa4e8a66d411e52983409fcafd9c3d81 100644 --- a/src/store/analysis/analysis.slice.ts +++ b/src/store/analysis/analysis.slice.ts @@ -5,6 +5,7 @@ import { AnalysisState } from 'models' const initialState: AnalysisState = { period: 'month', analysisMonth: DateTime.local().minus({ months: 1 }).startOf('day'), + haveSeenNewsletterReminder: false, } export const analysisSlice = createSlice({ @@ -17,7 +18,11 @@ export const analysisSlice = createSlice({ setAnalysisMonth: (state, action: PayloadAction<DateTime>) => { state.analysisMonth = action.payload }, + setHaveSeenNewsletterReminder: (state, action: PayloadAction<boolean>) => { + state.haveSeenNewsletterReminder = action.payload + }, }, }) -export const { setPeriod, setAnalysisMonth } = analysisSlice.actions +export const { setPeriod, setAnalysisMonth, setHaveSeenNewsletterReminder } = + analysisSlice.actions diff --git a/src/store/global/global.slice.spec.ts b/src/store/global/global.slice.spec.ts index e9d17bb0ad98ff3a59d30884cc215a44cccb5381..43ec508317a3beaada2df8065a1fc8538d83a5cc 100644 --- a/src/store/global/global.slice.spec.ts +++ b/src/store/global/global.slice.spec.ts @@ -2,7 +2,6 @@ import { FluidSlugType, FluidState, FluidType, ScreenType, Usage } from 'enums' import { DateTime } from 'luxon' import { FluidStatus, PartnersInfo, TermsStatus } from 'models' -import { SgeStore } from 'models/sgeStore.model' import { accountsData } from 'tests/__mocks__/accountsData.mock' import { konnectorsData } from 'tests/__mocks__/konnectorsData.mock' import { mockGlobalState } from 'tests/__mocks__/store' @@ -21,7 +20,6 @@ import { toggleChallengeExplorationNotification, updateEcogestureFilter, updateFluidConnection, - updateSgeStore, updateTermsStatus, } from './global.slice' @@ -231,28 +229,6 @@ describe('globalSlice', () => { shouldRefreshConsent: true, }) }) - it('should handle setSgeConnect', () => { - const expectedSgeConnect: SgeStore = { - address: 'address', - city: 'city', - currentStep: 1, - dataConsent: true, - firstName: 'firstName', - lastName: 'lastName', - pdl: 12345678901234, - pdlConfirm: true, - shouldLaunchAccount: true, - zipCode: 99999, - } - const state = globalSlice.reducer( - mockGlobalState, - updateSgeStore(expectedSgeConnect) - ) - expect(state).toEqual({ - ...mockGlobalState, - sgeConnect: expectedSgeConnect, - }) - }) it('should handle updateFluidConnection', () => { const state = globalSlice.reducer( mockGlobalState, diff --git a/src/store/global/global.slice.ts b/src/store/global/global.slice.ts index 95f659b33b0ea1d31e7730feb9402ae655756f5b..d012eec87f89230099e1bffc3393c47e281a2edd 100644 --- a/src/store/global/global.slice.ts +++ b/src/store/global/global.slice.ts @@ -7,7 +7,6 @@ import { GlobalState, Notes, PartnersInfo, - SgeStore, TermsStatus, } from 'models' @@ -107,18 +106,6 @@ const initialState: GlobalState = { notification_activated: false, }, shouldRefreshConsent: false, - sgeConnect: { - currentStep: 0, - firstName: '', - lastName: '', - pdl: null, - address: '', - zipCode: null, - city: '', - dataConsent: false, - pdlConfirm: false, - shouldLaunchAccount: false, - }, ecogestureFilter: Usage.ALL, } @@ -203,9 +190,6 @@ export const globalSlice = createSlice({ ) => { state.fluidStatus[fluidType].connection = fluidConnection }, - updateSgeStore: (state, action: PayloadAction<SgeStore>) => { - state.sgeConnect = action.payload - }, updateEcogestureFilter: (state, action: PayloadAction<Usage>) => { state.ecogestureFilter = action.payload }, @@ -225,6 +209,5 @@ export const { toggleChallengeExplorationNotification, updateEcogestureFilter, updateFluidConnection, - updateSgeStore, updateTermsStatus, } = globalSlice.actions diff --git a/src/store/profile/profile.slice.ts b/src/store/profile/profile.slice.ts index c2d5c3a5c3539b51bef2219bbfeda08974125b1c..876c44ab3b453e0a3307c40086c2f3cdfe9476fc 100644 --- a/src/store/profile/profile.slice.ts +++ b/src/store/profile/profile.slice.ts @@ -32,6 +32,7 @@ const initialState: Profile = { }), haveSeenLastAnalysis: true, sendAnalysisNotification: true, + isAnalysisReminderEnabled: false, sendConsumptionAlert: false, waterDailyConsumptionLimit: 0, mailToken: '', diff --git a/src/styles/components/_buttons.scss b/src/styles/components/_buttons.scss index ca1f7f8345d38588776604f15624d94da96903fe..c3cfc4fff498721495f1ecf7b1334eb8e518cfe3 100644 --- a/src/styles/components/_buttons.scss +++ b/src/styles/components/_buttons.scss @@ -22,8 +22,8 @@ button { } &.btnText { - text-decoration: underline; span { + text-decoration: underline; text-transform: none; font-weight: 400; } diff --git a/src/styles/index.scss b/src/styles/index.scss index 175d98f2b2e1f25451e321ade7d3ef715855af37..d1f1147728ad6c04df17e831508b26766b8176fa 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -79,7 +79,7 @@ justify-content: center; // useful when text is rendered with loader align-items: center; - background-color: $dark-light-2; + background-color: transparent; } // devtools button diff --git a/src/targets/services/monthlyReportNotification.ts b/src/targets/services/monthlyReportNotification.ts index 88f342166509782bb7ab4207adb682032593d0f2..672132e5726827f24949540b330877f5fb06bfc0 100644 --- a/src/targets/services/monthlyReportNotification.ts +++ b/src/targets/services/monthlyReportNotification.ts @@ -246,14 +246,16 @@ const monthlyReportNotification = async ({ const environmentService = new EnvironmentService() const baseUrl = environmentService.getPublicURL() + const comparisonExist = + monthComparisonText.length > 0 || yearComparisonText.length > 0 + const template = monthlyReportTemplate({ title: 'Infos & bilan consos', baseUrl: baseUrl, username: username, clientUrl: analysisLink, unsubscribeUrl: unsubscribeUrl, - comparisonExist: - monthComparisonText.length > 0 || yearComparisonText.length > 0, + comparisonExist: comparisonExist, monthComparisonExist: monthComparisonText.length > 0, monthComparisonText: monthComparisonText, yearComparisonExist: yearComparisonText.length > 0, @@ -281,7 +283,9 @@ const monthlyReportNotification = async ({ previousYear: date.year - 1, currentYear: date.year, previousMonthYear: date.month === 1 ? date.year - 1 : date.year, - consoImageUrl: baseUrl + '/assets/multifluidConsumption.png', + consoImageUrl: comparisonExist + ? baseUrl + '/assets/multifluidConsumption.png' + : baseUrl + '/assets/multifluidNoConsumption.png', feedbackImageUrl: baseUrl + '/assets/feedback.png', }) diff --git a/src/utils/utils.spec.ts b/src/utils/utils.spec.ts index 64a349dda2822c6df7a4267da875e6e07b63e964..6bd4fe0f095a1e5ceb2a332f9d3c5513b9b6a2c6 100644 --- a/src/utils/utils.spec.ts +++ b/src/utils/utils.spec.ts @@ -13,6 +13,7 @@ import { formatOffPeakHours, formatTwoDigits, getChallengeTitleWithLineReturn, + getFluidLabel, getFluidName, getFluidTypeTranslation, getFluidUnit, @@ -262,6 +263,21 @@ describe('utils test', () => { }) }) + describe('getFluidLabel', () => { + it('should return elec', () => { + expect(getFluidLabel(FluidType.ELECTRICITY)).toBe('elec') + }) + it('should return gas', () => { + expect(getFluidLabel(FluidType.GAS)).toBe('gaz') + }) + it('should return water', () => { + expect(getFluidLabel(FluidType.WATER)).toBe('water') + }) + it('should return multi', () => { + expect(getFluidLabel(FluidType.MULTIFLUID)).toBe('multi') + }) + }) + describe('formatListWithAnd', () => { it('should return empty string', () => { expect(formatListWithAnd([])).toBe('') diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 10ce560cae3060bf92779a22a43b95abe905a9a8..a2336f765e407f8bdcf6fa63a50f70a6ecf6f6cd 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -64,6 +64,19 @@ export const getPartnerKey = (fluidType: FluidType) => { } } +export const getFluidLabel = (fluidType: FluidType) => { + switch (fluidType) { + case FluidType.ELECTRICITY: + return 'elec' + case FluidType.GAS: + return 'gaz' + case FluidType.WATER: + return 'water' + case FluidType.MULTIFLUID: + return 'multi' + } +} + export function getKonnectorUpdateError(type: string) { switch (type.toUpperCase()) { case 'USER_ACTION_NEEDED.OAUTH_OUTDATED': diff --git a/tests/__mocks__/forms.mock.ts b/tests/__mocks__/forms.mock.ts new file mode 100644 index 0000000000000000000000000000000000000000..9f3f43e67924305e8024b8d7505bb8b084e8b833 --- /dev/null +++ b/tests/__mocks__/forms.mock.ts @@ -0,0 +1,14 @@ +import { SgeStore } from 'models' + +export const mockSgeState: SgeStore = { + address: '', + lastName: '', + firstName: '', + pdl: 0, + zipCode: 0, + city: '', + currentStep: 0, + dataConsent: false, + shouldLaunchAccount: false, + pdlConfirm: false, +} diff --git a/tests/__mocks__/store/analysis.state.mock.ts b/tests/__mocks__/store/analysis.state.mock.ts index 9c19d8e1d63b54d0f29d04a84ce2e3004a88ea47..8934561222754098eae6e9f7ff5efab3c62928d2 100644 --- a/tests/__mocks__/store/analysis.state.mock.ts +++ b/tests/__mocks__/store/analysis.state.mock.ts @@ -4,4 +4,5 @@ import { AnalysisState } from 'models' export const mockAnalysisState: AnalysisState = { period: 'month', analysisMonth: DateTime.local(2023, 1, 1).startOf('day'), + haveSeenNewsletterReminder: false, } diff --git a/tests/__mocks__/store/global.state.mock.ts b/tests/__mocks__/store/global.state.mock.ts index ab9dda82c8297b76e580dcc27bdc406498770d53..442b6c27d870b1cbd1a325c5436eddf8c9f375fe 100644 --- a/tests/__mocks__/store/global.state.mock.ts +++ b/tests/__mocks__/store/global.state.mock.ts @@ -93,17 +93,5 @@ export const mockGlobalState: GlobalState = { ], fluidTypes: [], shouldRefreshConsent: false, - sgeConnect: { - address: '', - city: '', - currentStep: 0, - dataConsent: false, - firstName: '', - lastName: '', - pdl: null, - pdlConfirm: false, - zipCode: null, - shouldLaunchAccount: false, - }, ecogestureFilter: Usage.ALL, } diff --git a/tests/__mocks__/store/profile.state.mock.ts b/tests/__mocks__/store/profile.state.mock.ts index cb896d683d30eddebab0b43fabca149d4a03d035..0f030bbb68454713f8d6fed13657f6fcf923313c 100644 --- a/tests/__mocks__/store/profile.state.mock.ts +++ b/tests/__mocks__/store/profile.state.mock.ts @@ -24,6 +24,7 @@ export const mockProfileState: Profile = { zone: 'utc', }), haveSeenLastAnalysis: true, + isAnalysisReminderEnabled: false, sendConsumptionAlert: false, waterDailyConsumptionLimit: 0, sendAnalysisNotification: true, diff --git a/yarn.lock b/yarn.lock index 85ddd95751bb07a5114f1fe2440ed250d107651d..22a4af88c750201b2c56d66f1baa45fd78e74c18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1840,6 +1840,108 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.8.0.tgz#11195513186f68d42fbf449f9a7136b2c0c92005" integrity sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg== +"@eslint-react/ast@1.14.3": + version "1.14.3" + resolved "https://registry.yarnpkg.com/@eslint-react/ast/-/ast-1.14.3.tgz#06db032754df5e37fb015e98803e9dba7bb2fffd" + integrity sha512-JU0619vNfl0RaqbsyyEfLJWKupJOmf5JmHt4sCAD6Y1LCW80Pi0ZbBXeCUTdCR36mA8IJxm9PoLFliQyDYj6uw== + dependencies: + "@eslint-react/tools" "1.14.3" + "@eslint-react/types" "1.14.3" + "@typescript-eslint/types" "^8.7.0" + "@typescript-eslint/typescript-estree" "^8.7.0" + "@typescript-eslint/utils" "^8.7.0" + birecord "^0.1.1" + string-ts "^2.2.0" + ts-pattern "^5.4.0" + +"@eslint-react/core@1.14.3": + version "1.14.3" + resolved "https://registry.yarnpkg.com/@eslint-react/core/-/core-1.14.3.tgz#8affe9aaa1ca95b786668e329d9fe64dd59218dd" + integrity sha512-1T/Zubn9PSwJHNN+4SnXXPb6ZjL1ILl9hN2pkPClh8IyBoCTM+u/BHTfxI3aVF5I8yWLNDaiG7nkaxmxoBEOfQ== + dependencies: + "@eslint-react/ast" "1.14.3" + "@eslint-react/jsx" "1.14.3" + "@eslint-react/shared" "1.14.3" + "@eslint-react/tools" "1.14.3" + "@eslint-react/types" "1.14.3" + "@eslint-react/var" "1.14.3" + "@typescript-eslint/scope-manager" "^8.7.0" + "@typescript-eslint/type-utils" "^8.7.0" + "@typescript-eslint/types" "^8.7.0" + "@typescript-eslint/utils" "^8.7.0" + birecord "^0.1.1" + short-unique-id "^5.2.0" + ts-pattern "^5.4.0" + +"@eslint-react/eslint-plugin@^1.14.3": + version "1.14.3" + resolved "https://registry.yarnpkg.com/@eslint-react/eslint-plugin/-/eslint-plugin-1.14.3.tgz#a146de41173c7b20564ee6656448219c2ca3efb3" + integrity sha512-U06zO3F56RAYXI0ZKTEpdwyWllV+muvi2gdC1SLARwk4AOmLAV8ke+iHW5EXBfNkCJQ3SgKRan4tpQqqwfEsMA== + dependencies: + "@eslint-react/shared" "1.14.3" + "@eslint-react/tools" "1.14.3" + "@eslint-react/types" "1.14.3" + "@typescript-eslint/scope-manager" "^8.7.0" + "@typescript-eslint/type-utils" "^8.7.0" + "@typescript-eslint/types" "^8.7.0" + "@typescript-eslint/utils" "^8.7.0" + eslint-plugin-react-debug "1.14.3" + eslint-plugin-react-dom "1.14.3" + eslint-plugin-react-hooks-extra "1.14.3" + eslint-plugin-react-naming-convention "1.14.3" + eslint-plugin-react-web-api "1.14.3" + eslint-plugin-react-x "1.14.3" + +"@eslint-react/jsx@1.14.3": + version "1.14.3" + resolved "https://registry.yarnpkg.com/@eslint-react/jsx/-/jsx-1.14.3.tgz#276d8ac6a962c999dc28362cb3a326df3200b38a" + integrity sha512-LJqS63/S8koDJNIqKZ/yLuFvVk4RiK7K3emjUFx+UXHrIdKwFDMpFkksVVbxqeMX70E+toMXgMepABE0pA54ag== + dependencies: + "@eslint-react/ast" "1.14.3" + "@eslint-react/tools" "1.14.3" + "@eslint-react/types" "1.14.3" + "@eslint-react/var" "1.14.3" + "@typescript-eslint/scope-manager" "^8.7.0" + "@typescript-eslint/types" "^8.7.0" + "@typescript-eslint/utils" "^8.7.0" + ts-pattern "^5.4.0" + +"@eslint-react/shared@1.14.3": + version "1.14.3" + resolved "https://registry.yarnpkg.com/@eslint-react/shared/-/shared-1.14.3.tgz#0a0cd1dd166f95298b5e6a52bfdcb17291d8ac13" + integrity sha512-GP+mjNZBGXq2CuwyVTE2+74K3tBixxNeaG3ho3ovpQ7e8NlTD3TOZk5vZyOJaeRqaOJoJa54PURe12Qt6loCdw== + dependencies: + "@eslint-react/tools" "1.14.3" + "@typescript-eslint/utils" "^8.7.0" + picomatch "^4.0.2" + +"@eslint-react/tools@1.14.3": + version "1.14.3" + resolved "https://registry.yarnpkg.com/@eslint-react/tools/-/tools-1.14.3.tgz#c97c04f99ad34f457a7a57c6b53f4f493df52dc6" + integrity sha512-NtewO4fWxzGtVCjAhD6NG4FwLev5Xq87KWpW92brPF+AvTzkr04abt3/14CpojJlW9L9SMK6FsX9tFVP7ZBqJQ== + +"@eslint-react/types@1.14.3": + version "1.14.3" + resolved "https://registry.yarnpkg.com/@eslint-react/types/-/types-1.14.3.tgz#75ed99a2df818ee0430e33522da1e457df7c8f07" + integrity sha512-Hi3rBCX0pAxoQs3MQYX/rt4Fxdz97U0pTjSQsm03dGUBb/BEVgrVK9SEZkCqpfPZ7NXrVhuiYudoJRUylNZyCw== + dependencies: + "@eslint-react/tools" "1.14.3" + "@typescript-eslint/types" "^8.7.0" + "@typescript-eslint/utils" "^8.7.0" + +"@eslint-react/var@1.14.3": + version "1.14.3" + resolved "https://registry.yarnpkg.com/@eslint-react/var/-/var-1.14.3.tgz#116f9db4ed8972635781dbf73e61d6400b30c548" + integrity sha512-APJJVSyrDrvJn3t4qXBg1XpWMxmW5AEym56a9/ILzLtgOWFsDj6gJCy4o2g5UB2AZqtfS6YS5IfU5B1G/wYtiw== + dependencies: + "@eslint-react/ast" "1.14.3" + "@eslint-react/tools" "1.14.3" + "@eslint-react/types" "1.14.3" + "@typescript-eslint/scope-manager" "^8.7.0" + "@typescript-eslint/types" "^8.7.0" + "@typescript-eslint/utils" "^8.7.0" + ts-pattern "^5.4.0" + "@eslint/eslintrc@^0.4.3": version "0.4.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" @@ -3378,6 +3480,14 @@ "@typescript-eslint/types" "6.7.4" "@typescript-eslint/visitor-keys" "6.7.4" +"@typescript-eslint/scope-manager@8.8.1", "@typescript-eslint/scope-manager@^8.7.0": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.8.1.tgz#b4bea1c0785aaebfe3c4ab059edaea1c4977e7ff" + integrity sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA== + dependencies: + "@typescript-eslint/types" "8.8.1" + "@typescript-eslint/visitor-keys" "8.8.1" + "@typescript-eslint/type-utils@6.7.4": version "6.7.4" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.7.4.tgz#847cd3b59baf948984499be3e0a12ff07373e321" @@ -3388,6 +3498,16 @@ debug "^4.3.4" ts-api-utils "^1.0.1" +"@typescript-eslint/type-utils@^8.0.0", "@typescript-eslint/type-utils@^8.7.0": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.8.1.tgz#31f59ec46e93a02b409fb4d406a368a59fad306e" + integrity sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA== + dependencies: + "@typescript-eslint/typescript-estree" "8.8.1" + "@typescript-eslint/utils" "8.8.1" + debug "^4.3.4" + ts-api-utils "^1.3.0" + "@typescript-eslint/types@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" @@ -3408,6 +3528,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.7.4.tgz#5d358484d2be986980c039de68e9f1eb62ea7897" integrity sha512-o9XWK2FLW6eSS/0r/tgjAGsYasLAnOWg7hvZ/dGYSSNjCh+49k5ocPN8OmG5aZcSJ8pclSOyVKP2x03Sj+RrCA== +"@typescript-eslint/types@8.8.1", "@typescript-eslint/types@^8.7.0": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.8.1.tgz#ebe85e0fa4a8e32a24a56adadf060103bef13bd1" + integrity sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q== + "@typescript-eslint/typescript-estree@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" @@ -3462,6 +3587,20 @@ semver "^7.5.4" ts-api-utils "^1.0.1" +"@typescript-eslint/typescript-estree@8.8.1", "@typescript-eslint/typescript-estree@^8.7.0": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.1.tgz#34649f4e28d32ee49152193bc7dedc0e78e5d1ec" + integrity sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg== + dependencies: + "@typescript-eslint/types" "8.8.1" + "@typescript-eslint/visitor-keys" "8.8.1" + debug "^4.3.4" + fast-glob "^3.3.2" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^1.3.0" + "@typescript-eslint/utils@6.7.4": version "6.7.4" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.7.4.tgz#2236f72b10e38277ee05ef06142522e1de470ff2" @@ -3475,6 +3614,16 @@ "@typescript-eslint/typescript-estree" "6.7.4" semver "^7.5.4" +"@typescript-eslint/utils@8.8.1", "@typescript-eslint/utils@^8.7.0": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.8.1.tgz#9e29480fbfa264c26946253daa72181f9f053c9d" + integrity sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@typescript-eslint/scope-manager" "8.8.1" + "@typescript-eslint/types" "8.8.1" + "@typescript-eslint/typescript-estree" "8.8.1" + "@typescript-eslint/utils@^5.10.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" @@ -3521,6 +3670,14 @@ "@typescript-eslint/types" "6.7.4" eslint-visitor-keys "^3.4.1" +"@typescript-eslint/visitor-keys@8.8.1": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.1.tgz#0fb1280f381149fc345dfde29f7542ff4e587fc5" + integrity sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag== + dependencies: + "@typescript-eslint/types" "8.8.1" + eslint-visitor-keys "^3.4.3" + "@webassemblyjs/ast@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" @@ -4593,6 +4750,11 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" +birecord@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/birecord/-/birecord-0.1.1.tgz#abc07c8187bf24fb1e0055cd9feb18b30e477a03" + integrity sha512-VUpsf/qykW0heRlC8LooCq28Kxn3mAqKohhDG/49rrsQ1dT1CXyj/pgXS+5BSRzFTR/3DyIBOqQOrGyZOh71Aw== + bl@^2.0.1: version "2.2.1" resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.1.tgz#8c11a7b730655c5d56898cdc871224f40fd901d5" @@ -6063,16 +6225,16 @@ cozy-client@29.2.0: sift "^6.0.0" url-search-params-polyfill "^8.0.0" -cozy-client@48.21.0: - version "48.21.0" - resolved "https://registry.yarnpkg.com/cozy-client/-/cozy-client-48.21.0.tgz#cb51d4765ad5cc42644bdc00afff8db300a9de31" - integrity sha512-kec/fcvZ8aOEa4jbbOpaDauOOHRFGw1PihowJTCcbm8P7gOVvKFoQEcLJi/QwchngWPgNt4vcPevmokhYvRCYQ== +cozy-client@49.1.1: + version "49.1.1" + resolved "https://registry.yarnpkg.com/cozy-client/-/cozy-client-49.1.1.tgz#076cd590fe06112a9fcfa93f9fad99269b563d5e" + integrity sha512-ORjQzl3LZNJVh7rWqjM2quSMsG9nq3JZRrhYWVXttjrL/bq9NTQ0EhQIm6MR7yUd3fKjjlIjIs4/Vhc9gFTCow== dependencies: "@cozy/minilog" "1.0.0" "@types/jest" "^26.0.20" "@types/lodash" "^4.14.170" btoa "^1.2.1" - cozy-stack-client "^48.16.0" + cozy-stack-client "^49.0.0" date-fns "2.29.3" json-stable-stringify "^1.0.1" lodash "^4.17.13" @@ -6188,10 +6350,10 @@ cozy-harvest-lib@9.26.14: react-markdown "^4.2.2" uuid "^3.3.2" -cozy-intent@^2.22.0: - version "2.22.0" - resolved "https://registry.yarnpkg.com/cozy-intent/-/cozy-intent-2.22.0.tgz#a4333463ca934d2a5cfe34a23b0d6f0e50d27934" - integrity sha512-aCIlwLuia5llX36eubgbkah3vR3709V7VFDWl2hndvdmyjZBHh1siJUlJxpA5nYayqJkLy7aC4bdfGpTetgoCQ== +cozy-intent@^2.23.0: + version "2.23.0" + resolved "https://registry.yarnpkg.com/cozy-intent/-/cozy-intent-2.23.0.tgz#b6f3a407413df05c108e848b9dcb074b8780824b" + integrity sha512-DFn0ny4B4HpOE+3PYuZTTa074gRnFHqID+XaJ3gY2OrPL2xUQKEZmmFLp2bPVWThi5FvgvsU3EQeWPHZNQPbaQ== dependencies: cozy-minilog "^3.3.1" post-me "0.4.5" @@ -6401,10 +6563,10 @@ cozy-stack-client@^33.4.0: mime "^2.4.0" qs "^6.7.0" -cozy-stack-client@^48.16.0: - version "48.16.0" - resolved "https://registry.yarnpkg.com/cozy-stack-client/-/cozy-stack-client-48.16.0.tgz#e8820197a0eb640e2d9061e21fad430ca5e56065" - integrity sha512-JgXAsmXESCfbC+T2EiXzUoPiDNFf2ePzNFQK98ZbWorAj8G6DcggdcG83/eD4QCwmv/1jXTaUWI1euTQdYEDQg== +cozy-stack-client@^49.0.0: + version "49.0.0" + resolved "https://registry.yarnpkg.com/cozy-stack-client/-/cozy-stack-client-49.0.0.tgz#1bda328d0d62b00bb8895be5b991d59ad6b41cfc" + integrity sha512-mlh/hR9KsIve+et17P6WXlO33FjXftzXK8ovWAKr8zk+5FcD/wy/yxV/9Mr3Q+SSabdUbFbBIqu8kZncafUzdg== dependencies: detect-node "^2.0.4" mime "^2.4.0" @@ -8017,6 +8179,60 @@ eslint-plugin-prettier@^5.0.0: prettier-linter-helpers "^1.0.0" synckit "^0.9.1" +eslint-plugin-react-debug@1.14.3: + version "1.14.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-debug/-/eslint-plugin-react-debug-1.14.3.tgz#9f47fe2d5717bda21734770d5703b9912f8f406b" + integrity sha512-qEsGT5LGFtYR1Hs9nqfrCqgE8MxrTe5VA7LO7Old8epgHgpgOGIuSIdIKYu7dxlEFGAXFB3JLW7ieYJYcgobbQ== + dependencies: + "@eslint-react/ast" "1.14.3" + "@eslint-react/core" "1.14.3" + "@eslint-react/jsx" "1.14.3" + "@eslint-react/shared" "1.14.3" + "@eslint-react/tools" "1.14.3" + "@eslint-react/types" "1.14.3" + "@eslint-react/var" "1.14.3" + "@typescript-eslint/scope-manager" "^8.7.0" + "@typescript-eslint/type-utils" "^8.7.0" + "@typescript-eslint/types" "^8.7.0" + "@typescript-eslint/utils" "^8.7.0" + string-ts "^2.2.0" + ts-pattern "^5.4.0" + +eslint-plugin-react-dom@1.14.3: + version "1.14.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-dom/-/eslint-plugin-react-dom-1.14.3.tgz#2a5a477d71941ac8ed875fad84ae6d35eb688333" + integrity sha512-tVA7RQI6Jxomeqrckqi/y1gEmcdI29b268p7K8WjRUWNUDXbZR6vEyaLBqzI8+ykO1HsK8+QhOKUHgUKHjOZBQ== + dependencies: + "@eslint-react/ast" "1.14.3" + "@eslint-react/core" "1.14.3" + "@eslint-react/jsx" "1.14.3" + "@eslint-react/shared" "1.14.3" + "@eslint-react/tools" "1.14.3" + "@eslint-react/types" "1.14.3" + "@eslint-react/var" "1.14.3" + "@typescript-eslint/scope-manager" "^8.7.0" + "@typescript-eslint/types" "^8.7.0" + "@typescript-eslint/utils" "^8.7.0" + ts-pattern "^5.4.0" + +eslint-plugin-react-hooks-extra@1.14.3: + version "1.14.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks-extra/-/eslint-plugin-react-hooks-extra-1.14.3.tgz#42d31698e593429155fcee9df8c08619f48c1854" + integrity sha512-G6mFfYiKgKbGJOUlmvcsN+n0hNiRGa9pNenv4hSlbm3TJFmlrLG+cHvOa9xe88AvaLJHfF5obgF8X/zhSekIfA== + dependencies: + "@eslint-react/ast" "1.14.3" + "@eslint-react/core" "1.14.3" + "@eslint-react/jsx" "1.14.3" + "@eslint-react/shared" "1.14.3" + "@eslint-react/tools" "1.14.3" + "@eslint-react/types" "1.14.3" + "@eslint-react/var" "1.14.3" + "@typescript-eslint/scope-manager" "^8.7.0" + "@typescript-eslint/type-utils" "^8.7.0" + "@typescript-eslint/types" "^8.7.0" + "@typescript-eslint/utils" "^8.7.0" + ts-pattern "^5.4.0" + eslint-plugin-react-hooks@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.3.0.tgz#53e073961f1f5ccf8dd19558036c1fac8c29d99a" @@ -8032,6 +8248,60 @@ eslint-plugin-react-hooks@^4.6.0: resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== +eslint-plugin-react-naming-convention@1.14.3: + version "1.14.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-naming-convention/-/eslint-plugin-react-naming-convention-1.14.3.tgz#cac4c869473a0a2cb3a11b3066d3c5cef9811eef" + integrity sha512-qj7XpwYQAKNCTloWA9vPNYDRMsiLa5H/jlF3mH17Is+j/pLH97NRG9CQXbh6kEdLbBFSsHwTDvyP22+CPVZhiA== + dependencies: + "@eslint-react/ast" "1.14.3" + "@eslint-react/core" "1.14.3" + "@eslint-react/jsx" "1.14.3" + "@eslint-react/shared" "1.14.3" + "@eslint-react/tools" "1.14.3" + "@eslint-react/types" "1.14.3" + "@typescript-eslint/scope-manager" "^8.7.0" + "@typescript-eslint/type-utils" "^8.7.0" + "@typescript-eslint/types" "^8.7.0" + "@typescript-eslint/utils" "^8.7.0" + ts-pattern "^5.4.0" + +eslint-plugin-react-web-api@1.14.3: + version "1.14.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-web-api/-/eslint-plugin-react-web-api-1.14.3.tgz#dcc381ab0a73b3d0ef59e55aafeca4d8a601b0b6" + integrity sha512-1G/WIUe+ZIPW8px1lmn7ib5fy6LcuwoHDsnq9G92iE8MFXYPA0Pry0ZKaB2lAsjP8rUROv1L9B457QjyCpro2g== + dependencies: + "@eslint-react/ast" "1.14.3" + "@eslint-react/core" "1.14.3" + "@eslint-react/jsx" "1.14.3" + "@eslint-react/shared" "1.14.3" + "@eslint-react/tools" "1.14.3" + "@eslint-react/types" "1.14.3" + "@eslint-react/var" "1.14.3" + "@typescript-eslint/scope-manager" "^8.7.0" + "@typescript-eslint/types" "^8.7.0" + "@typescript-eslint/utils" "^8.7.0" + birecord "^0.1.1" + ts-pattern "^5.4.0" + +eslint-plugin-react-x@1.14.3: + version "1.14.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-x/-/eslint-plugin-react-x-1.14.3.tgz#f9b63817e2e5075d747acf9a80867a0c0e67e945" + integrity sha512-VKyF4v1kWp9P6vI7JDJfonmny0HOQiS5v/rMLyldK9UC8k+efJN7dUtLE2Kt7TfxggE5gf+v4rsDB2Opvt5Tvg== + dependencies: + "@eslint-react/ast" "1.14.3" + "@eslint-react/core" "1.14.3" + "@eslint-react/jsx" "1.14.3" + "@eslint-react/shared" "1.14.3" + "@eslint-react/tools" "1.14.3" + "@eslint-react/types" "1.14.3" + "@eslint-react/var" "1.14.3" + "@typescript-eslint/scope-manager" "^8.7.0" + "@typescript-eslint/type-utils" "^8.7.0" + "@typescript-eslint/types" "^8.7.0" + "@typescript-eslint/utils" "^8.7.0" + is-immutable-type "5.0.0" + ts-pattern "^5.4.0" + eslint-plugin-react@7.14.3: version "7.14.3" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.14.3.tgz#911030dd7e98ba49e1b2208599571846a66bdf13" @@ -8682,6 +8952,17 @@ fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -10463,6 +10744,15 @@ is-hexadecimal@^1.0.0: resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== +is-immutable-type@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-immutable-type/-/is-immutable-type-5.0.0.tgz#2325dfa548f8c7f33a0a8926f2603ec9c2ab839f" + integrity sha512-mcvHasqbRBWJznuPqqHRKiJgYAz60sZ0mvO3bN70JbkuK7ksfmgc489aKZYxMEjIbRvyOseaTjaRZLRF/xFeRA== + dependencies: + "@typescript-eslint/type-utils" "^8.0.0" + ts-api-utils "^1.3.0" + ts-declaration-location "^1.0.4" + is-in-browser@^1.0.2, is-in-browser@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" @@ -12254,6 +12544,20 @@ minimatch@9.0.3: dependencies: brace-expansion "^2.0.1" +minimatch@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.1.tgz#ce0521856b453c86e25f2c4c0d03e6ff7ddc440b" + integrity sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + minimist-options@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -13807,6 +14111,11 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" + integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== + pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -15664,6 +15973,11 @@ semver@^7.3.6, semver@^7.5.4: dependencies: lru-cache "^6.0.0" +semver@^7.6.0: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -15860,6 +16174,11 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== +short-unique-id@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/short-unique-id/-/short-unique-id-5.2.0.tgz#a7e0668e0a8998d3151f27a36cf046055b1f270b" + integrity sha512-cMGfwNyfDZ/nzJ2k2M+ClthBIh//GlZl1JEf47Uoa9XR11bz8Pa2T2wQO4bVrRdH48LrIDWJahQziKo3MjhsWg== + side-channel@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" @@ -16301,6 +16620,11 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" +string-ts@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/string-ts/-/string-ts-2.2.0.tgz#46573f475f90f9b43c50cd01c9a603c83426bd25" + integrity sha512-VTP0LLZo4Jp9Gz5IiDVMS9WyLx/3IeYh0PXUn0NdPqusUFNgkHPWiEdbB9TU2Iv3myUskraD5WtYEdHUrQEIlQ== + string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -17062,6 +17386,23 @@ ts-api-utils@^1.0.1: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331" integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg== +ts-api-utils@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" + integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== + +ts-declaration-location@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/ts-declaration-location/-/ts-declaration-location-1.0.4.tgz#60c64133202ec5d171fdf0395f70f786f92f14c0" + integrity sha512-r4JoxYhKULbZuH81Pjrp9OEG5St7XWk7zXwGkLKhmVcjiBVHTJXV5wK6dEa9JKW5QGSTW6b1lOjxAKp8R1SQhg== + dependencies: + minimatch "^10.0.0" + +ts-pattern@^5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/ts-pattern/-/ts-pattern-5.4.0.tgz#efbe74d1ffbb63b80298dbc89b6ec442eab095fa" + integrity sha512-hgfOMfjlrARCnYtGD/xEAkFHDXuSyuqjzFSltyQCbN689uNvoQL20TVN2XFcLMjfNuwSsQGU+xtH6MrjIwhwUg== + tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"